# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from collections import OrderedDict

import json
import datetime

from odoo import api, fields, models, _
from odoo.exceptions import AccessError, ValidationError
from odoo.addons import decimal_precision as dp
from odoo.osv import expression


class LunchOrder(models.Model):
    """
    A lunch order contains one or more lunch order line(s). It is associated to a user for a given
    date. When creating a lunch order, applicable lunch alerts are displayed.
    """
    _name = 'lunch.order'
    _description = 'Lunch Order'
    _order = 'date desc'

    def _default_previous_order_ids(self):
        prev_order = self.env['lunch.order.line'].search([('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='id desc')
        # If we return return prev_order.ids, we will have duplicates (identical orders).
        # Therefore, this following part removes duplicates based on product_id and note.
        return list({
            (order.product_id, order.note): order.id
            for order in prev_order
        }.values())

    user_id = fields.Many2one('res.users', 'User', readonly=True,
                              states={'new': [('readonly', False)]},
                              default=lambda self: self.env.uid)
    date = fields.Date('Date', required=True, readonly=True,
                       states={'new': [('readonly', False)]},
                       default=fields.Date.context_today)
    order_line_ids = fields.One2many('lunch.order.line', 'order_id', 'Products',
                                     readonly=True, copy=True,
                                     states={'new': [('readonly', False)], False: [('readonly', False)]})
    total = fields.Float(compute='_compute_total', string="Total", store=True)
    state = fields.Selection([('new', 'New'),
                              ('confirmed', 'Received'),
                              ('cancelled', 'Cancelled')],
                             'Status', readonly=True, index=True, copy=False,
                             compute='_compute_order_state', store=True)
    alerts = fields.Text(compute='_compute_alerts_get', string="Alerts")
    company_id = fields.Many2one('res.company', related='user_id.company_id', store=True, readonly=False)
    currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True, store=True)
    cash_move_balance = fields.Monetary(compute='_compute_cash_move_balance', multi='cash_move_balance')
    balance_visible = fields.Boolean(compute='_compute_cash_move_balance', multi='cash_move_balance')
    previous_order_ids = fields.Many2many('lunch.order.line', compute='_compute_previous_order')
    previous_order_widget = fields.Text(compute='_compute_previous_order')

    @api.one
    @api.depends('order_line_ids')
    def _compute_total(self):
        """
        get and sum the order lines' price
        """
        self.total = sum(
            orderline.price for orderline in self.order_line_ids)

    @api.multi
    def name_get(self):
        return [(order.id, '%s %s' % (_('Lunch Order'), '#%d' % order.id)) for order in self]

    @api.depends('state')
    def _compute_alerts_get(self):
        """
        get the alerts to display on the order form
        """
        alert_msg = [alert.message
                     for alert in self.env['lunch.alert'].search([])
                     if alert.display]

        if self.state == 'new':
            self.alerts = alert_msg and '\n'.join(alert_msg) or False

    @api.multi
    @api.depends('user_id', 'state')
    def _compute_previous_order(self):
        self.ensure_one()
        self.previous_order_widget = json.dumps(False)

        prev_order = self.env['lunch.order.line'].search([('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='date desc, id desc')
        # If we use prev_order.ids, we will have duplicates (identical orders).
        # Therefore, this following part removes duplicates based on product_id and note.
        self.previous_order_ids = list({
            (order.product_id, order.note): order.id
            for order in prev_order
        }.values())

        if self.previous_order_ids:
            lunch_data = {}
            for line in self.previous_order_ids:
                lunch_data[line.id] = {
                    'line_id': line.id,
                    'product_id': line.product_id.id,
                    'product_name': line.product_id.name,
                    'supplier': line.supplier.name,
                    'note': line.note,
                    'price': line.price,
                    'date': fields.Date.to_string(line.date),
                    'currency_id': line.currency_id.id,
                }
            # sort the old lunch orders by (date, id)
            lunch_data = OrderedDict(sorted(lunch_data.items(), key=lambda t: (t[1]['date'], t[0]), reverse=True))
            self.previous_order_widget = json.dumps(lunch_data)

    @api.one
    @api.depends('user_id')
    def _compute_cash_move_balance(self):
        domain = [('user_id', '=', self.user_id.id)]
        lunch_cash = self.env['lunch.cashmove'].read_group(domain, ['amount', 'user_id'], ['user_id'])
        if len(lunch_cash):
            self.cash_move_balance = lunch_cash[0]['amount']
        self.balance_visible = (self.user_id == self.env.user) or self.user_has_groups('lunch.group_lunch_manager')

    @api.one
    @api.constrains('date')
    def _check_date(self):
        """
        Prevents the user to create an order in the past
        """
        date_order = self.date
        date_today = fields.Date.context_today(self)
        if date_order < date_today:
            raise ValidationError(_('The date of your order is in the past.'))

    @api.one
    @api.depends('order_line_ids.state')
    def _compute_order_state(self):
        """
        Update the state of lunch.order based on its orderlines. Here is the logic:
        - if at least one order line is cancelled, the order is set as cancelled
        - if no line is cancelled but at least one line is not confirmed, the order is set as new
        - if all lines are confirmed, the order is set as confirmed
        """
        if not self.order_line_ids:
            self.state = 'new'
        else:
            isConfirmed = True
            for orderline in self.order_line_ids:
                if orderline.state == 'cancelled':
                    self.state = 'cancelled'
                    return
                elif orderline.state == 'confirmed':
                    continue
                else:
                    isConfirmed = False

            if isConfirmed:
                self.state = 'confirmed'
            else:
                self.state = 'new'
        return


class LunchOrderLine(models.Model):
    _name = 'lunch.order.line'
    _description = 'Lunch Order Line'
    _order = 'date desc, id desc'

    name = fields.Char(related='product_id.name', string="Product Name", readonly=True)
    order_id = fields.Many2one('lunch.order', 'Order', ondelete='cascade', required=True)
    product_id = fields.Many2one('lunch.product', 'Product', required=True,
                                 domain=[('available', '=', True)])
    category_id = fields.Many2one('lunch.product.category', string='Product Category',
                                  related='product_id.category_id', readonly=True, store=True)
    date = fields.Date(string='Date', related='order_id.date', readonly=True, store=True)
    supplier = fields.Many2one('res.partner', string='Vendor', related='product_id.supplier',
                               readonly=True, store=True)
    user_id = fields.Many2one('res.users', string='User', related='order_id.user_id',
                              readonly=True, store=True)
    note = fields.Text('Note')
    price = fields.Float(related='product_id.price', readonly=True, store=True,
                         digits=dp.get_precision('Account'))
    state = fields.Selection([('new', 'New'),
                              ('confirmed', 'Received'),
                              ('ordered', 'Ordered'),
                              ('cancelled', 'Cancelled')],
                             'Status', readonly=True, index=True, default='new')
    cashmove = fields.One2many('lunch.cashmove', 'order_id', 'Cash Move')
    currency_id = fields.Many2one('res.currency', related='order_id.currency_id', readonly=False)

    def _check_supplier_availibility(self):
        products = self.mapped('product_id')
        if not all(product.available for product in products):
            supplier_name = ", ".join(product.supplier.display_name for product in products if not product.available)
            raise ValidationError(_("Vendor(s) '%s' is not available today") % supplier_name)

    @api.model
    def create(self, vals):
        """ Override as an onchange would not apply if using the history buttons """
        res = super(LunchOrderLine, self).create(vals)
        res.with_context(lunch_date=res.order_id.date)._check_supplier_availibility()
        return res

    @api.multi
    def write(self, vals):
        """ Override as an onchange would not apply if using the history buttons """
        res = super(LunchOrderLine, self).write(vals)
        if vals.get('product_id'):
            for line in self:
                line.with_context(lunch_date=line.order_id.date)._check_supplier_availibility()
        return res

    def order(self):
        """
        The order_line is ordered to the vendor but isn't received yet
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            self.write({'state': 'ordered'})

            order = {
                'supplier': False,
                'company': False,
                'currency': False,
            }
            group_lines = {}
            for line in self:
                if not line.supplier:
                    # do not send emails for products with no suppliers
                    continue

                if order['supplier'] and line.supplier != order['supplier']:
                    raise ValidationError(_("Validate order for one supplier at a time to send emails (mixed orders from %s and %s)") % (
                                            order['supplier'].display_name, line.supplier.display_name))
                order['supplier'] = line.supplier

                if order['company'] and line.order_id.company_id != order['company']:
                    raise ValidationError(_("Validate order for one company at a time to send emails (mixed orders from %s and %s)") % (
                                            order['company'].name, line.order_id.company_id.name))
                order['company'] = line.order_id.company_id

                if order['currency'] and line.currency_id != order['currency']:
                    raise ValidationError(_("Validate order for one currency at a time to send emails (mixed orders from %s and %s)") % (
                                            order['currency'].name, line.currency_id.name))
                order['currency'] = line.currency_id

                # group the order by products and note
                key = (line.product_id, line.note)
                group_lines.setdefault(key, 0)
                group_lines[key] += 1

            order['company_name'] = order['company'].name
            order['currency_id'] = order['currency'].id
            order['supplier_id'] = order['supplier'].id
            order['supplier_name'] = order['supplier'].name
            order['supplier_email'] = order['supplier'].email_formatted

            lines = []
            # sort by product name, note
            for product, note in sorted(group_lines, key=lambda k: (k[0].name, bool(k[1]))):
                quantity = group_lines[(product, note)]
                lines.append({
                    'product': product.name,
                    'note': note or '',
                    'quantity': quantity,
                    'price': product.price * quantity,
                })

            order['amount_total'] = sum(l['price'] for l in lines)

            template = self.env.ref('lunch.lunch_order_mail_supplier', raise_if_not_found=False)
            ctx = dict(
                default_composition_mode='mass_mail',
                default_use_template=bool(template),
                default_template_id=template.id,
                default_lang=order['supplier'].lang or self.env.user.lang,
                order=order,
                lines=lines,
            )
            return {
                'type': 'ir.actions.act_window',
                'view_type': 'form',
                'view_mode': 'form',
                'res_model': 'mail.compose.message',
                'target': 'new',
                'context': ctx,
            }

        else:
            raise AccessError(_("Only your lunch manager processes the orders."))

    @api.one
    def confirm(self):
        """
        confirm one or more order line, update order status and create new cashmove
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            if self.state != 'confirmed':
                values = {
                    'user_id': self.user_id.id,
                    'amount': -self.price,
                    'description': self.product_id.name,
                    'order_id': self.id,
                    'state': 'order',
                    'date': self.date,
                }
                self.env['lunch.cashmove'].create(values)
                self.state = 'confirmed'
        else:
            raise AccessError(_("Only your lunch manager sets the orders as received."))

    @api.one
    def cancel(self):
        """
        cancel one or more order.line, update order status and unlink existing cashmoves
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            self.state = 'cancelled'
            self.cashmove.unlink()
        else:
            raise AccessError(_("Only your lunch manager cancels the orders."))


class LunchProduct(models.Model):
    """ Products available to order. A product is linked to a specific vendor. """
    _name = 'lunch.product'
    _description = 'Lunch Product'

    name = fields.Char('Product', required=True)
    category_id = fields.Many2one('lunch.product.category', 'Product Category', required=True)
    description = fields.Text('Description')
    price = fields.Float('Price', digits=dp.get_precision('Account'))
    supplier = fields.Many2one('res.partner', 'Vendor')
    active = fields.Boolean(default=True)
    available = fields.Boolean(compute='_get_available_product', search='_search_available_products')

    @api.depends('supplier')
    def _get_available_product(self):
        for product in self:
            if not product.supplier:
                product.available = True
            else:
                alerts = self.env['lunch.alert'].search([
                    ('partner_id', '=', self.supplier.id)
                ])
                if alerts and not any(alert.display for alert in alerts):
                    # every alert is not available
                    product.available = False
                else:
                    # no alert for the supplier or at least one is not available
                    product.available = True

    def _search_available_products(self, operator, value):
        alerts = self.env['lunch.alert'].search([])
        supplier_w_alerts = alerts.mapped('partner_id')
        available_suppliers = alerts.filtered(lambda a: a.display).mapped('partner_id')
        available_products = self.search([
            '|',
                ('supplier', 'not in', supplier_w_alerts.ids),
                ('supplier', 'in', available_suppliers.ids)
        ])

        if (operator in expression.NEGATIVE_TERM_OPERATORS and value) or \
           (operator not in expression.NEGATIVE_TERM_OPERATORS and not value):
            # e.g. (available = False) or (available != True)
            return [('id', 'not in', available_products.ids)]
        else:
            # e.g. (available = True) or (available != False)
            return [('id', 'in', available_products.ids)]


class LunchProductCategory(models.Model):
    """ Category of the product such as pizza, sandwich, pasta, chinese, burger... """
    _name = 'lunch.product.category'
    _description = 'Lunch Product Category'

    name = fields.Char('Product Category', required=True)


class LunchCashMove(models.Model):
    """ Two types of cashmoves: payment (credit) or order (debit) """
    _name = 'lunch.cashmove'
    _description = 'Lunch Cashmove'

    user_id = fields.Many2one('res.users', 'User',
                              default=lambda self: self.env.uid)
    date = fields.Date('Date', required=True, default=fields.Date.context_today)
    amount = fields.Float('Amount', required=True, help='Can be positive (payment) or negative (order or payment if user wants to get his money back)')
    description = fields.Text('Description', help='Can be an order or a payment')
    order_id = fields.Many2one('lunch.order.line', 'Order', ondelete='cascade')
    state = fields.Selection([('order', 'Order'), ('payment', 'Payment')],
                             'Is an order or a payment', default='payment')

    @api.multi
    def name_get(self):
        return [(cashmove.id, '%s %s' % (_('Lunch Cashmove'), '#%d' % cashmove.id)) for cashmove in self]


class LunchAlert(models.Model):
    """ Alerts to display during a lunch order. An alert can be specific to a
    given day, weekly or daily. The alert is displayed from start to end hour. """
    _name = 'lunch.alert'
    _description = 'Lunch Alert'
    _rec_name = 'message'

    display = fields.Boolean(compute='_compute_display_get')
    message = fields.Text('Message', required=True)
    alert_type = fields.Selection([('specific', 'Specific Day'),
                                   ('week', 'Every Week'),
                                   ('days', 'Every Day')],
                                  string='Recurrence', required=True, index=True, default='specific')
    partner_id = fields.Many2one('res.partner', string="Vendor",
                                 help="If specified, the selected vendor can be ordered only on selected days")
    specific_day = fields.Date('Day', default=fields.Date.context_today)
    monday = fields.Boolean('Monday')
    tuesday = fields.Boolean('Tuesday')
    wednesday = fields.Boolean('Wednesday')
    thursday = fields.Boolean('Thursday')
    friday = fields.Boolean('Friday')
    saturday = fields.Boolean('Saturday')
    sunday = fields.Boolean('Sunday')
    start_hour = fields.Float('Between', oldname='active_from', required=True, default=7)
    end_hour = fields.Float('And', oldname='active_to', required=True, default=23)
    active = fields.Boolean(default=True)

    @api.multi
    def name_get(self):
        return [(alert.id, '%s %s' % (_('Alert'), '#%d' % alert.id)) for alert in self]

    @api.depends('alert_type', 'specific_day', 'monday', 'tuesday', 'thursday',
                 'friday', 'saturday', 'sunday', 'start_hour', 'end_hour')
    def _compute_display_get(self):
        """
        This method check if the alert can be displayed today
        if alert type is specific : compare specific_day(date) with today's date
        if alert type is week : check today is set as alert (checkbox true) eg. self['monday']
        if alert type is day : True
        return : Message if can_display_alert is True else False
        """
        days_codes = {'0': 'sunday',
                      '1': 'monday',
                      '2': 'tuesday',
                      '3': 'wednesday',
                      '4': 'thursday',
                      '5': 'friday',
                      '6': 'saturday'}
        fullday = False
        now = datetime.datetime.now()
        if self.env.context.get('lunch_date'):
            # lunch_date is a fields.Date -> 00:00:00
            lunch_date = fields.Datetime.from_string(self.env.context['lunch_date'])
            # if lunch_date is in the future, planned lunch, ignore hours
            fullday = lunch_date > now
            now = max(lunch_date, now)
        mynow = fields.Datetime.context_timestamp(self, now)

        for alert in self:
            can_display_alert = {
                'specific': (str(alert.specific_day) == fields.Date.to_string(mynow)),
                'week': alert[days_codes[mynow.strftime('%w')]],
                'days': True
            }

            if can_display_alert[alert.alert_type]:
                hour_to = int(alert.end_hour)
                min_to = int((alert.end_hour - hour_to) * 60)
                to_alert = datetime.time(hour_to, min_to)
                hour_from = int(alert.start_hour)
                min_from = int((alert.start_hour - hour_from) * 60)
                from_alert = datetime.time(hour_from, min_from)

                if fullday or (from_alert <= mynow.time() <= to_alert):
                    alert.display = True
                else:
                    alert.display = False

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from collections import OrderedDict

import json
import datetime

from odoo import api, fields, models, _
from odoo.exceptions import AccessError, ValidationError
from odoo.addons import decimal_precision as dp
from odoo.osv import expression


class LunchOrder(models.Model):
    """
    A lunch order contains one or more lunch order line(s). It is associated to a user for a given
    date. When creating a lunch order, applicable lunch alerts are displayed.
    """
    _name = 'lunch.order'
    _description = 'Lunch Order'
    _order = 'date desc'

    def _default_previous_order_ids(self):
        prev_order = self.env['lunch.order.line'].search([('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='id desc')
        # If we return return prev_order.ids, we will have duplicates (identical orders).
        # Therefore, this following part removes duplicates based on product_id and note.
        return list({
            (order.product_id, order.note): order.id
            for order in prev_order
        }.values())

    user_id = fields.Many2one('res.users', 'User', readonly=True,
                              states={'new': [('readonly', False)]},
                              default=lambda self: self.env.uid)
    date = fields.Date('Date', required=True, readonly=True,
                       states={'new': [('readonly', False)]},
                       default=fields.Date.context_today)
    order_line_ids = fields.One2many('lunch.order.line', 'order_id', 'Products',
                                     readonly=True, copy=True,
                                     states={'new': [('readonly', False)], False: [('readonly', False)]})
    total = fields.Float(compute='_compute_total', string="Total", store=True)
    state = fields.Selection([('new', 'New'),
                              ('confirmed', 'Received'),
                              ('cancelled', 'Cancelled')],
                             'Status', readonly=True, index=True, copy=False,
                             compute='_compute_order_state', store=True)
    alerts = fields.Text(compute='_compute_alerts_get', string="Alerts")
    company_id = fields.Many2one('res.company', related='user_id.company_id', store=True, readonly=False)
    currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True, store=True)
    cash_move_balance = fields.Monetary(compute='_compute_cash_move_balance', multi='cash_move_balance')
    balance_visible = fields.Boolean(compute='_compute_cash_move_balance', multi='cash_move_balance')
    previous_order_ids = fields.Many2many('lunch.order.line', compute='_compute_previous_order')
    previous_order_widget = fields.Text(compute='_compute_previous_order')

    @api.one
    @api.depends('order_line_ids')
    def _compute_total(self):
        """
        get and sum the order lines' price
        """
        self.total = sum(
            orderline.price for orderline in self.order_line_ids)

    @api.multi
    def name_get(self):
        return [(order.id, '%s %s' % (_('Lunch Order'), '#%d' % order.id)) for order in self]

    @api.depends('state')
    def _compute_alerts_get(self):
        """
        get the alerts to display on the order form
        """
        alert_msg = [alert.message
                     for alert in self.env['lunch.alert'].search([])
                     if alert.display]

        if self.state == 'new':
            self.alerts = alert_msg and '\n'.join(alert_msg) or False

    @api.multi
    @api.depends('user_id', 'state')
    def _compute_previous_order(self):
        self.ensure_one()
        self.previous_order_widget = json.dumps(False)

        prev_order = self.env['lunch.order.line'].search([('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='date desc, id desc')
        # If we use prev_order.ids, we will have duplicates (identical orders).
        # Therefore, this following part removes duplicates based on product_id and note.
        self.previous_order_ids = list({
            (order.product_id, order.note): order.id
            for order in prev_order
        }.values())

        if self.previous_order_ids:
            lunch_data = {}
            for line in self.previous_order_ids:
                lunch_data[line.id] = {
                    'line_id': line.id,
                    'product_id': line.product_id.id,
                    'product_name': line.product_id.name,
                    'supplier': line.supplier.name,
                    'note': line.note,
                    'price': line.price,
                    'date': fields.Date.to_string(line.date),
                    'currency_id': line.currency_id.id,
                }
            # sort the old lunch orders by (date, id)
            lunch_data = OrderedDict(sorted(lunch_data.items(), key=lambda t: (t[1]['date'], t[0]), reverse=True))
            self.previous_order_widget = json.dumps(lunch_data)

    @api.one
    @api.depends('user_id')
    def _compute_cash_move_balance(self):
        domain = [('user_id', '=', self.user_id.id)]
        lunch_cash = self.env['lunch.cashmove'].read_group(domain, ['amount', 'user_id'], ['user_id'])
        if len(lunch_cash):
            self.cash_move_balance = lunch_cash[0]['amount']
        self.balance_visible = (self.user_id == self.env.user) or self.user_has_groups('lunch.group_lunch_manager')

    @api.one
    @api.constrains('date')
    def _check_date(self):
        """
        Prevents the user to create an order in the past
        """
        date_order = self.date
        date_today = fields.Date.context_today(self)
        if date_order < date_today:
            raise ValidationError(_('The date of your order is in the past.'))

    @api.one
    @api.depends('order_line_ids.state')
    def _compute_order_state(self):
        """
        Update the state of lunch.order based on its orderlines. Here is the logic:
        - if at least one order line is cancelled, the order is set as cancelled
        - if no line is cancelled but at least one line is not confirmed, the order is set as new
        - if all lines are confirmed, the order is set as confirmed
        """
        if not self.order_line_ids:
            self.state = 'new'
        else:
            isConfirmed = True
            for orderline in self.order_line_ids:
                if orderline.state == 'cancelled':
                    self.state = 'cancelled'
                    return
                elif orderline.state == 'confirmed':
                    continue
                else:
                    isConfirmed = False

            if isConfirmed:
                self.state = 'confirmed'
            else:
                self.state = 'new'
        return


class LunchOrderLine(models.Model):
    _name = 'lunch.order.line'
    _description = 'Lunch Order Line'
    _order = 'date desc, id desc'

    name = fields.Char(related='product_id.name', string="Product Name", readonly=True)
    order_id = fields.Many2one('lunch.order', 'Order', ondelete='cascade', required=True)
    product_id = fields.Many2one('lunch.product', 'Product', required=True,
                                 domain=[('available', '=', True)])
    category_id = fields.Many2one('lunch.product.category', string='Product Category',
                                  related='product_id.category_id', readonly=True, store=True)
    date = fields.Date(string='Date', related='order_id.date', readonly=True, store=True)
    supplier = fields.Many2one('res.partner', string='Vendor', related='product_id.supplier',
                               readonly=True, store=True)
    user_id = fields.Many2one('res.users', string='User', related='order_id.user_id',
                              readonly=True, store=True)
    note = fields.Text('Note')
    price = fields.Float(related='product_id.price', readonly=True, store=True,
                         digits=dp.get_precision('Account'))
    state = fields.Selection([('new', 'New'),
                              ('confirmed', 'Received'),
                              ('ordered', 'Ordered'),
                              ('cancelled', 'Cancelled')],
                             'Status', readonly=True, index=True, default='new')
    cashmove = fields.One2many('lunch.cashmove', 'order_id', 'Cash Move')
    currency_id = fields.Many2one('res.currency', related='order_id.currency_id', readonly=False)

    def _check_supplier_availibility(self):
        products = self.mapped('product_id')
        if not all(product.available for product in products):
            supplier_name = ", ".join(product.supplier.display_name for product in products if not product.available)
            raise ValidationError(_("Vendor(s) '%s' is not available today") % supplier_name)

    @api.model
    def create(self, vals):
        """ Override as an onchange would not apply if using the history buttons """
        res = super(LunchOrderLine, self).create(vals)
        res.with_context(lunch_date=res.order_id.date)._check_supplier_availibility()
        return res

    @api.multi
    def write(self, vals):
        """ Override as an onchange would not apply if using the history buttons """
        res = super(LunchOrderLine, self).write(vals)
        if vals.get('product_id'):
            for line in self:
                line.with_context(lunch_date=line.order_id.date)._check_supplier_availibility()
        return res

    def order(self):
        """
        The order_line is ordered to the vendor but isn't received yet
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            self.write({'state': 'ordered'})

            order = {
                'supplier': False,
                'company': False,
                'currency': False,
            }
            group_lines = {}
            for line in self:
                if not line.supplier:
                    # do not send emails for products with no suppliers
                    continue

                if order['supplier'] and line.supplier != order['supplier']:
                    raise ValidationError(_("Validate order for one supplier at a time to send emails (mixed orders from %s and %s)") % (
                                            order['supplier'].display_name, line.supplier.display_name))
                order['supplier'] = line.supplier

                if order['company'] and line.order_id.company_id != order['company']:
                    raise ValidationError(_("Validate order for one company at a time to send emails (mixed orders from %s and %s)") % (
                                            order['company'].name, line.order_id.company_id.name))
                order['company'] = line.order_id.company_id

                if order['currency'] and line.currency_id != order['currency']:
                    raise ValidationError(_("Validate order for one currency at a time to send emails (mixed orders from %s and %s)") % (
                                            order['currency'].name, line.currency_id.name))
                order['currency'] = line.currency_id

                # group the order by products and note
                key = (line.product_id, line.note)
                group_lines.setdefault(key, 0)
                group_lines[key] += 1

            order['company_name'] = order['company'].name
            order['currency_id'] = order['currency'].id
            order['supplier_id'] = order['supplier'].id
            order['supplier_name'] = order['supplier'].name
            order['supplier_email'] = order['supplier'].email_formatted

            lines = []
            # sort by product name, note
            for product, note in sorted(group_lines, key=lambda k: (k[0].name, bool(k[1]))):
                quantity = group_lines[(product, note)]
                lines.append({
                    'product': product.name,
                    'note': note or '',
                    'quantity': quantity,
                    'price': product.price * quantity,
                })

            order['amount_total'] = sum(l['price'] for l in lines)

            template = self.env.ref('lunch.lunch_order_mail_supplier', raise_if_not_found=False)
            ctx = dict(
                default_composition_mode='mass_mail',
                default_use_template=bool(template),
                default_template_id=template.id,
                default_lang=order['supplier'].lang or self.env.user.lang,
                order=order,
                lines=lines,
            )
            return {
                'type': 'ir.actions.act_window',
                'view_type': 'form',
                'view_mode': 'form',
                'res_model': 'mail.compose.message',
                'target': 'new',
                'context': ctx,
            }

        else:
            raise AccessError(_("Only your lunch manager processes the orders."))

    @api.one
    def confirm(self):
        """
        confirm one or more order line, update order status and create new cashmove
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            if self.state != 'confirmed':
                values = {
                    'user_id': self.user_id.id,
                    'amount': -self.price,
                    'description': self.product_id.name,
                    'order_id': self.id,
                    'state': 'order',
                    'date': self.date,
                }
                self.env['lunch.cashmove'].create(values)
                self.state = 'confirmed'
        else:
            raise AccessError(_("Only your lunch manager sets the orders as received."))

    @api.one
    def cancel(self):
        """
        cancel one or more order.line, update order status and unlink existing cashmoves
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            self.state = 'cancelled'
            self.cashmove.unlink()
        else:
            raise AccessError(_("Only your lunch manager cancels the orders."))


class LunchProduct(models.Model):
    """ Products available to order. A product is linked to a specific vendor. """
    _name = 'lunch.product'
    _description = 'Lunch Product'

    name = fields.Char('Product', required=True)
    category_id = fields.Many2one('lunch.product.category', 'Product Category', required=True)
    description = fields.Text('Description')
    price = fields.Float('Price', digits=dp.get_precision('Account'))
    supplier = fields.Many2one('res.partner', 'Vendor')
    active = fields.Boolean(default=True)
    available = fields.Boolean(compute='_get_available_product', search='_search_available_products')

    @api.depends('supplier')
    def _get_available_product(self):
        for product in self:
            if not product.supplier:
                product.available = True
            else:
                alerts = self.env['lunch.alert'].search([
                    ('partner_id', '=', self.supplier.id)
                ])
                if alerts and not any(alert.display for alert in alerts):
                    # every alert is not available
                    product.available = False
                else:
                    # no alert for the supplier or at least one is not available
                    product.available = True

    def _search_available_products(self, operator, value):
        alerts = self.env['lunch.alert'].search([])
        supplier_w_alerts = alerts.mapped('partner_id')
        available_suppliers = alerts.filtered(lambda a: a.display).mapped('partner_id')
        available_products = self.search([
            '|',
                ('supplier', 'not in', supplier_w_alerts.ids),
                ('supplier', 'in', available_suppliers.ids)
        ])

        if (operator in expression.NEGATIVE_TERM_OPERATORS and value) or \
           (operator not in expression.NEGATIVE_TERM_OPERATORS and not value):
            # e.g. (available = False) or (available != True)
            return [('id', 'not in', available_products.ids)]
        else:
            # e.g. (available = True) or (available != False)
            return [('id', 'in', available_products.ids)]


class LunchProductCategory(models.Model):
    """ Category of the product such as pizza, sandwich, pasta, chinese, burger... """
    _name = 'lunch.product.category'
    _description = 'Lunch Product Category'

    name = fields.Char('Product Category', required=True)


class LunchCashMove(models.Model):
    """ Two types of cashmoves: payment (credit) or order (debit) """
    _name = 'lunch.cashmove'
    _description = 'Lunch Cashmove'

    user_id = fields.Many2one('res.users', 'User',
                              default=lambda self: self.env.uid)
    date = fields.Date('Date', required=True, default=fields.Date.context_today)
    amount = fields.Float('Amount', required=True, help='Can be positive (payment) or negative (order or payment if user wants to get his money back)')
    description = fields.Text('Description', help='Can be an order or a payment')
    order_id = fields.Many2one('lunch.order.line', 'Order', ondelete='cascade')
    state = fields.Selection([('order', 'Order'), ('payment', 'Payment')],
                             'Is an order or a payment', default='payment')

    @api.multi
    def name_get(self):
        return [(cashmove.id, '%s %s' % (_('Lunch Cashmove'), '#%d' % cashmove.id)) for cashmove in self]


class LunchAlert(models.Model):
    """ Alerts to display during a lunch order. An alert can be specific to a
    given day, weekly or daily. The alert is displayed from start to end hour. """
    _name = 'lunch.alert'
    _description = 'Lunch Alert'
    _rec_name = 'message'

    display = fields.Boolean(compute='_compute_display_get')
    message = fields.Text('Message', required=True)
    alert_type = fields.Selection([('specific', 'Specific Day'),
                                   ('week', 'Every Week'),
                                   ('days', 'Every Day')],
                                  string='Recurrence', required=True, index=True, default='specific')
    partner_id = fields.Many2one('res.partner', string="Vendor",
                                 help="If specified, the selected vendor can be ordered only on selected days")
    specific_day = fields.Date('Day', default=fields.Date.context_today)
    monday = fields.Boolean('Monday')
    tuesday = fields.Boolean('Tuesday')
    wednesday = fields.Boolean('Wednesday')
    thursday = fields.Boolean('Thursday')
    friday = fields.Boolean('Friday')
    saturday = fields.Boolean('Saturday')
    sunday = fields.Boolean('Sunday')
    start_hour = fields.Float('Between', oldname='active_from', required=True, default=7)
    end_hour = fields.Float('And', oldname='active_to', required=True, default=23)
    active = fields.Boolean(default=True)

    @api.multi
    def name_get(self):
        return [(alert.id, '%s %s' % (_('Alert'), '#%d' % alert.id)) for alert in self]

    @api.depends('alert_type', 'specific_day', 'monday', 'tuesday', 'thursday',
                 'friday', 'saturday', 'sunday', 'start_hour', 'end_hour')
    def _compute_display_get(self):
        """
        This method check if the alert can be displayed today
        if alert type is specific : compare specific_day(date) with today's date
        if alert type is week : check today is set as alert (checkbox true) eg. self['monday']
        if alert type is day : True
        return : Message if can_display_alert is True else False
        """
        days_codes = {'0': 'sunday',
                      '1': 'monday',
                      '2': 'tuesday',
                      '3': 'wednesday',
                      '4': 'thursday',
                      '5': 'friday',
                      '6': 'saturday'}
        fullday = False
        now = datetime.datetime.now()
        if self.env.context.get('lunch_date'):
            # lunch_date is a fields.Date -> 00:00:00
            lunch_date = fields.Datetime.from_string(self.env.context['lunch_date'])
            # if lunch_date is in the future, planned lunch, ignore hours
            fullday = lunch_date > now
            now = max(lunch_date, now)
        mynow = fields.Datetime.context_timestamp(self, now)

        for alert in self:
            can_display_alert = {
                'specific': (str(alert.specific_day) == fields.Date.to_string(mynow)),
                'week': alert[days_codes[mynow.strftime('%w')]],
                'days': True
            }

            if can_display_alert[alert.alert_type]:
                hour_to = int(alert.end_hour)
                min_to = int((alert.end_hour - hour_to) * 60)
                to_alert = datetime.time(hour_to, min_to)
                hour_from = int(alert.start_hour)
                min_from = int((alert.start_hour - hour_from) * 60)
                from_alert = datetime.time(hour_from, min_from)

                if fullday or (from_alert <= mynow.time() <= to_alert):
                    alert.display = True
                else:
                    alert.display = False

# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. A lunch order contains one or more lunch order line(s). It is associated to a user for a given     date. When creating a lunch order, applicable lunch alerts are displayed. # If we return return prev_order.ids, we will have duplicates (identical orders). # Therefore, this following part removes duplicates based on product_id and note. get and sum the order lines' price get the alerts to display on the order form # If we use prev_order.ids, we will have duplicates (identical orders). # Therefore, this following part removes duplicates based on product_id and note. # sort the old lunch orders by (date, id) Prevents the user to create an order in the past Update the state of lunch.order based on its orderlines. Here is the logic:         - if at least one order line is cancelled, the order is set as cancelled         - if no line is cancelled but at least one line is not confirmed, the order is set as new         - if all lines are confirmed, the order is set as confirmed Override as an onchange would not apply if using the history buttons Override as an onchange would not apply if using the history buttons The order_line is ordered to the vendor but isn't received yet # do not send emails for products with no suppliers # group the order by products and note # sort by product name, note confirm one or more order line, update order status and create new cashmove cancel one or more order.line, update order status and unlink existing cashmoves Products available to order. A product is linked to a specific vendor. # every alert is not available # no alert for the supplier or at least one is not available # e.g. (available = False) or (available != True) # e.g. (available = True) or (available != False) Category of the product such as pizza, sandwich, pasta, chinese, burger... Two types of cashmoves: payment (credit) or order (debit) Alerts to display during a lunch order. An alert can be specific to a     given day, weekly or daily. The alert is displayed from start to end hour. This method check if the alert can be displayed today         if alert type is specific : compare specific_day(date) with today's date         if alert type is week : check today is set as alert (checkbox true) eg. self['monday']         if alert type is day : True         return : Message if can_display_alert is True else False # lunch_date is a fields.Date -> 00:00:00 # if lunch_date is in the future, planned lunch, ignore hours