diff --git a/addons/lunch/__manifest__.py b/addons/lunch/__manifest__.py index ab38919601e..828f2f29f45 100644 --- a/addons/lunch/__manifest__.py +++ b/addons/lunch/__manifest__.py @@ -4,7 +4,7 @@ 'name': 'Lunch', 'sequence': 120, 'version': '1.0', - 'depends': ['base', 'web', 'decimal_precision'], + 'depends': ['decimal_precision', 'mail'], 'category': 'Human Resources', 'summary': 'Handle lunch orders of your employees', 'description': """ diff --git a/addons/lunch/data/lunch_data.xml b/addons/lunch/data/lunch_data.xml index f66fbb4788a..2332d2e5272 100644 --- a/addons/lunch/data/lunch_data.xml +++ b/addons/lunch/data/lunch_data.xml @@ -1,5 +1,6 @@ + Lunch: Order meals @@ -7,7 +8,7 @@ code if records: - records.order() + action = records.order() @@ -26,5 +27,121 @@ if records: code records.cancel() + + + + Lunch: Send by email + + ${ctx['order']['supplier_email'] |safe} + ${ctx['order']['supplier_id']} + Orders for ${ctx['order']['company_name']} + + + + +
+ + + + + + + + + + + + + + + +
+ + + +
+ Lunch Order
+
+ +
+
+
+
+ + + + + + + +
+
+ % set lines = ctx['lines'] + % set order = ctx['order'] + % set supplier = user.env['res.partner'].browse(order['supplier_id']) + % set currency = user.env['res.currency'].browse(order['currency_id']) +

+ Dear ${order['supplier_name']}, +

+ Here is, today orders for ${order['company_name']}: +

+
    + % for line in lines: +
  • + + ${line['product']} + (x${line['quantity']}) + + % if line['note']: + (${line['note']}) + % endif +
    + ${format_amount(line['price'], currency)} +
    +
  • + % endfor +
+ +

Amounting in ${format_amount(order['amount_total'], currency)}.

+

Do not hesitate to contact us if you have any question.

+
+
+
+
+
+ + + +
+ ${user.company_id.name} +
+ ${user.company_id.phone} + % if user.company_id.phone and (user.company_id.email or user.company_id.website) + | + % endif + % if user.company_id.email + ${user.company_id.email} + % endif + % if user.company_id.email and user.company_id.website + | + % endif + % if user.company_id.website + + ${user.company_id.website} + + % endif +
+
+
+ + +
+ Powered by Odoo +
+
+
+
+ +
diff --git a/addons/lunch/models/lunch.py b/addons/lunch/models/lunch.py index 5808015d09b..a5e021ca7e4 100644 --- a/addons/lunch/models/lunch.py +++ b/addons/lunch/models/lunch.py @@ -9,6 +9,7 @@ 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): @@ -164,7 +165,8 @@ class LunchOrderLine(models.Model): 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) + 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) @@ -183,13 +185,104 @@ class LunchOrderLine(models.Model): cashmove = fields.One2many('lunch.cashmove', 'order_id', 'Cash Move') currency_id = fields.Many2one('res.currency', related='order_id.currency_id') - @api.one + 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.state = 'ordered' + 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.")) @@ -236,6 +329,41 @@ class LunchProduct(models.Model): 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): @@ -270,6 +398,7 @@ class LunchAlert(models.Model): 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) @@ -277,6 +406,8 @@ class LunchAlert(models.Model): ('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') @@ -293,7 +424,8 @@ class LunchAlert(models.Model): def name_get(self): return [(alert.id, '%s %s' % (_('Alert'), '#%d' % alert.id)) for alert in self] - @api.one + @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 @@ -302,7 +434,6 @@ class LunchAlert(models.Model): if alert type is day : True return : Message if can_display_alert is True else False """ - days_codes = {'0': 'sunday', '1': 'monday', '2': 'tuesday', @@ -310,22 +441,32 @@ class LunchAlert(models.Model): '4': 'thursday', '5': 'friday', '6': 'saturday'} - can_display_alert = { - 'specific': (str(self.specific_day) == fields.Date.context_today(self)), - 'week': self[days_codes[datetime.datetime.now().strftime('%w')]], - 'days': True - } + 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) - if can_display_alert[self.alert_type]: - mynow = fields.Datetime.context_timestamp(self, datetime.datetime.now()) - hour_to = int(self.end_hour) - min_to = int((self.end_hour - hour_to) * 60) - to_alert = datetime.time(hour_to, min_to) - hour_from = int(self.start_hour) - min_from = int((self.start_hour - hour_from) * 60) - from_alert = datetime.time(hour_from, min_from) + 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 from_alert <= mynow.time() <= to_alert: - self.display = True - else: - self.display = False + 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 diff --git a/addons/lunch/tests/test_lunch.py b/addons/lunch/tests/test_lunch.py index 608b1ed2d3f..fe30ca02854 100644 --- a/addons/lunch/tests/test_lunch.py +++ b/addons/lunch/tests/test_lunch.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo.exceptions import ValidationError from odoo.tests import common +from datetime import date, datetime +from unittest.mock import patch class Test_Lunch(common.TransactionCase): @@ -11,18 +14,17 @@ class Test_Lunch(common.TransactionCase): super(Test_Lunch, self).setUp() self.demo_user = self.env['res.users'].search([('name', '=', 'Marc Brown')]) - self.product_bolognese_ref = self.env['ir.model.data'].get_object_reference('lunch', 'product_Bolognese') - self.product_Bolognese_id = self.product_bolognese_ref and self.product_bolognese_ref[1] or False + self.product_Bolognese_id = self.env.ref('lunch.product_Bolognese') self.new_id_order = self.env['lunch.order'].create({ 'user_id': self.demo_user.id, 'order_line_ids': '[]', }) self.new_id_order_line = self.env['lunch.order.line'].create({ 'order_id': self.new_id_order.id, - 'product_id': self.product_Bolognese_id, + 'product_id': self.product_Bolognese_id.id, 'note': '+Emmental', 'cashmove': [], - 'price': self.env['lunch.product'].browse(self.product_Bolognese_id).price, + 'price': self.product_Bolognese_id.price, }) def test_00_lunch_order(self): @@ -60,3 +62,88 @@ class Test_Lunch(common.TransactionCase): #We check that the state is cancelled and that the cashmove has been deleted self.assertEqual(self.order_one.state, 'cancelled') self.assertFalse(self.order_one.cashmove) + + def test_03_lunch_alert(self): + """ specify a lunch alert and make sure it is not possible to make an + order that day + """ + alert_01 = self.env['lunch.alert'].create({ + 'message': 'Order Bolognese only on Tuesday', + 'partner_id': self.product_Bolognese_id.supplier.id, + 'alert_type': 'week', + 'tuesday': True + }) + + patcher_date = patch('odoo.addons.lunch.models.lunch.datetime.date', wraps=date) + patcher_datetime = patch('odoo.addons.lunch.models.lunch.datetime.datetime', wraps=datetime) + mock_date = patcher_date.start() + mock_datetime = patcher_datetime.start() + + mock_date.today.return_value = date(2018, 8, 14) # a Tuesday + mock_datetime.now.return_value = datetime(2018, 8, 14, 9, 0, 0) + + # We check that we can make an order on a Tuesday + order_01 = self.env['lunch.order'].create({ + 'user_id': self.demo_user.id, + 'order_line_ids': [(0, 0, { + 'product_id': self.product_Bolognese_id.id, + })] + }) + order_01.order_line_ids.order() + + with self.assertRaises(ValidationError): + # Planning an order to an non-authorized day must not be possible + order_02 = self.env['lunch.order'].create({ + 'user_id': self.demo_user.id, + 'date': "2018-08-15", + 'order_line_ids': [(0, 0, { + 'product_id': self.product_Bolognese_id.id, + })] + }) + + alert_02 = self.env['lunch.alert'].create({ + 'message': 'Working one more day', + 'partner_id': self.product_Bolognese_id.supplier.id, + 'alert_type': 'specific', + 'specific_day': date(2018, 8, 15) + }) + + # should now be possible to order tomorrow + order_03 = self.env['lunch.order'].create({ + 'user_id': self.demo_user.id, + 'date': "2018-08-15", + 'order_line_ids': [(0, 0, { + 'product_id': self.product_Bolognese_id.id, + })] + }) + + # should be possible to planned on an allowed day + order_04 = self.env['lunch.order'].create({ + 'user_id': self.demo_user.id, + 'date': "2018-08-21", # next Tuesday + 'order_line_ids': [(0, 0, { + 'product_id': self.product_Bolognese_id.id, + })] + }) + order_04.order_line_ids.order() + + # combinaision of alerts should work + alert_03 = self.env['lunch.alert'].create({ + 'message': 'Order Bolognese also on Wednesday', + 'partner_id': self.product_Bolognese_id.supplier.id, + 'alert_type': 'week', + 'wednesday': True + }) + # Planning an order Wednesday must now be possible + order_05 = self.env['lunch.order'].create({ + 'user_id': self.demo_user.id, + 'date': "2018-08-22", # next Wednesday + 'order_line_ids': [(0, 0, { + 'product_id': self.product_Bolognese_id.id, + })] + }) + order_05.order_line_ids.order() + + + patcher_date.stop() + patcher_datetime.stop() diff --git a/addons/lunch/views/lunch_views.xml b/addons/lunch/views/lunch_views.xml index 13582e9bac7..aa858b7f4c0 100644 --- a/addons/lunch/views/lunch_views.xml +++ b/addons/lunch/views/lunch_views.xml @@ -477,7 +477,7 @@ - + @@ -485,7 +485,7 @@
- + @@ -660,37 +660,52 @@ lunch.alert -
- -
- - - - - - - - - - - - - - - - - - + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ Products from this vendor can not be order at the moment. +
+
+ Products from this vendor can be order at the moment. +
-
- - - + + + +