277 lines
14 KiB
Python
277 lines
14 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
from odoo import api, fields, models, _
|
||
|
from odoo.exceptions import ValidationError, UserError
|
||
|
|
||
|
|
||
|
class LunchOrder(models.Model):
|
||
|
_name = 'lunch.order'
|
||
|
_description = 'Lunch Order'
|
||
|
_order = 'id desc'
|
||
|
_display_name = 'product_id'
|
||
|
|
||
|
name = fields.Char(related='product_id.name', string="Product Name", readonly=True)
|
||
|
topping_ids_1 = fields.Many2many('lunch.topping', 'lunch_order_topping', 'order_id', 'topping_id', string='Extras 1', domain=[('topping_category', '=', 1)])
|
||
|
topping_ids_2 = fields.Many2many('lunch.topping', 'lunch_order_topping', 'order_id', 'topping_id', string='Extras 2', domain=[('topping_category', '=', 2)])
|
||
|
topping_ids_3 = fields.Many2many('lunch.topping', 'lunch_order_topping', 'order_id', 'topping_id', string='Extras 3', domain=[('topping_category', '=', 3)])
|
||
|
product_id = fields.Many2one('lunch.product', string="Product", required=True)
|
||
|
category_id = fields.Many2one(
|
||
|
string='Product Category', related='product_id.category_id', store=True)
|
||
|
date = fields.Date('Order Date', required=True, readonly=False,
|
||
|
default=fields.Date.context_today)
|
||
|
supplier_id = fields.Many2one(
|
||
|
string='Vendor', related='product_id.supplier_id', store=True, index=True)
|
||
|
available_today = fields.Boolean(related='supplier_id.available_today')
|
||
|
order_deadline_passed = fields.Boolean(related='supplier_id.order_deadline_passed')
|
||
|
user_id = fields.Many2one('res.users', 'User', readonly=False,
|
||
|
default=lambda self: self.env.uid)
|
||
|
lunch_location_id = fields.Many2one('lunch.location', default=lambda self: self.env.user.last_lunch_location_id)
|
||
|
note = fields.Text('Notes')
|
||
|
price = fields.Monetary('Total Price', compute='_compute_total_price', readonly=True, store=True)
|
||
|
active = fields.Boolean('Active', default=True)
|
||
|
state = fields.Selection([('new', 'To Order'),
|
||
|
('ordered', 'Ordered'), # "Internally" ordered
|
||
|
('sent', 'Sent'), # Order sent to the supplier
|
||
|
('confirmed', 'Received'), # Order received
|
||
|
('cancelled', 'Cancelled')],
|
||
|
'Status', readonly=True, index=True, default='new')
|
||
|
notified = fields.Boolean(default=False)
|
||
|
company_id = fields.Many2one('res.company', default=lambda self: self.env.company.id)
|
||
|
currency_id = fields.Many2one(related='company_id.currency_id', store=True)
|
||
|
quantity = fields.Float('Quantity', required=True, default=1)
|
||
|
|
||
|
display_toppings = fields.Text('Extras', compute='_compute_display_toppings', store=True)
|
||
|
|
||
|
product_description = fields.Html('Description', related='product_id.description')
|
||
|
topping_label_1 = fields.Char(related='product_id.supplier_id.topping_label_1')
|
||
|
topping_label_2 = fields.Char(related='product_id.supplier_id.topping_label_2')
|
||
|
topping_label_3 = fields.Char(related='product_id.supplier_id.topping_label_3')
|
||
|
topping_quantity_1 = fields.Selection(related='product_id.supplier_id.topping_quantity_1')
|
||
|
topping_quantity_2 = fields.Selection(related='product_id.supplier_id.topping_quantity_2')
|
||
|
topping_quantity_3 = fields.Selection(related='product_id.supplier_id.topping_quantity_3')
|
||
|
image_1920 = fields.Image(compute='_compute_product_images')
|
||
|
image_128 = fields.Image(compute='_compute_product_images')
|
||
|
|
||
|
available_toppings_1 = fields.Boolean(help='Are extras available for this product', compute='_compute_available_toppings')
|
||
|
available_toppings_2 = fields.Boolean(help='Are extras available for this product', compute='_compute_available_toppings')
|
||
|
available_toppings_3 = fields.Boolean(help='Are extras available for this product', compute='_compute_available_toppings')
|
||
|
display_reorder_button = fields.Boolean(compute='_compute_display_reorder_button')
|
||
|
|
||
|
@api.depends('product_id')
|
||
|
def _compute_product_images(self):
|
||
|
for line in self:
|
||
|
line.image_1920 = line.product_id.image_1920 or line.category_id.image_1920
|
||
|
line.image_128 = line.product_id.image_128 or line.category_id.image_128
|
||
|
|
||
|
@api.depends('category_id')
|
||
|
def _compute_available_toppings(self):
|
||
|
for order in self:
|
||
|
order.available_toppings_1 = bool(order.env['lunch.topping'].search_count([('supplier_id', '=', order.supplier_id.id), ('topping_category', '=', 1)]))
|
||
|
order.available_toppings_2 = bool(order.env['lunch.topping'].search_count([('supplier_id', '=', order.supplier_id.id), ('topping_category', '=', 2)]))
|
||
|
order.available_toppings_3 = bool(order.env['lunch.topping'].search_count([('supplier_id', '=', order.supplier_id.id), ('topping_category', '=', 3)]))
|
||
|
|
||
|
@api.depends_context('show_reorder_button')
|
||
|
@api.depends('state')
|
||
|
def _compute_display_reorder_button(self):
|
||
|
show_button = self.env.context.get('show_reorder_button')
|
||
|
for order in self:
|
||
|
order.display_reorder_button = show_button and order.state == 'confirmed' and order.supplier_id.available_today
|
||
|
|
||
|
def init(self):
|
||
|
self._cr.execute("""CREATE INDEX IF NOT EXISTS lunch_order_user_product_date ON %s (user_id, product_id, date)"""
|
||
|
% self._table)
|
||
|
|
||
|
def _extract_toppings(self, values):
|
||
|
"""
|
||
|
If called in api.multi then it will pop topping_ids_1,2,3 from values
|
||
|
"""
|
||
|
topping_1_values = values.get('topping_ids_1', False)
|
||
|
topping_2_values = values.get('topping_ids_2', False)
|
||
|
topping_3_values = values.get('topping_ids_3', False)
|
||
|
if self.ids:
|
||
|
# TODO This is not taking into account all the toppings for each individual order, this is usually not a problem
|
||
|
# since in the interface you usually don't update more than one order at a time but this is a bug nonetheless
|
||
|
topping_1 = values.pop('topping_ids_1')[0][2] if topping_1_values else self[:1].topping_ids_1.ids
|
||
|
topping_2 = values.pop('topping_ids_2')[0][2] if topping_2_values else self[:1].topping_ids_2.ids
|
||
|
topping_3 = values.pop('topping_ids_3')[0][2] if topping_3_values else self[:1].topping_ids_3.ids
|
||
|
else:
|
||
|
topping_1 = values['topping_ids_1'][0][2] if topping_1_values else []
|
||
|
topping_2 = values['topping_ids_2'][0][2] if topping_2_values else []
|
||
|
topping_3 = values['topping_ids_3'][0][2] if topping_3_values else []
|
||
|
|
||
|
return topping_1 + topping_2 + topping_3
|
||
|
|
||
|
@api.constrains('topping_ids_1', 'topping_ids_2', 'topping_ids_3')
|
||
|
def _check_topping_quantity(self):
|
||
|
errors = {
|
||
|
'1_more': _('You should order at least one %s'),
|
||
|
'1': _('You have to order one and only one %s'),
|
||
|
}
|
||
|
for line in self:
|
||
|
for index in range(1, 4):
|
||
|
availability = line['available_toppings_%s' % index]
|
||
|
quantity = line['topping_quantity_%s' % index]
|
||
|
toppings = line['topping_ids_%s' % index].filtered(lambda x: x.topping_category == index)
|
||
|
label = line['topping_label_%s' % index]
|
||
|
|
||
|
if availability and quantity != '0_more':
|
||
|
check = bool(len(toppings) == 1 if quantity == '1' else toppings)
|
||
|
if not check:
|
||
|
raise ValidationError(errors[quantity] % label)
|
||
|
|
||
|
@api.model_create_multi
|
||
|
def create(self, vals_list):
|
||
|
orders = self.env['lunch.order']
|
||
|
for vals in vals_list:
|
||
|
lines = self._find_matching_lines({
|
||
|
**vals,
|
||
|
'toppings': self._extract_toppings(vals),
|
||
|
})
|
||
|
if lines.filtered(lambda l: l.state not in ['sent', 'confirmed']):
|
||
|
# YTI FIXME This will update multiple lines in the case there are multiple
|
||
|
# matching lines which should not happen through the interface
|
||
|
lines.update_quantity(1)
|
||
|
orders |= lines[:1]
|
||
|
else:
|
||
|
orders |= super().create(vals)
|
||
|
return orders
|
||
|
|
||
|
def write(self, values):
|
||
|
merge_needed = 'note' in values or 'topping_ids_1' in values or 'topping_ids_2' in values or 'topping_ids_3' in values
|
||
|
default_location_id = self.env.user.last_lunch_location_id and self.env.user.last_lunch_location_id.id or False
|
||
|
|
||
|
if merge_needed:
|
||
|
lines_to_deactivate = self.env['lunch.order']
|
||
|
for line in self:
|
||
|
# Only write on topping_ids_1 because they all share the same table
|
||
|
# and we don't want to remove all the records
|
||
|
# _extract_toppings will pop topping_ids_1, topping_ids_2 and topping_ids_3 from values
|
||
|
# This also forces us to invalidate the cache for topping_ids_2 and topping_ids_3 that
|
||
|
# could have changed through topping_ids_1 without the cache knowing about it
|
||
|
toppings = self._extract_toppings(values)
|
||
|
self.invalidate_model(['topping_ids_2', 'topping_ids_3'])
|
||
|
values['topping_ids_1'] = [(6, 0, toppings)]
|
||
|
matching_lines = self._find_matching_lines({
|
||
|
'user_id': values.get('user_id', line.user_id.id),
|
||
|
'product_id': values.get('product_id', line.product_id.id),
|
||
|
'note': values.get('note', line.note or False),
|
||
|
'toppings': toppings,
|
||
|
'lunch_location_id': values.get('lunch_location_id', default_location_id),
|
||
|
})
|
||
|
if matching_lines:
|
||
|
lines_to_deactivate |= line
|
||
|
matching_lines.update_quantity(line.quantity)
|
||
|
lines_to_deactivate.write({'active': False})
|
||
|
return super(LunchOrder, self - lines_to_deactivate).write(values)
|
||
|
return super().write(values)
|
||
|
|
||
|
@api.model
|
||
|
def _find_matching_lines(self, values):
|
||
|
default_location_id = self.env.user.last_lunch_location_id and self.env.user.last_lunch_location_id.id or False
|
||
|
domain = [
|
||
|
('user_id', '=', values.get('user_id', self.default_get(['user_id'])['user_id'])),
|
||
|
('product_id', '=', values.get('product_id', False)),
|
||
|
('date', '=', fields.Date.today()),
|
||
|
('note', '=', values.get('note', False)),
|
||
|
('lunch_location_id', '=', values.get('lunch_location_id', default_location_id)),
|
||
|
]
|
||
|
toppings = values.get('toppings', [])
|
||
|
return self.search(domain).filtered(lambda line: (line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3).ids == toppings)
|
||
|
|
||
|
@api.depends('topping_ids_1', 'topping_ids_2', 'topping_ids_3', 'product_id', 'quantity')
|
||
|
def _compute_total_price(self):
|
||
|
for line in self:
|
||
|
line.price = line.quantity * (line.product_id.price + sum((line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3).mapped('price')))
|
||
|
|
||
|
@api.depends('topping_ids_1', 'topping_ids_2', 'topping_ids_3')
|
||
|
def _compute_display_toppings(self):
|
||
|
for line in self:
|
||
|
toppings = line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3
|
||
|
line.display_toppings = ' + '.join(toppings.mapped('name'))
|
||
|
|
||
|
def update_quantity(self, increment):
|
||
|
for line in self.filtered(lambda line: line.state not in ['sent', 'confirmed']):
|
||
|
if line.quantity <= -increment:
|
||
|
# TODO: maybe unlink the order?
|
||
|
line.active = False
|
||
|
else:
|
||
|
line.quantity += increment
|
||
|
self._check_wallet()
|
||
|
|
||
|
def add_to_cart(self):
|
||
|
"""
|
||
|
This method currently does nothing, we currently need it in order to
|
||
|
be able to reuse this model in place of a wizard
|
||
|
"""
|
||
|
# YTI FIXME: Find a way to drop this.
|
||
|
return True
|
||
|
|
||
|
def _check_wallet(self):
|
||
|
self.env.flush_all()
|
||
|
for line in self:
|
||
|
if self.env['lunch.cashmove'].get_wallet_balance(line.user_id) < 0:
|
||
|
raise ValidationError(_('Your wallet does not contain enough money to order that. To add some money to your wallet, please contact your lunch manager.'))
|
||
|
|
||
|
def action_order(self):
|
||
|
for order in self:
|
||
|
if not order.supplier_id.available_today:
|
||
|
raise UserError(_('The vendor related to this order is not available today.'))
|
||
|
if self.filtered(lambda line: not line.product_id.active):
|
||
|
raise ValidationError(_('Product is no longer available.'))
|
||
|
self.write({
|
||
|
'state': 'ordered',
|
||
|
})
|
||
|
for order in self:
|
||
|
order.lunch_location_id = order.user_id.last_lunch_location_id
|
||
|
self._check_wallet()
|
||
|
|
||
|
def action_reorder(self):
|
||
|
self.ensure_one()
|
||
|
if not self.supplier_id.available_today:
|
||
|
raise UserError(_('The vendor related to this order is not available today.'))
|
||
|
self.copy({
|
||
|
'date': fields.Date.context_today(self),
|
||
|
'state': 'ordered',
|
||
|
})
|
||
|
action = self.env['ir.actions.act_window']._for_xml_id('lunch.lunch_order_action')
|
||
|
return action
|
||
|
|
||
|
def action_confirm(self):
|
||
|
self.write({'state': 'confirmed'})
|
||
|
|
||
|
def action_cancel(self):
|
||
|
self.write({'state': 'cancelled'})
|
||
|
|
||
|
def action_reset(self):
|
||
|
self.write({'state': 'ordered'})
|
||
|
|
||
|
def action_send(self):
|
||
|
self.state = 'sent'
|
||
|
|
||
|
def action_notify(self):
|
||
|
self -= self.filtered('notified')
|
||
|
if not self:
|
||
|
return
|
||
|
notified_users = set()
|
||
|
# (company, lang): (subject, body)
|
||
|
translate_cache = dict()
|
||
|
for order in self:
|
||
|
user = order.user_id
|
||
|
if user in notified_users:
|
||
|
continue
|
||
|
_key = (order.company_id, user.lang)
|
||
|
if _key not in translate_cache:
|
||
|
context = {'lang': user.lang}
|
||
|
translate_cache[_key] = (_('Lunch notification'), order.company_id.with_context(lang=user.lang).lunch_notify_message)
|
||
|
del context
|
||
|
subject, body = translate_cache[_key]
|
||
|
user.partner_id.message_notify(
|
||
|
subject=subject,
|
||
|
body=body,
|
||
|
partner_ids=user.partner_id.ids,
|
||
|
email_layout_xmlid='mail.mail_notification_light',
|
||
|
)
|
||
|
notified_users.add(user)
|
||
|
self.write({'notified': True})
|