This commit adapts the business code to changes introduced by the parent commit in order to keep the same behaviour as before. All readonly=False fields will have to be checked afterwards to confirm that the business case requires write access to the source field.
473 lines
21 KiB
Python
473 lines
21 KiB
Python
# -*- 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.model
|
|
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='comment',
|
|
default_model='lunch.order',
|
|
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
|