diff --git a/addons/lunch/__manifest__.py b/addons/lunch/__manifest__.py index 5268ecca6a7..1b6c80814f0 100644 --- a/addons/lunch/__manifest__.py +++ b/addons/lunch/__manifest__.py @@ -28,6 +28,7 @@ If you want to save your employees' time and avoid them to always have coins in 'report/lunch_order_templates.xml', 'views/lunch_templates.xml', 'wizard/lunch_line_wizard_views.xml', + 'views/lunch_alert_views.xml', 'views/lunch_cashmove_views.xml', 'views/lunch_orders_views.xml', 'views/lunch_product_views.xml', diff --git a/addons/lunch/controllers/main.py b/addons/lunch/controllers/main.py index 314cf5ad292..f252eb716b9 100644 --- a/addons/lunch/controllers/main.py +++ b/addons/lunch/controllers/main.py @@ -17,6 +17,7 @@ class LunchController(http.Controller): 'userimage': '/web/image?model=res.users&id=%s&field=image_small' % user.id, 'is_manager': request.env.user.has_group('lunch.group_lunch_manager'), 'users': request.env['res.users'].search_read([('groups_id', 'not in', [request.env.ref('base.group_portal').id])], ['name']), + 'alerts': request.env['lunch.alert'].search_read([('available_today', '=', True)], ['message']) } order = self._get_current_order(user.id) diff --git a/addons/lunch/data/lunch_demo.xml b/addons/lunch/data/lunch_demo.xml index f91a57a183d..a5f44ebe955 100644 --- a/addons/lunch/data/lunch_demo.xml +++ b/addons/lunch/data/lunch_demo.xml @@ -285,11 +285,15 @@ - + + Garniture + + + Olives 0.30 - - + + diff --git a/addons/lunch/models/__init__.py b/addons/lunch/models/__init__.py index 58a34e7891b..1bdf9862ffe 100644 --- a/addons/lunch/models/__init__.py +++ b/addons/lunch/models/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from . import lunch_alert from . import lunch_cashmove from . import lunch_order from . import lunch_product diff --git a/addons/lunch/models/lunch_alert.py b/addons/lunch/models/lunch_alert.py new file mode 100644 index 00000000000..bd0e7c04a9b --- /dev/null +++ b/addons/lunch/models/lunch_alert.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import math +import pytz + +from datetime import datetime, time + +from odoo import api, fields, models + +from odoo.osv import expression +from odoo.tools import float_round + + +WEEKDAY_TO_NAME = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] + +def float_to_time(hours, tz=None): + """ Convert a number of hours into a time object. """ + if hours == 24.0: + return time.max + fractional, integral = math.modf(hours) + res = time(int(integral), int(float_round(60 * fractional, precision_digits=0)), 0) + if tz: + res = res.replace(tzinfo=pytz.timezone(tz)) + return res + +def time_to_float(t): + return float_round(t.hour + t.minute/60 + t.second/3600, precision_digits=2) + + +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' + + message = fields.Text('Message', required=True) + + recurrency = fields.Selection([('once', 'Specific Day'), ('reccurent', 'Reccurent')], 'Recurrency', default='once') + recurrency_from = fields.Float('From', default=7) + recurrency_to = fields.Float('To', default=23) + recurrency_date = fields.Date('Day', default=fields.Date.today()) + recurrency_date_from = fields.Datetime('from', compute='_compute_recurrency_date_from', store=True) + recurrency_date_to = fields.Datetime('to', compute='_compute_recurrency_date_to', store=True) + recurrency_monday = fields.Boolean('Monday') + recurrency_tuesday = fields.Boolean('Tuesday') + recurrency_wednesday = fields.Boolean('Wednesday') + recurrency_thursday = fields.Boolean('Thursday') + recurrency_friday = fields.Boolean('Friday') + recurrency_saturday = fields.Boolean('Saturday') + recurrency_sunday = fields.Boolean('Sunday') + + available_today = fields.Boolean('This is True when if the supplier is available today', + compute='_compute_available_today', search='_search_available_today') + + @api.depends('recurrency_date', 'recurrency_from') + def _compute_recurrency_date_from(self): + for alert in self: + if alert.recurrency_date and alert.recurrency_from: + alert.recurrency_date_from = datetime.combine(alert.recurrency_date, float_to_time(alert.recurrency_from)) + + @api.depends('recurrency_date', 'recurrency_to') + def _compute_recurrency_date_to(self): + for alert in self: + if alert.recurrency_date and alert.recurrency_to: + alert.recurrency_date_to = datetime.combine(alert.recurrency_date, float_to_time(alert.recurrency_to)) + + @api.depends('recurrency', 'recurrency_date', 'recurrency_from', 'recurrency_to', 'recurrency_monday', + 'recurrency_tuesday', 'recurrency_wednesday', 'recurrency_thursday', + 'recurrency_friday', 'recurrency_saturday', 'recurrency_sunday') + def _compute_available_today(self): + now = fields.Datetime.now() + + for alert in self: + time_from = float_to_time(alert.recurrency_from) + time_to = float_to_time(alert.recurrency_to) + + if alert.recurrency == 'once': + alert.available_today = (alert.recurrency_date_from <= now <= alert.recurrency_date_to) + else: + fieldname = 'recurrency_%s' % (WEEKDAY_TO_NAME[now.weekday()]) + alert.available_today = alert[fieldname] and (time_from <= now.time() <= time_to) + + def _search_available_today(self, operator, value): + if (not operator in ['=', '!=']) or (not value in [True, False]): + return [] + + searching_for_true = (operator == '=' and value) or (operator == '!=' and not value) + now = fields.Datetime.now() + float_now = time_to_float(now.time()) + fieldname = 'recurrency_%s' % (WEEKDAY_TO_NAME[now.weekday()]) + + if searching_for_true: + specific = expression.AND([ + [('recurrency', '=', 'once')], + [('recurrency_date_from', '<=', now)], + [('recurrency_date_to', '>=', now)] + ]) + else: + specific = expression.AND([ + [('recurrency', '=', 'once')], + expression.OR([ + [('recurrency_date_from', '>=', now)], + [('recurrency_date_to', '<=', now)] + ]) + ]) + + recurrence = expression.AND([ + [(fieldname, operator, value)], + [('recurrency_from', '<=' if searching_for_true else '>=', float_now)], + [('recurrency_to', '>=' if searching_for_true else '<=', float_now)] + ]) + + return expression.OR([specific, recurrence]) diff --git a/addons/lunch/models/lunch_order.py b/addons/lunch/models/lunch_order.py index 623a9cada50..cadd1ca449e 100644 --- a/addons/lunch/models/lunch_order.py +++ b/addons/lunch/models/lunch_order.py @@ -123,8 +123,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) - topping_ids = fields.Many2many('lunch.product', string='Toppings') - product_id = fields.Many2one('lunch.product', string="Product", domain=[('is_topping', '=', False)], required=True) + topping_ids = fields.Many2many('lunch.topping', string='Toppings') + product_id = fields.Many2one('lunch.product', string="Product", required=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) @@ -147,7 +147,7 @@ class LunchOrderLine(models.Model): @api.depends('topping_ids', 'product_id', 'quantity') def _compute_total_price(self): for line in self: - line.price = line.quantity * sum(product.price for product in line.product_id | line.topping_ids) + line.price = line.quantity * (line.product_id.price + sum(line.topping_ids.mapped('price'))) def update_quantity(self, increment): for line in self: diff --git a/addons/lunch/models/lunch_product.py b/addons/lunch/models/lunch_product.py index 1751ea7a6e1..af3d25df7d7 100644 --- a/addons/lunch/models/lunch_product.py +++ b/addons/lunch/models/lunch_product.py @@ -14,6 +14,37 @@ class LunchProductCategory(models.Model): _description = 'Lunch Product Category' name = fields.Char('Product Category', required=True) + topping_ids = fields.One2many('lunch.topping', 'category_id') + + +class LunchToppingType(models.Model): + """""" + _name = 'lunch.topping.type' + _description = 'Lunch Topping Type' + + name = fields.Char('Name', required=True) + + +class LunchTopping(models.Model): + """""" + _name = 'lunch.topping' + _description = 'Lunch Toppings' + + name = fields.Char('Name', required=True) + price = fields.Float('Price', digits=dp.get_precision('Account'), required=True) + category_id = fields.Many2one('lunch.product.category') + type_id = fields.Many2one('lunch.topping.type') + + def name_get(self): + currency_id = self.env.user.company_id.currency_id + res = dict(super(LunchTopping, self).name_get()) + for topping in self: + if currency_id.position == 'before': + price = '%s %s' % (currency_id.symbol, topping.price) + else: + price = '%s %s' % (topping.price, currency_id.symbol) + res[topping.id] = '%s %s' % (topping.name, price) + return list(res.items()) class LunchProduct(models.Model): @@ -22,12 +53,11 @@ class LunchProduct(models.Model): _description = 'Lunch Product' name = fields.Char('Name', required=True) - category_id = fields.Many2one('lunch.product.category', 'Product Category') + category_id = fields.Many2one('lunch.product.category', 'Product Category', required=True) description = fields.Text('Description') price = fields.Float('Price', digits=dp.get_precision('Account'), required=True) supplier_id = fields.Many2one('lunch.supplier', 'Vendor', required=True) active = fields.Boolean(default=True) - is_topping = fields.Boolean("This product is an extra garniture") # image: all image fields are base64 encoded and PIL-supported image = fields.Binary( diff --git a/addons/lunch/models/lunch_supplier.py b/addons/lunch/models/lunch_supplier.py index 07286aa1ff0..ce16eb8460b 100644 --- a/addons/lunch/models/lunch_supplier.py +++ b/addons/lunch/models/lunch_supplier.py @@ -49,6 +49,9 @@ class LunchSupplier(models.Model): vat = fields.Char(related='partner_id.vat') + responsible_id = fields.Many2one('res.users', string="Responsible", domain=lambda self: [('groups_id', 'in', self.env.ref('lunch.group_lunch_manager').id)], + help="This is used to set a responsible for this particular vendor") + image = fields.Binary(related='partner_id.image', readonly=False) image_medium = fields.Binary(related='partner_id.image_medium', readonly=False) image_small = fields.Binary(related='partner_id.image_small', readonly=False) @@ -60,17 +63,17 @@ class LunchSupplier(models.Model): automatic_email_send = fields.Boolean('Automatic Email Sending') automatic_email_time = fields.Float('Hour') - recurrency = fields.Selection([('once', 'Specific Day'), ('reccurent', 'Reccurent')], 'Recurrency', default='once') - recurrency_from = fields.Float('From') - recurrency_to = fields.Float('To') + recurrency = fields.Selection([('once', 'Specific Day'), ('reccurent', 'Reccurent')], 'Recurrency', default='reccurent') + recurrency_from = fields.Float('From', default=7) + recurrency_to = fields.Float('To', default=23) recurrency_date = fields.Date('Day', default=fields.Date.today()) recurrency_date_from = fields.Datetime('from', compute='_compute_recurrency_date_from', store=True) recurrency_date_to = fields.Datetime('to', compute='_compute_recurrency_date_to', store=True) - recurrency_monday = fields.Boolean('Monday') - recurrency_tuesday = fields.Boolean('Tuesday') - recurrency_wednesday = fields.Boolean('Wednesday') - recurrency_thursday = fields.Boolean('Thursday') - recurrency_friday = fields.Boolean('Friday') + recurrency_monday = fields.Boolean('Monday', default=True) + recurrency_tuesday = fields.Boolean('Tuesday', default=True) + recurrency_wednesday = fields.Boolean('Wednesday', default=True) + recurrency_thursday = fields.Boolean('Thursday', default=True) + recurrency_friday = fields.Boolean('Friday', default=True) recurrency_saturday = fields.Boolean('Saturday') recurrency_sunday = fields.Boolean('Sunday') @@ -142,7 +145,7 @@ class LunchSupplier(models.Model): time_to = float_to_time(supplier.recurrency_to) if supplier.recurrency == 'once': - supplier.available_today = (supplier.reccurrency_date_from <= now <= supplier.reccurrency_date_to) + supplier.available_today = (supplier.recurrency_date_from <= now <= supplier.recurrency_date_to) else: fieldname = 'recurrency_%s' % (WEEKDAY_TO_NAME[now.weekday()]) supplier.available_today = supplier[fieldname] and (time_from <= now.time() <= time_to) diff --git a/addons/lunch/security/ir.model.access.csv b/addons/lunch/security/ir.model.access.csv index 1efb5be6c6d..06df5f431de 100644 --- a/addons/lunch/security/ir.model.access.csv +++ b/addons/lunch/security/ir.model.access.csv @@ -11,3 +11,10 @@ product_user,"Product user",model_lunch_product,group_lunch_user,1,0,0,0 product_category_user,"Product category user",model_lunch_product_category,group_lunch_user,1,0,0,0 lunch_supplier_user,"Lunch Supplier User Rights",model_lunch_supplier,group_lunch_user,1,0,0,0 lunch_supplier_manager,"Lunch Supplier Manager Rights",model_lunch_supplier,group_lunch_manager,1,1,1,1 +lunch_alert_access,access_lunch_alert_user,model_lunch_alert,base.group_user,1,0,0,0 +lunch_alert_user,access_lunch_alert_lunch_user,model_lunch_alert,group_lunch_user,1,1,1,1 +lunch_alert_manager,access_lunch_alert_manager,model_lunch_alert,group_lunch_manager,1,1,1,1 +lunch_topping_type_user,access_lunch_topping_type,model_lunch_topping_type,base.group_user,1,0,0,0 +lunch_topping_type_manager,access_lunch_topping_type,model_lunch_topping_type,group_lunch_manager,1,1,1,1 +lunch_topping_user,access_lunch_topping,model_lunch_topping,base.group_user,1,0,0,0 +lunch_topping_manager,access_lunch_topping,model_lunch_topping,group_lunch_manager,1,1,1,1 diff --git a/addons/lunch/static/src/scss/lunch_kanban.scss b/addons/lunch/static/src/scss/lunch_kanban.scss index 44bac01fa4b..eff03959d52 100644 --- a/addons/lunch/static/src/scss/lunch_kanban.scss +++ b/addons/lunch/static/src/scss/lunch_kanban.scss @@ -5,18 +5,17 @@ .o_lunch_widget { display: flex; - overflow: auto; border-bottom: 1px solid #CED4DA; background-color: white; min-height: 90px; - max-height: 90px; + overflow-y: visible; .o_lunch_widget_info { padding: 5px 12px; margin: 4px 8px; width: 300px; flex: 1 1 auto; - overflow-y: auto; + overflow-y: visible; .o_lunch_ordered { color: white; diff --git a/addons/lunch/static/src/xml/lunch_kanban.xml b/addons/lunch/static/src/xml/lunch_kanban.xml index 74a57f7cfec..61077e54e13 100644 --- a/addons/lunch/static/src/xml/lunch_kanban.xml +++ b/addons/lunch/static/src/xml/lunch_kanban.xml @@ -1,101 +1,108 @@ -
-
-
- -
-
+
+
+
- - - - - - +
-
- - - - - +
+
+ + - - - Add money - + + + +
+
+ + + + + + + + + Add money + +
-
-
- - - - - - - -
-
- Your order - - Edit - Save -
-
-
-
+
+ + + + + + +
-
- - - +
+ Your order + + Edit + Save
-
- +
+
+
+
+
+ + + +
+
+ +
+
+ + + + +
-
+
+
+ + +
+
+ + + + +
+
+
+ +
+
+ +
+
+ Total +
+
- +
-
-
- + -
-
- - - - -
-
-
- -
-
- -
-
- Total -
-
- - - - -
-
- -
+ + +
+ + +
diff --git a/addons/lunch/static/tests/test_lunch_kanban.js b/addons/lunch/static/tests/test_lunch_kanban.js index b2062609974..649c84b680e 100644 --- a/addons/lunch/static/tests/test_lunch_kanban.js +++ b/addons/lunch/static/tests/test_lunch_kanban.js @@ -49,7 +49,8 @@ QUnit.module('Views', { state: 'new', lines: [ {id: 1, product: ['Pizza Italiana', 7.4], toppings: [], quantity: 1.0, price: 7.4} - ] + ], + alerts: [], }); } else if (route.startsWith('data:image/png;base64,')) { return $.when(); @@ -111,7 +112,8 @@ QUnit.module('Views', { state: state, lines: [ {id: 1, product: ['Pizza Italiana', 7.4], toppings: [], quantity: 1.0, price: 7.4} - ] + ], + alerts: [], }); } else if (route === '/lunch/payment_message') { return $.when({message: 'Hello'}); @@ -182,7 +184,8 @@ QUnit.module('Views', { state: 'new', lines: [ {id: 1, product: ['Pizza Italiana', 7.4], toppings: [], quantity: 1.0, price: 7.4} - ] + ], + alerts: [], }); } else if (route.startsWith('data:image/png;base64,')) { return $.when(); diff --git a/addons/lunch/tests/common.py b/addons/lunch/tests/common.py index f38b9c547ec..3c5a31188e3 100644 --- a/addons/lunch/tests/common.py +++ b/addons/lunch/tests/common.py @@ -67,9 +67,13 @@ class TestsCommon(common.TransactionCase): 'supplier_id': self.supplier_coin_gourmand.id, }) - self.topping_olives = self.env['lunch.product'].create({ + self.garniture_type = self.env['lunch.topping.type'].create({ + 'name': 'Graniture', + }) + + self.topping_olives = self.env['lunch.topping'].create({ 'name': 'Olives', 'price': 0.3, - 'supplier_id': self.supplier_pizza_inn.id, - 'is_topping': True, + 'category_id': self.category_pizza.id, + 'type_id': self.garniture_type.id, }) diff --git a/addons/lunch/views/lunch_alert_views.xml b/addons/lunch/views/lunch_alert_views.xml new file mode 100644 index 00000000000..cbd587e3065 --- /dev/null +++ b/addons/lunch/views/lunch_alert_views.xml @@ -0,0 +1,60 @@ + + + + lunch.alert.search + lunch.alert + + + + + + + + + + lunch.alert.tree + lunch.alert + + + + + + + + + + + + lunch.alert.form + lunch.alert + +
+ + + + + + + + + + + + + + + +
+
+
+ + + Lunch Alerts + lunch.alert + + tree,form + + +
diff --git a/addons/lunch/views/lunch_product_views.xml b/addons/lunch/views/lunch_product_views.xml index 70a8695564e..a90cfe826a9 100644 --- a/addons/lunch/views/lunch_product_views.xml +++ b/addons/lunch/views/lunch_product_views.xml @@ -11,8 +11,8 @@ - - + + @@ -48,12 +48,8 @@