diff --git a/.tx/config b/.tx/config index 464e3cd4a54..8fd8c2cc976 100644 --- a/.tx/config +++ b/.tx/config @@ -387,11 +387,6 @@ file_filter = addons/lunch/i18n/.po source_file = addons/lunch/i18n/lunch.pot source_lang = en -[odoo-master.lunch_hr] -file_filter = addons/lunch_hr/i18n/.po -source_file = addons/lunch_hr/i18n/lunch_hr.pot -source_lang = en - [odoo-master.mail] file_filter = addons/mail/i18n/.po source_file = addons/mail/i18n/mail.pot diff --git a/addons/lunch/__init__.py b/addons/lunch/__init__.py index 88710707227..b6f07433380 100644 --- a/addons/lunch/__init__.py +++ b/addons/lunch/__init__.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import controllers -from . import models from . import wizard +from . import models diff --git a/addons/lunch/__manifest__.py b/addons/lunch/__manifest__.py index 1b6c80814f0..e28b88a82f2 100644 --- a/addons/lunch/__manifest__.py +++ b/addons/lunch/__manifest__.py @@ -27,18 +27,16 @@ If you want to save your employees' time and avoid them to always have coins in 'report/lunch_order_views.xml', 'report/lunch_order_templates.xml', 'views/lunch_templates.xml', - 'wizard/lunch_line_wizard_views.xml', + 'wizard/lucky_order_view.xml', 'views/lunch_alert_views.xml', 'views/lunch_cashmove_views.xml', 'views/lunch_orders_views.xml', 'views/lunch_product_views.xml', - 'views/lunch_supplier_views.xml', 'views/lunch_views.xml', 'data/lunch_data.xml', - 'data/ir_cron_data.xml' ], 'demo': ['data/lunch_demo.xml'], - 'qweb': ['static/src/xml/lunch_kanban.xml', ], + 'qweb': ['static/src/xml/lunch.xml', ], 'installable': True, 'application': True, 'certificate': '001292377792581874189', diff --git a/addons/lunch/controllers/__init__.py b/addons/lunch/controllers/__init__.py deleted file mode 100644 index 12a7e529b67..00000000000 --- a/addons/lunch/controllers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import main diff --git a/addons/lunch/controllers/main.py b/addons/lunch/controllers/main.py deleted file mode 100644 index f252eb716b9..00000000000 --- a/addons/lunch/controllers/main.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import http, fields -from odoo.http import request - - -class LunchController(http.Controller): - @http.route('/lunch/infos', type='json', auth='user') - def infos(self, user_id=None): - user = request.env['res.users'].browse(user_id) if user_id else request.env.user - - infos = { - 'order': False, - 'wallet': request.env['lunch.cashmove'].get_wallet_balance(user), - 'username': user.sudo().name, - '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) - if order: - lines = [{'id': line.id, - 'product': (line.product_id.name, line.product_id.price), - 'toppings': [(topping.name, topping.price) for topping in line.topping_ids], - 'quantity': line.quantity, - 'price': line.price} for line in order.order_line_ids] - infos.update({ - 'order': order.id, - 'total': order.total, - 'state': order.state, - 'lines': lines, - }) - return infos - - @http.route('/lunch/trash', type='json', auth='user') - def trash(self, user_id=None): - user = request.env['res.users'].browse(user_id) if user_id else request.env.user - - order = self._get_current_order(user.id) - order.unlink() - - @http.route('/lunch/pay', type='json', auth='user') - def pay(self, user_id=None): - user = request.env['res.users'].browse(user_id) if user_id else request.env.user - - order = self._get_current_order(user.id) - if order: - wallet_balance = request.env['lunch.cashmove'].get_wallet_balance(user) - - if order.state == 'new' and order.total <= wallet_balance: - return self._make_payment(order) - - return False - - @http.route('/lunch/payment_message', type='json', auth='user') - def payment_message(self): - return {'message': request.env['ir.qweb'].render('lunch.lunch_payment_dialog', {})} - - def _get_current_order(self, user_id): - order = request.env['lunch.order'].search([('user_id', '=', user_id), ('date', '=', fields.Date.today())], limit=1) - return order - - def _make_payment(self, order): - order.action_order() - return True diff --git a/addons/lunch/data/ir_cron_data.xml b/addons/lunch/data/ir_cron_data.xml deleted file mode 100644 index 19275e67f21..00000000000 --- a/addons/lunch/data/ir_cron_data.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Lunch: automatic email send - - code - model._auto_email_send() - - - 10 - minutes - -1 - - - - diff --git a/addons/lunch/data/lunch_data.xml b/addons/lunch/data/lunch_data.xml index 0f269f42d0c..2332d2e5272 100644 --- a/addons/lunch/data/lunch_data.xml +++ b/addons/lunch/data/lunch_data.xml @@ -1,12 +1,23 @@ + + Lunch: Order meals + + + code + +if records: + action = records.order() + + + Lunch: Receive meals code - records.action_confirm() + records.confirm() @@ -14,7 +25,7 @@ code - records.action_cancel() + records.cancel() diff --git a/addons/lunch/data/lunch_demo.xml b/addons/lunch/data/lunch_demo.xml index a5f44ebe955..bd36db1f0af 100644 --- a/addons/lunch/data/lunch_demo.xml +++ b/addons/lunch/data/lunch_demo.xml @@ -28,7 +28,7 @@ Chirashi - + Coin gourmand Tirana @@ -37,13 +37,7 @@ coin.gourmand@yourcompany.example.com - - - 8 - 23 - - - + Pizza Inn New Delhi TN @@ -62,12 +56,6 @@ info@corner.com - - - 8 - 23 - - Sushi Shop Paris @@ -77,232 +65,178 @@ order@sushi.com - - - 8 - 23 - - Bacon 7.5 - + Beef, Bacon, Salad, Cheddar, Fried Onion, BBQ Sauce - Cheese Burger 7.0 - + Beef, Cheddar, Salad, Fried Onions, BBQ Sauce - Chicken Curry 3.0 - - + Spicy Tuna 3.0 - - + Mozzarella 3.9 - + Mozzarella, Pesto, Tomatoes - Club 3.4 - + Ham, Cheese, Vegetables - Lunch Maki 18pc 12.0 - + 6 Maki Salmon - 6 Maki Tuna - 6 Maki Shrimp/Avocado - Lunch Salmon 20pc 13.80 - + 4 Sushi Salmon - 6 Maki Salmon - 4 Sashimi Salmon - Lunch Temaki mix 3pc 14.0 - - 1 Avocado - 1 Salmon - 1 Eggs - 1 Tuna - + + 1 Avocado/Cooked Tuna - 1 Salmon/Eggs - 1 Salmon/Avocado - 1 Tuna/Mango Salmon and Avocado 9.25 - - 2 Tempuras, Cabbages, Onions, Sesame Sauce - - - - - - mail - - 11 - 8 - 23 - reccurent - - - - - + + Shrimp Tempura, Yam Tempura, Chinese Cabbage, Pickled Onions, Sesame Sauce Cheese And Ham 3.30 - + Cheese, Ham, Salad, Tomatoes, cucumbers, eggs - The Country 3.30 - + Brie, Honey, Walnut Kernels - Tuna 2.50 - + Tuna, Mayonnaise - Gouda Cheese 2.50 - + - Chicken Curry 2.60 - + - Pizza Margherita 6.90 - + Tomatoes, Mozzarella - Pizza Funghi 7.00 - + Tomatoes, Mushrooms, Mozzarella - Pizza Vegetarian 7.00 - + Tomatoes, Mozzarella, Mushrooms, Peppers, Olives - Pizza Italiana 7.40 - + Fresh Tomatoes, Basil, Mozzarella - Bolognese Pasta 7.70 - + - Napoli Pasta 7.70 - + Tomatoes, Basil - - - - - Garniture - - - - Olives - 0.30 - - 4 Formaggi 5.50 - + Tomato sauce, Olive oil, Fresh Tomatoes, Onions, Vegetables, Parmesan - @@ -310,7 +244,6 @@ new - @@ -318,7 +251,6 @@ confirmed - @@ -326,7 +258,6 @@ confirmed - @@ -342,9 +273,9 @@ 7.70 new - + + +Emmental - 1 @@ -353,9 +284,9 @@ 7.40 confirmed - + + +Mushrooms - 1 @@ -364,9 +295,9 @@ 2.50 cancelled - + + +Salad +Tomatoes +Cucumbers - 1 @@ -375,9 +306,9 @@ 2.60 confirmed - + + +Salad +Tomatoes +Eggs - 1 @@ -386,7 +317,7 @@ 5.50 confirmed - + @@ -395,7 +326,7 @@ Pizza Italiana -7.40 - + order @@ -404,7 +335,7 @@ Chicken curry -2.60 - + order @@ -429,8 +360,22 @@ Pasta 4 Formaggi -5.50 - + order + + + week + + + + + + + + 7 + 23 + Lunch must be ordered before 10:30 AM + diff --git a/addons/lunch/models/__init__.py b/addons/lunch/models/__init__.py index 1bdf9862ffe..f99750c87d0 100644 --- a/addons/lunch/models/__init__.py +++ b/addons/lunch/models/__init__.py @@ -4,4 +4,3 @@ from . import lunch_alert from . import lunch_cashmove from . import lunch_order from . import lunch_product -from . import lunch_supplier diff --git a/addons/lunch/models/lunch_alert.py b/addons/lunch/models/lunch_alert.py index bd0e7c04a9b..325a64f4385 100644 --- a/addons/lunch/models/lunch_alert.py +++ b/addons/lunch/models/lunch_alert.py @@ -1,31 +1,9 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import math -import pytz +import datetime -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) +from odoo import api, fields, models, _ class LunchAlert(models.Model): @@ -35,81 +13,73 @@ class LunchAlert(models.Model): _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) - 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') + @api.multi + def name_get(self): + return [(alert.id, '%s %s' % (_('Alert'), '#%d' % alert.id)) for alert in self] - 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() + @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: - time_from = float_to_time(alert.recurrency_from) - time_to = float_to_time(alert.recurrency_to) + can_display_alert = { + 'specific': (str(alert.specific_day) == fields.Date.to_string(mynow)), + 'week': alert[days_codes[mynow.strftime('%w')]], + 'days': True + } - 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) + 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) - 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]) + if fullday or (from_alert <= mynow.time() <= to_alert): + alert.display = True + else: + alert.display = False diff --git a/addons/lunch/models/lunch_cashmove.py b/addons/lunch/models/lunch_cashmove.py index 2cb71a77c69..7d4a6b56ae4 100644 --- a/addons/lunch/models/lunch_cashmove.py +++ b/addons/lunch/models/lunch_cashmove.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models, _ -from odoo.tools import float_round class LunchCashMove(models.Model): @@ -15,14 +14,10 @@ class LunchCashMove(models.Model): 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_line_id = fields.Many2one('lunch.order.line', 'Order', ondelete='cascade') + 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] - - @api.model - def get_wallet_balance(self, user): - return float_round(sum(move['amount'] for move in self.search_read([('user_id', '=', user.id)], ['amount'])), precision_digits=2) diff --git a/addons/lunch/models/lunch_order.py b/addons/lunch/models/lunch_order.py index cadd1ca449e..f4354913db8 100644 --- a/addons/lunch/models/lunch_order.py +++ b/addons/lunch/models/lunch_order.py @@ -1,15 +1,19 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from collections import OrderedDict + +import json + from odoo import api, fields, models, _ -from odoo.exceptions import UserError, ValidationError +from odoo.exceptions import AccessError, ValidationError from odoo.addons import decimal_precision as dp 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. + date. When creating a lunch order, applicable lunch alerts are displayed. """ _name = 'lunch.order' _description = 'Lunch Order' @@ -35,29 +39,24 @@ class LunchOrder(models.Model): states={'new': [('readonly', False)], False: [('readonly', False)]}) total = fields.Float(compute='_compute_total', string="Total", store=True) state = fields.Selection([('new', 'New'), - ('ordered', 'Ordered'), ('confirmed', 'Received'), ('cancelled', 'Cancelled')], - 'Status', readonly=True, index=True, copy=False, default='new') + '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') - mail_sent = fields.Boolean('Mail Sent', readonly=True) - supplier_ids = fields.Many2many('lunch.supplier', compute='_compute_supplier_ids', store=True) - - @api.depends('order_line_ids', 'order_line_ids.supplier_id', 'order_line_ids.product_id') - def _compute_supplier_ids(self): - for order in self: - order.supplier_ids = order.mapped('order_line_ids.supplier_id') - - @api.depends('order_line_ids', 'order_line_ids.quantity', 'order_line_ids.price') + @api.one + @api.depends('order_line_ids') def _compute_total(self): """ get and sum the order lines' price """ - self.ensure_one() self.total = sum( orderline.price for orderline in self.order_line_ids) @@ -65,55 +64,96 @@ class LunchOrder(models.Model): 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): - self.ensure_one() domain = [('user_id', '=', self.user_id.id)] lunch_cash = self.env['lunch.cashmove'].read_group(domain, ['amount', 'user_id'], ['user_id']) - if lunch_cash: + 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 """ - self.ensure_one() 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.')) - def action_order(self): - for order in self: - for line in order.order_line_ids: - self.env['lunch.cashmove'].sudo().create({ - 'user_id': line.user_id.id, - 'amount': -line.price, - 'description': line.product_id.name, - 'order_line_id': line.id, - 'state': 'order', - 'date': line.date, - }) + @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 - self.mapped('order_line_ids').write({'state': 'ordered'}) - self.write({'state': 'ordered'}) - - def action_confirm(self, supplier=None): - for order in self: - if supplier: - order.order_line_ids.filtered(lambda line: line.state == 'ordered' and line.supplier_id == supplier).write({'state': 'confirmed'}) + if isConfirmed: + self.state = 'confirmed' else: - order.order_line_ids.filtered(lambda line: line.state == 'ordered').write({'state': 'confirmed'}) - order.write({'state': 'confirmed', - 'mail_sent': True}) - - def action_cancel(self): - for order in self: - # RLI FIXME maybe we don't need to filter - order.order_line_ids.filtered(lambda line: line.state != 'confirmed').action_cancel() - order.write({'state': 'cancelled'}) + self.state = 'new' + return class LunchOrderLine(models.Model): @@ -123,58 +163,153 @@ 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.topping', string='Toppings') - product_id = fields.Many2one('lunch.product', string="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) - supplier_id = fields.Many2one('lunch.supplier', string='Vendor', related='product_id.supplier_id', - readonly=True, store=True, index=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('Total Price', compute='_compute_total_price', readonly=True, store=True, + price = fields.Float(related='product_id.price', readonly=True, store=True, digits=dp.get_precision('Account')) state = fields.Selection([('new', 'New'), - ('ordered', 'Ordered'), ('confirmed', 'Received'), + ('ordered', 'Ordered'), ('cancelled', 'Cancelled')], 'Status', readonly=True, index=True, default='new') - cashmove = fields.One2many('lunch.cashmove', 'order_line_id', 'Cash Move') + cashmove = fields.One2many('lunch.cashmove', 'order_id', 'Cash Move') currency_id = fields.Many2one('res.currency', related='order_id.currency_id', readonly=False) - quantity = fields.Float('Quantity', required=True, default=1) - @api.depends('topping_ids', 'product_id', 'quantity') - def _compute_total_price(self): - for line in self: - line.price = line.quantity * (line.product_id.price + sum(line.topping_ids.mapped('price'))) + 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) - def update_quantity(self, increment): - for line in self: - old_cost = line.price - line.quantity += increment - new_cost = line.price - if new_cost - old_cost > self.env['lunch.cashmove'].get_wallet_balance(line.user_id): - raise UserError(_('Your wallet does not contain enough money to order that.' - 'To add some money to your wallet, please contact your lunch manager.')) - cashmove = self.env['lunch.cashmove'].search([('order_line_id', '=', line.id)], limit=1) - if cashmove: - cashmove.amount = -new_cost - if line.quantity <= 0: - order_id = line.order_id - line.sudo().unlink() - if not order_id.order_line_ids: - order_id.sudo().unlink() + @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 - def action_confirm(self): - self.write({'state': 'confirmed'}) + @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 - for order in self.mapped('order_id'): - if all(line.state == 'confirmed' for line in order.order_line_ids): - order.write({'state': 'confirmed', 'mail_sent': True}) + 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'}) - def action_cancel(self): - for line in self: - line.cashmove.sudo().unlink() + 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 - self.write({'state': 'cancelled'}) + 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.")) diff --git a/addons/lunch/models/lunch_product.py b/addons/lunch/models/lunch_product.py index af3d25df7d7..8ded108fc81 100644 --- a/addons/lunch/models/lunch_product.py +++ b/addons/lunch/models/lunch_product.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from dateutil.relativedelta import relativedelta - -from odoo import api, fields, models, tools, _ +from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp +from odoo.osv import expression class LunchProductCategory(models.Model): @@ -14,37 +13,6 @@ 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): @@ -52,51 +20,44 @@ class LunchProduct(models.Model): _name = 'lunch.product' _description = 'Lunch Product' - name = fields.Char('Name', required=True) + 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'), required=True) - supplier_id = fields.Many2one('lunch.supplier', 'Vendor', required=True) + 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') - # image: all image fields are base64 encoded and PIL-supported - image = fields.Binary( - "Image", attachment=True, - help="This field holds the image used as image for the product, limited to 1024x1024px.") - image_medium = fields.Binary( - "Medium-sized image", attachment=True, - help="Medium-sized image of the product. It is automatically " - "resized as a 128x128px image, with aspect ratio preserved, " - "only when the image exceeds one of those sizes. Use this field in form views or some kanban views.") - image_small = fields.Binary( - "Small-sized image", attachment=True, - help="Small-sized image of the product. It is automatically " - "resized as a 64x64px image, with aspect ratio preserved. " - "Use this field anywhere a small image is required.") - - already_ordered = fields.Boolean('Has Already Been Ordered', compute='_compute_already_ordered') - - @api.model - def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None): - res = super(LunchProduct, self).search_read(domain, fields, offset, limit, order) - - if not order and 'already_ordered' in fields: - res = sorted(res, key=lambda x: x['already_ordered'], reverse=True) - - return res - - @api.model_create_multi - def create(self, vals_list): - for values in vals_list: - tools.image_resize_images(values) - return super(LunchProduct, self).create(vals_list) - - def write(self, values): - tools.image_resize_images(values) - return super(LunchProduct, self).write(values) - - def _compute_already_ordered(self): - last_ordered = fields.Date.today() - relativedelta(weeks=2) + @api.depends('supplier') + def _get_available_product(self): for product in self: - product.already_ordered = bool(self.env['lunch.order.line'].search_count( - [('product_id', '=', product.id), ('date', '>=', last_ordered), ('user_id', '=', self.env.user.id)])) + 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)] diff --git a/addons/lunch/models/lunch_supplier.py b/addons/lunch/models/lunch_supplier.py deleted file mode 100644 index ce16eb8460b..00000000000 --- a/addons/lunch/models/lunch_supplier.py +++ /dev/null @@ -1,183 +0,0 @@ -# -*- 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 - -from odoo.addons.base.models.res_partner import _tz_get - - -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 LunchSupplier(models.Model): - _name = 'lunch.supplier' - _description = 'Lunch Supplier' - - partner_id = fields.Many2one('res.partner', string='Linked partner', required=True) - - name = fields.Char('Name', related='partner_id.name') - description = fields.Text('Description') - - email = fields.Char(related='partner_id.email') - email_formatted = fields.Char(related='partner_id.email_formatted', readonly=True) - phone = fields.Char(related='partner_id.phone') - street = fields.Char(related='partner_id.street') - street2 = fields.Char(related='partner_id.street2') - zip_code = fields.Char(related='partner_id.zip') - city = fields.Char(related='partner_id.city') - state_id = fields.Many2one("res.country.state", related='partner_id.state_id') - country_id = fields.Many2one('res.country', related='partner_id.country_id') - - 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) - - send_by = fields.Selection([ - ('phone', 'Phone'), - ('mail', 'Email'), - ], 'Send Order By', default='phone') - automatic_email_send = fields.Boolean('Automatic Email Sending') - automatic_email_time = fields.Float('Hour') - - 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', 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') - - available_today = fields.Boolean('This is True when if the supplier is available today', - compute='_compute_available_today', search='_search_available_today') - - tz = fields.Selection(_tz_get, string='Timezone', required=True, default='UTC') - - _sql_constraints = [ - ('automatic_email_time_range', 'CHECK(automatic_email_time >= 0 AND automatic_email_time <= 24)', 'Automatic Email Sending Time should be between 0 and 24'), - ('recurrency_from', 'CHECK(recurrency_from >= 0 AND recurrency_from <= 24)', 'Recurrency From should be between 0 and 24'), - ('recurrency_to', 'CHECK(recurrency_to >= 0 AND recurrency_to <= 24)', 'Recurrency To should be between 0 and 24') - ] - - @api.model - def _auto_email_send(self): - """ - This method is called every 10 minutes via a cron. - Its job is simply to get all the orders made for each supplier and send an email - automatically to the supplier if the supplier is configured for it and we are ready - to send it (usually at 11am or so) - """ - records = self.search([('automatic_email_send', '=', True)]) - - for supplier in records: - send_at = datetime.combine(fields.Date.today(), float_to_time(supplier.automatic_email_time, supplier.tz)).astimezone(pytz.UTC).replace(tzinfo=None) - if supplier.available_today and fields.Datetime.now() > send_at: - orders = self.env['lunch.order'].search([('mail_sent', '=', False), ('supplier_ids', 'in', [supplier.id]), ('state', '=', 'ordered')]) - - if orders: - order = { - 'company_name': orders[0].company_id.name, - 'currency_id': orders[0].currency_id.id, - 'supplier_id': supplier.partner_id.id, - 'supplier_name': supplier.name, - 'supplier_email': supplier.email_formatted - } - - lines = orders.mapped('order_line_ids').filtered(lambda line: line.supplier_id == supplier) - lines = [{'product': line.product_id.name, 'note': line.note, 'quantity': line.quantity, 'price': line.price} for line in lines] - - order['amount_total'] = sum(line['price'] for line in lines) - - self.env.ref('lunch.lunch_order_mail_supplier').with_context(order=order, lines=lines).send_mail(supplier.id) - - orders.action_confirm(supplier) - - @api.depends('recurrency_date', 'recurrency_from') - def _compute_recurrency_date_from(self): - for supplier in self: - if supplier.recurrency_date and supplier.recurrency_from: - supplier.recurrency_date_from = datetime.combine(supplier.recurrency_date, float_to_time(supplier.recurrency_from)) - - @api.depends('recurrency_date', 'recurrency_to') - def _compute_recurrency_date_to(self): - for supplier in self: - if supplier.recurrency_date and supplier.recurrency_to: - supplier.recurrency_date_to = datetime.combine(supplier.recurrency_date, float_to_time(supplier.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().replace(tzinfo=pytz.UTC) - - for supplier in self: - now = now.astimezone(pytz.timezone(supplier.tz)) - time_from = float_to_time(supplier.recurrency_from) - time_to = float_to_time(supplier.recurrency_to) - - if supplier.recurrency == 'once': - 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) - - 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/security/ir.model.access.csv b/addons/lunch/security/ir.model.access.csv index 06df5f431de..244fbf11f18 100644 --- a/addons/lunch/security/ir.model.access.csv +++ b/addons/lunch/security/ir.model.access.csv @@ -4,17 +4,10 @@ order_line_manager,"Order Line user",model_lunch_order_line,group_lunch_manager, cashmove_manager,"Cashmove user",model_lunch_cashmove,group_lunch_manager,1,1,1,1 product_manager,"Product user",model_lunch_product,group_lunch_manager,1,1,1,1 product_category_manager,"Product category user",model_lunch_product_category,group_lunch_manager,1,1,1,1 +alert_manager,"Alert user",model_lunch_alert,group_lunch_manager,1,1,1,1 order_user,"Order user",model_lunch_order,group_lunch_user,1,1,1,0 order_line_user,"Order Line user",model_lunch_order_line,group_lunch_user,1,1,1,1 cashmove_user,"Cashmove user",model_lunch_cashmove,group_lunch_user,1,0,0,0 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 +alert_user,"Alert user",model_lunch_alert,group_lunch_user,1,0,0,0 diff --git a/addons/lunch/static/img/4formaggio.png b/addons/lunch/static/img/4formaggio.png deleted file mode 100644 index f19c95ac934..00000000000 Binary files a/addons/lunch/static/img/4formaggio.png and /dev/null differ diff --git a/addons/lunch/static/img/Coke.png b/addons/lunch/static/img/Coke.png deleted file mode 100644 index e822a2656ac..00000000000 Binary files a/addons/lunch/static/img/Coke.png and /dev/null differ diff --git a/addons/lunch/static/img/bacon_burger.png b/addons/lunch/static/img/bacon_burger.png deleted file mode 100644 index ad4177d8ac2..00000000000 Binary files a/addons/lunch/static/img/bacon_burger.png and /dev/null differ diff --git a/addons/lunch/static/img/brie.png b/addons/lunch/static/img/brie.png deleted file mode 100644 index 5b76bfbb1ee..00000000000 Binary files a/addons/lunch/static/img/brie.png and /dev/null differ diff --git a/addons/lunch/static/img/cheeseburger.png b/addons/lunch/static/img/cheeseburger.png deleted file mode 100644 index f9d5a3e5c01..00000000000 Binary files a/addons/lunch/static/img/cheeseburger.png and /dev/null differ diff --git a/addons/lunch/static/img/chicken_curry.png b/addons/lunch/static/img/chicken_curry.png deleted file mode 100644 index 09e494482d6..00000000000 Binary files a/addons/lunch/static/img/chicken_curry.png and /dev/null differ diff --git a/addons/lunch/static/img/chirashi.png b/addons/lunch/static/img/chirashi.png deleted file mode 100644 index 65143711848..00000000000 Binary files a/addons/lunch/static/img/chirashi.png and /dev/null differ diff --git a/addons/lunch/static/img/club.png b/addons/lunch/static/img/club.png deleted file mode 100644 index 5f3e36f3f37..00000000000 Binary files a/addons/lunch/static/img/club.png and /dev/null differ diff --git a/addons/lunch/static/img/coke_zero.png b/addons/lunch/static/img/coke_zero.png deleted file mode 100644 index 72dbba5542b..00000000000 Binary files a/addons/lunch/static/img/coke_zero.png and /dev/null differ diff --git a/addons/lunch/static/img/fanta.png b/addons/lunch/static/img/fanta.png deleted file mode 100644 index b7a48332e0d..00000000000 Binary files a/addons/lunch/static/img/fanta.png and /dev/null differ diff --git a/addons/lunch/static/img/fuze_black.png b/addons/lunch/static/img/fuze_black.png deleted file mode 100644 index 1daac338d90..00000000000 Binary files a/addons/lunch/static/img/fuze_black.png and /dev/null differ diff --git a/addons/lunch/static/img/fuze_green.png b/addons/lunch/static/img/fuze_green.png deleted file mode 100644 index 9a274ab6796..00000000000 Binary files a/addons/lunch/static/img/fuze_green.png and /dev/null differ diff --git a/addons/lunch/static/img/gouda.png b/addons/lunch/static/img/gouda.png deleted file mode 100644 index c50d6d8dfbf..00000000000 Binary files a/addons/lunch/static/img/gouda.png and /dev/null differ diff --git a/addons/lunch/static/img/italiana.png b/addons/lunch/static/img/italiana.png deleted file mode 100644 index 2f8c4244436..00000000000 Binary files a/addons/lunch/static/img/italiana.png and /dev/null differ diff --git a/addons/lunch/static/img/lipton.png b/addons/lunch/static/img/lipton.png deleted file mode 100644 index a1f8ef21f2e..00000000000 Binary files a/addons/lunch/static/img/lipton.png and /dev/null differ diff --git a/addons/lunch/static/img/maki.png b/addons/lunch/static/img/maki.png deleted file mode 100644 index c92dd8c83a4..00000000000 Binary files a/addons/lunch/static/img/maki.png and /dev/null differ diff --git a/addons/lunch/static/img/mozza.png b/addons/lunch/static/img/mozza.png deleted file mode 100644 index 8820fbb9cd5..00000000000 Binary files a/addons/lunch/static/img/mozza.png and /dev/null differ diff --git a/addons/lunch/static/img/napoli.png b/addons/lunch/static/img/napoli.png deleted file mode 100644 index c7ecd65db8a..00000000000 Binary files a/addons/lunch/static/img/napoli.png and /dev/null differ diff --git a/addons/lunch/static/img/pasta_bolognese.png b/addons/lunch/static/img/pasta_bolognese.png deleted file mode 100644 index 64f9972e5c2..00000000000 Binary files a/addons/lunch/static/img/pasta_bolognese.png and /dev/null differ diff --git a/addons/lunch/static/img/pizza_funghi.png b/addons/lunch/static/img/pizza_funghi.png deleted file mode 100644 index 00bceba96b6..00000000000 Binary files a/addons/lunch/static/img/pizza_funghi.png and /dev/null differ diff --git a/addons/lunch/static/img/pizza_margherita.png b/addons/lunch/static/img/pizza_margherita.png deleted file mode 100644 index 0e317265524..00000000000 Binary files a/addons/lunch/static/img/pizza_margherita.png and /dev/null differ diff --git a/addons/lunch/static/img/pizza_veggie.png b/addons/lunch/static/img/pizza_veggie.png deleted file mode 100644 index 950afb54a12..00000000000 Binary files a/addons/lunch/static/img/pizza_veggie.png and /dev/null differ diff --git a/addons/lunch/static/img/salmon_sushi.png b/addons/lunch/static/img/salmon_sushi.png deleted file mode 100644 index 1467b54f120..00000000000 Binary files a/addons/lunch/static/img/salmon_sushi.png and /dev/null differ diff --git a/addons/lunch/static/img/temaki.png b/addons/lunch/static/img/temaki.png deleted file mode 100644 index 8f47e7c035a..00000000000 Binary files a/addons/lunch/static/img/temaki.png and /dev/null differ diff --git a/addons/lunch/static/img/tuna_sandwich.png b/addons/lunch/static/img/tuna_sandwich.png deleted file mode 100644 index b616d6156a5..00000000000 Binary files a/addons/lunch/static/img/tuna_sandwich.png and /dev/null differ diff --git a/addons/lunch/static/src/js/lunch.js b/addons/lunch/static/src/js/lunch.js new file mode 100644 index 00000000000..8c455083b0d --- /dev/null +++ b/addons/lunch/static/src/js/lunch.js @@ -0,0 +1,94 @@ +odoo.define('lunch.previous_orders', function (require) { +"use strict"; + +var AbstractField = require('web.AbstractField'); +var core = require('web.core'); +var field_registry = require('web.field_registry'); +var field_utils = require('web.field_utils'); + +var QWeb = core.qweb; + +var LunchPreviousOrdersWidget = AbstractField.extend({ + events: { + 'click .o_add_button': '_onAddOrder', + }, + supportedFieldTypes: ['one2many'], + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + this.lunchData = JSON.parse(this.value); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Used by the widget to render the previous order price like a monetary. + * + * @private + * @param {Object} order + * @returns {string} the monetary formatting of order price + */ + _formatValue: function (order) { + var options = _.extend({}, this.nodeOptions, order); + return field_utils.format.monetary(order.price, this.field, options); + }, + /** + * @private + * @override + */ + _render: function () { + if (this.lunchData !== false) { + // group data by supplier for display + var categories = _.groupBy(this.lunchData, 'supplier'); + this.$el.html(QWeb.render('LunchPreviousOrdersWidgetList', { + formatValue: this._formatValue.bind(this), + categories: categories, + })); + } else { + return this.$el.html(QWeb.render('LunchPreviousOrdersWidgetNoOrder')); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {MouseEvent} event + */ + _onAddOrder: function (event) { + // Get order details from line + var lineID = parseInt($(event.currentTarget).data('id')); + if (!lineID) { + return; + } + var values = { + product_id: { + id: this.lunchData[lineID].product_id, + display_name: this.lunchData[lineID].product_name, + }, + note: this.lunchData[lineID].note, + price: this.lunchData[lineID].price, + }; + + // create a new order line + this.trigger_up('field_changed', { + dataPointID: this.dataPointID, + changes: { + order_line_ids: { + operation: 'CREATE', + data: values, + }, + }, + }); + }, +}); + +field_registry.add('previous_order', LunchPreviousOrdersWidget); + +}); diff --git a/addons/lunch/static/src/js/lunch_kanban_controller.js b/addons/lunch/static/src/js/lunch_kanban_controller.js deleted file mode 100644 index 5c9a0228f46..00000000000 --- a/addons/lunch/static/src/js/lunch_kanban_controller.js +++ /dev/null @@ -1,190 +0,0 @@ -odoo.define('lunch.LunchKanbanController', function (require) { -"use strict"; - -/** - * This file defines the Controller for the Lunch Kanban view, which is an - * override of the KanbanController. - */ - -var core = require('web.core'); -var KanbanController = require('web.KanbanController'); -var LunchKanbanWidget = require('lunch.LunchKanbanWidget'); -var LunchPaymentDialog = require('lunch.LunchPaymentDialog'); -var session = require('web.session'); - -var qweb = core.qweb; -var _t = core._t; - -var LunchKanbanController = KanbanController.extend({ - custom_events: _.extend({}, KanbanController.prototype.custom_events, { - add_money: '_onAddMoney', - add_product: '_onAddProduct', - change_user: '_onUserChanged', - edit_order: '_onEditOrder', - open_wizard: '_onOpenWizard', - order_now: '_onOrderNow', - remove_product: '_onRemoveProduct', - save_order: '_onSaveOrder', - unlink_order: '_onUnlinkOrder', - }), - - /** - * @override - */ - init: function () { - this.userId = null; - this.editMode = false; - return this._super.apply(this, arguments); - }, - start: function () { - this.$el.addClass('o_lunch_kanban'); - return this._super.apply(this, arguments); - }, - - _fetchPaymentInfo: function (){ - return this._rpc({ - route: '/lunch/payment_message', - }); - }, - - _onAddMoney: function (ev) { - var self = this; - - ev.stopPropagation(); - - this._showPaymentDialog(_t('Your wallet is empty!')); - }, - _onAddProduct: function (ev) { - var self = this; - ev.stopPropagation(); - - this._rpc({ - model: 'lunch.order.line', - method: 'update_quantity', - args: [[ev.data.lineId], 1], - }).then(function () { - self.reload(); - }); - }, - _onEditOrder: function (ev) { - ev.stopPropagation(); - - this.editMode = true; - this.reload(); - }, - _onOpenWizard: function (ev) { - var self = this; - ev.stopPropagation(); - - var ctx = this.userId ? {default_user_id: this.userId}: {}; - - var options = { - on_close: function () { - self.reload(); - }, - }; - - this.do_action({ - res_model: 'lunch.order.line.temp', - type: 'ir.actions.act_window', - views: [[false, 'form']], - target: 'new', - context: _.extend(ctx, {default_product_id: ev.data.res_id}), - }, options); - }, - _onOrderNow: function (ev) { - var self = this; - ev.stopPropagation(); - - this._rpc({ - route: '/lunch/pay', - params: { - user_id: this.userId, - }, - }).then(function (isPaid) { - if (isPaid) { - // TODO: feedback? - self.reload(); - } else { - self._showPaymentDialog(); - self.reload(); - } - }); - }, - _onRemoveProduct: function (ev) { - var self = this; - ev.stopPropagation(); - - this._rpc({ - model: 'lunch.order.line', - method: 'update_quantity', - args: [[ev.data.lineId], -1], - }).then(function () { - self.reload(); - }); - }, - _onSaveOrder: function (ev) { - ev.stopPropagation(); - - this.editMode = false; - this.reload(); - }, - _onUserChanged: function (ev) { - ev.stopPropagation(); - - this.userId = ev.data.userId; - this.reload(); - }, - _onUnlinkOrder: function (ev) { - var self = this; - ev.stopPropagation(); - - this._rpc({ - route: '/lunch/trash', - params: { - user_id: this.userId, - }, - }).then(function () { - self.reload(); - }); - }, - _orderPaid: function () { - Dialog.alert(this, _t('Your order has been paid have a nice day'), {'title': 'Order Paid'}); - }, - _orderNotPaid: function () { - this._showPaymentDialog(); - }, - _showPaymentDialog: function (title) { - var self = this; - - title = title || ''; - - this._fetchPaymentInfo().then(function (data) { - var paymentDialog = new LunchPaymentDialog(self, _.extend(data, {title: title})); - paymentDialog.open(); - }); - }, - _update: function () { - var self = this; - - this._rpc({ - route: '/lunch/infos', - params: { - user_id: this.userId, - }, - }).then(function (data) { - if (self.widget) { - self.widget.destroy(); - } - data.wallet = parseFloat(data.wallet).toFixed(2); - self.widget = new LunchKanbanWidget(self, _.extend(data, {edit: self.editMode})); - self.widget.insertBefore(self.$('.o_kanban_view')); - }); - - return this._super.apply(this, arguments); - }, -}); - -return LunchKanbanController; - -}); diff --git a/addons/lunch/static/src/js/lunch_kanban_model.js b/addons/lunch/static/src/js/lunch_kanban_model.js deleted file mode 100644 index c28f5594b7a..00000000000 --- a/addons/lunch/static/src/js/lunch_kanban_model.js +++ /dev/null @@ -1,19 +0,0 @@ -odoo.define('lunch.LunchKanbanModel', function (require) { -"use strict"; - -/** - * This file defines the Model for the Lunch Kanban view, which is an - * override of the KanbanModel. - */ - -var KanbanModel = require('web.KanbanModel'); -var core = require('web.core'); - -var _t = core._t; - -var LunchKanbanModel = KanbanModel.extend({ -}); - -return LunchKanbanModel; - -}); diff --git a/addons/lunch/static/src/js/lunch_kanban_record.js b/addons/lunch/static/src/js/lunch_kanban_record.js deleted file mode 100644 index 45a928b892d..00000000000 --- a/addons/lunch/static/src/js/lunch_kanban_record.js +++ /dev/null @@ -1,22 +0,0 @@ -odoo.define('lunch.LunchKanbanRecord', function (require) { -"use strict"; - -/** - * This file defines the KanbanRecord for the Lunch Kanban view. - */ - -var KanbanRecord = require('web.KanbanRecord'); - -var LunchKanbanRecord = KanbanRecord.extend({ - _onGlobalClick: function (ev) { - ev.preventDefault(); - // ignore clicks on oe_kanban_action elements - if (!$(ev.target).hasClass('oe_kanban_action')) { - this.trigger_up('open_wizard', {res_id: this.id}); - } - }, -}); - -return LunchKanbanRecord; - -}); diff --git a/addons/lunch/static/src/js/lunch_kanban_renderer.js b/addons/lunch/static/src/js/lunch_kanban_renderer.js deleted file mode 100644 index bffe639a655..00000000000 --- a/addons/lunch/static/src/js/lunch_kanban_renderer.js +++ /dev/null @@ -1,29 +0,0 @@ -odoo.define('lunch.LunchKanbanRenderer', function (require) { -"use strict"; - -/** - * This file defines the Renderer for the Lunch Kanban view, which is an - * override of the KanbanRenderer. - */ - -var LunchKanbanRecord = require('lunch.LunchKanbanRecord'); - -var KanbanRenderer = require('web.KanbanRenderer'); - -var LunchKanbanRenderer = KanbanRenderer.extend({ - config: _.extend({}, KanbanRenderer.prototype.config, { - KanbanRecord: LunchKanbanRecord, - }), - - /** - * @override - */ - start: function () { - this.$el.addClass('o_lunch_kanban_view position-relative align-content-start flex-grow-1 flex-shrink-1'); - return this._super.apply(this, arguments); - }, -}); - -return LunchKanbanRenderer; - -}); diff --git a/addons/lunch/static/src/js/lunch_kanban_view.js b/addons/lunch/static/src/js/lunch_kanban_view.js deleted file mode 100644 index 296ebac08cd..00000000000 --- a/addons/lunch/static/src/js/lunch_kanban_view.js +++ /dev/null @@ -1,42 +0,0 @@ -odoo.define('lunch.LunchKanbanView', function (require) { -"use strict"; - -var LunchKanbanController = require('lunch.LunchKanbanController'); -var LunchKanbanModel = require('lunch.LunchKanbanModel'); -var LunchKanbanRenderer = require('lunch.LunchKanbanRenderer'); - -var config = require('web.config'); -var core = require('web.core'); -var KanbanView = require('web.KanbanView'); -var view_registry = require('web.view_registry'); - -var _lt = core._lt; - -if (config.device.isMobile) { - // use the classical KanbanView in mobile - view_registry.add('lunch_kanban', KanbanView); - return; -} - -var LunchKanbanView = KanbanView.extend({ - config: _.extend({}, KanbanView.prototype.config, { - Controller: LunchKanbanController, - Model: LunchKanbanModel, - Renderer: LunchKanbanRenderer, - }), - display_name: _lt('Lunch Kanban'), - groupable: false, - - /** - * @override - */ - init: function () { - return this._super.apply(this, arguments); - }, -}); - -view_registry.add('lunch_kanban', LunchKanbanView); - -return LunchKanbanView; - -}); diff --git a/addons/lunch/static/src/js/lunch_kanban_widget.js b/addons/lunch/static/src/js/lunch_kanban_widget.js deleted file mode 100644 index cfc81ab8c47..00000000000 --- a/addons/lunch/static/src/js/lunch_kanban_widget.js +++ /dev/null @@ -1,87 +0,0 @@ -odoo.define('lunch.LunchKanbanWidget', function (require) { -"use strict"; - -var core = require('web.core'); -var session = require('web.session'); -var Widget = require('web.Widget'); - -var qweb = core.qweb; - -var LunchKanbanWidget = Widget.extend({ - template: 'LunchKanbanWidget', - custom_events: { - }, - events: { - 'click .o_add_money': '_onAddMoney', - 'click .o_add_product': '_onAddProduct', - 'click .o_lunch_widget_edit': '_onEditOrder', - 'click .o_lunch_widget_order_button': '_onOrderNow', - 'click .o_remove_product': '_onRemoveProduct', - 'click .o_lunch_widget_save': '_onSaveOrder', - 'click .o_lunch_widget_unlink': '_onUnlinkOrder', - 'change select': '_onUserChanged', - }, - - init: function (parent, params) { - var self = this; - this._super.apply(this, arguments); - this.data = params; - this.currency = session.get_currency(session.company_currency_id); - }, - renderElement: function () { - var self = this; - this._super.apply(this, arguments); - this.$('select').val(this.data.username); - }, - - _onAddMoney: function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - this.trigger_up('add_money', {}); - }, - _onAddProduct: function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - - this.trigger_up('add_product', {lineId: $(ev.currentTarget).data('id')}); - }, - _onEditOrder: function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - - this.trigger_up('edit_order', {}); - }, - _onOrderNow: function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - - this.trigger_up('order_now', {}); - }, - _onRemoveProduct: function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - - this.trigger_up('remove_product', {lineId: $(ev.currentTarget).data('id')}); - }, - _onSaveOrder: function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - - this.trigger_up('save_order', {}); - }, - _onUnlinkOrder: function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - - this.trigger_up('unlink_order', {}); - }, - _onUserChanged: function (ev) { - ev.stopPropagation(); - - this.trigger_up('change_user', {userId: $(ev.currentTarget).find(':selected').data('user-id')}); - } -}); - -return LunchKanbanWidget; - -}); diff --git a/addons/lunch/static/src/js/lunch_payment_dialog.js b/addons/lunch/static/src/js/lunch_payment_dialog.js deleted file mode 100644 index 00e49299189..00000000000 --- a/addons/lunch/static/src/js/lunch_payment_dialog.js +++ /dev/null @@ -1,24 +0,0 @@ -odoo.define('lunch.LunchPaymentDialog', function (require) { -"use strict"; - -var core = require('web.core'); -var Dialog = require('web.Dialog'); - -var qweb = core.qweb; - -var LunchPaymentDialog = Dialog.extend({ - template: 'lunch.LunchPaymentDialog', - - init: function (parent, options) { - var self = this; - this._super.apply(this, arguments); - - options = (options || {}); - - this.message = options.message || ''; - }, -}); - -return LunchPaymentDialog; - -}); diff --git a/addons/lunch/static/src/scss/lunch.scss b/addons/lunch/static/src/scss/lunch.scss new file mode 100644 index 00000000000..9e36f20dbd8 --- /dev/null +++ b/addons/lunch/static/src/scss/lunch.scss @@ -0,0 +1,14 @@ + +.o_web_client .o_lunch { + .o_lunch_last_orders { + width: 100%; + + .o_lunch_vignette { + padding: 5px 0; + + + .o_lunch_vignette { + border-top: 1px solid $o-brand-lightsecondary; + } + } + } +} diff --git a/addons/lunch/static/src/scss/lunch_kanban.scss b/addons/lunch/static/src/scss/lunch_kanban.scss deleted file mode 100644 index eff03959d52..00000000000 --- a/addons/lunch/static/src/scss/lunch_kanban.scss +++ /dev/null @@ -1,47 +0,0 @@ -.o_lunch_kanban { - .o_pink { - color: nth($o-colors, 10); - } - - .o_lunch_widget { - display: flex; - border-bottom: 1px solid #CED4DA; - background-color: white; - min-height: 90px; - overflow-y: visible; - - .o_lunch_widget_info { - padding: 5px 12px; - margin: 4px 8px; - width: 300px; - flex: 1 1 auto; - overflow-y: visible; - - .o_lunch_ordered { - color: white; - background-color: #F0D970; - padding: 1px 3px; - border-radius: .25em; - } - - .o_lunch_confirmed { - color: white; - background-color: #00BA4E; - padding: 1px 3px; - border-radius: .25em; - } - - .o_lunch_widget_title { - font-weight: bold; - font-size: initial; - } - - .o_lunch_widget_order_button { - display: block; - width: 100%; - margin-bottom: 4px; - margin-top: 12px; - } - } - } -} diff --git a/addons/lunch/static/src/xml/lunch_kanban.xml b/addons/lunch/static/src/xml/lunch_kanban.xml deleted file mode 100644 index 61077e54e13..00000000000 --- a/addons/lunch/static/src/xml/lunch_kanban.xml +++ /dev/null @@ -1,125 +0,0 @@ - - -
-
-
-
- -
-
-
- - - - - - -
-
- - - - - - - - - Add money - -
-
-
-
- - - - - - - -
-
- Your order - - Edit - Save -
-
-
-
-
-
- - - -
-
- -
-
- - - - -
-
-
-
- + -
-
- - - - -
-
-
- -
-
- -
-
- Total -
-
- - - - -
-
- -
-
-
- - - -
- - - - - - - - - - - - - - - -
- -
- diff --git a/addons/lunch/static/tests/lunch_tests.js b/addons/lunch/static/tests/lunch_tests.js new file mode 100644 index 00000000000..3ae1e3491cb --- /dev/null +++ b/addons/lunch/static/tests/lunch_tests.js @@ -0,0 +1,145 @@ +odoo.define('lunch.tests', function (require) { +"use strict"; + +var FormView = require('web.FormView'); +var testUtils = require('web.test_utils'); + +var createView = testUtils.createView; + +QUnit.module('lunch', { + beforeEach: function () { + this.data = { + order: { + fields: { + previous_order_widget: {type: 'text'}, + order_line_ids: {type: 'many2many', relation: 'order_line'} + }, + records: [{ + id: 1, + name: 'coucou', + }, { + id: 2, + name: 'coucou', + }, { + id: 11, + }], + }, + order_line: { + fields: { + product_id: {type: 'many2one', relation: 'product'}, + note: {type: 'text'}, + price: {type: 'float'}, + }, + }, + supplier: { + fields: {}, + records: [{ + id: 1, + display_name: "Coin Gourmant", + }, { + id: 2, + display_name: "Pizza Inn", + }] + }, + product: { + fields: {}, + records: [{ + id: 1, + name: 'Pizza Margherita', + }], + } + }; + }, +}, function () { + + QUnit.test("empty previous_order widget", function (assert) { + assert.expect(2); + + var form = createView({ + View: FormView, + arch: '
' + + '' + + '' + + '' + + '' + + '
', + data: this.data, + model: 'order', + archs: { + "order_line,false,list": '' + + '' + + '' + + '' + + '', + }, + }); + + var widgetText = form.$('.o_field_widget[name="previous_order_widget"]').text(); + assert.ok(widgetText.indexOf('This is the first time you order a meal') !== -1, + "the widget should display its no content text"); + assert.containsNone(form, '.o_lunch_vignette', + "there should be no vignette"); + + form.destroy(); + }); + + QUnit.test("add an order line with previous_order widget", function (assert) { + assert.expect(4); + + var form = createView({ + View: FormView, + arch: '
' + + '' + + '' + + '' + + '' + + '
', + data: this.data, + model: 'order', + mockRPC: function (route, args) { + if (args.method === 'default_get') { + return $.when({ + previous_order_widget: '{' + + '"8": {' + + '"note": "coucou",' + + '"line_id": 8,' + + '"digits": [69, 2],' + + '"product_id": 1,' + + '"currency": "$",' + + '"supplier": "Pizza Inn",' + + '"position": "before",' + + '"price": 6.9,' + + '"product_name": "Pizza Margherita"' + + '}' + + '}' + }); + } + return this._super.apply(this, arguments); + }, + archs: { + "order_line,false,list": '' + + '' + + '' + + '' + + '', + }, + }); + + assert.containsNone(form, '.o_list_view tr.o_data_row', + "there should be no row in the one2many list"); + + assert.containsOnce(form, '.o_lunch_vignette', + "there should be a vignette"); + + // add an order + testUtils.dom.click(form.$('.o_lunch_vignette .o_add_button')); + + assert.containsOnce(form, '.o_list_view tr.o_data_row', + "there should be one new row in the one2many list"); + assert.strictEqual(form.$('.o_list_view tr.o_data_row').text(), "Pizza Margheritacoucou6.90", + "the line should contain the correct data"); + + form.destroy(); + }); +}); +}); diff --git a/addons/lunch/static/tests/test_lunch_kanban.js b/addons/lunch/static/tests/test_lunch_kanban.js deleted file mode 100644 index 649c84b680e..00000000000 --- a/addons/lunch/static/tests/test_lunch_kanban.js +++ /dev/null @@ -1,210 +0,0 @@ -odoo.define('lunch.lunchKanbanTests', function (require) { -"use strict"; - -var LunchKanbanView = require('lunch.LunchKanbanView'); -var testUtils = require('web.test_utils'); - -var createView = testUtils.createView; - -QUnit.module('Views', { - beforeEach: function () { - this.data = { - product: { - fields: { - id: {string: 'ID', type: 'integer'}, - }, - records: [ - {id: 1, name: 'Tuna sandwich', price: 3.0}, - ] - }, - }; - }, -}, function () { - QUnit.module('LunchKanbanView'); - - QUnit.test('Simple rendering of LunchKanbanView', function (assert) { - assert.expect(8); - - var lunchKanban = createView({ - View: LunchKanbanView, - model: 'product', - data: this.data, - arch: '' + - '
' + - '
' + - '
', - mockRPC: function(route, args) { - if (route === '/lunch/infos') { - return $.when({ - order: 1, - wallet: 20, - username: 'Marc Demo', - userimage: '', - is_manager: false, - users: [ - {id: 1, name: 'Mitchell Admin'}, - {id: 2, name: 'Marc Demo'}, - ], - total: 7.4, - 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(); - } - return this._super(route, args); - }, - }); - - var $section = $(lunchKanban.$('.o_lunch_widget_info')[0]); - // username - assert.strictEqual($section.find('div:eq(1) div:eq(0)').text().trim(), 'Marc Demo', 'Username should have been Marc Demo'); - // wallet_balance - assert.strictEqual($section.find('div:eq(1) div:eq(1) span:eq(0)').text().trim(), '20.00', 'Wallet balance should have been 20'); - // your order section - $section = $(lunchKanban.$('.o_lunch_widget_info')[1]); - // only one line - assert.strictEqual($section.find('div:eq(2) div.d-flex').length, 1, 'There should be only one line'); - // quantity = 1 - assert.strictEqual($section.find('div:eq(2) div:eq(0) span:eq(0)').text().trim(), '1', 'The line should contain only one product'); - // buttons to remove and to add product - assert.strictEqual($section.find('.o_remove_product').length, 1, 'There should be a remove product button'); - assert.strictEqual($section.find('.o_add_product').length, 1, 'There should be a add product button'); - - $section = $(lunchKanban.$('.o_lunch_widget_info')[2]); - // total - assert.strictEqual($section.find('div:eq(0) div:eq(1)').text().trim(), '7.4', 'total should be of 7.4'); - // order now button - assert.strictEqual($section.find('button').length, 1, 'order now button should be available'); - - lunchKanban.destroy(); - }); - - QUnit.test('User interactions', function (assert) { - assert.expect(8); - - var state = 'new'; - - var lunchKanban = createView({ - View: LunchKanbanView, - model: 'product', - data: this.data, - arch: '' + - '
' + - '
' + - '
', - mockRPC: function(route, args) { - if (route === '/lunch/infos') { - return $.when({ - order: 1, - wallet: 20, - username: 'Marc Demo', - userimage: '', - is_manager: false, - users: [ - {id: 1, name: 'Mitchell Admin'}, - {id: 2, name: 'Marc Demo'}, - ], - total: 7.4, - 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'}); - } else if (route === '/lunch/pay') { - return $.when(true); - } else if (args.method === 'update_quantity') { - assert.step(args.args); - return $.when(); - } else if (route.startsWith('data:image/png;base64,')) { - return $.when(); - } - return this._super(route, args); - }, - }); - - lunchKanban.$('.o_add_money').click(); - var modal = $('.modal-dialog'); - assert.strictEqual(modal.find('.modal-body').text().trim(), 'Hello', 'Message should have been Hello'); - modal.find('.btn.btn-primary').click(); - - lunchKanban.$('.o_add_product').click(); - lunchKanban.$('.o_remove_product').click(); - - state = 'ordered'; - lunchKanban.$('.o_lunch_widget_order_button').click(); - // state is shown - assert.strictEqual(lunchKanban.$('.o_lunch_ordered').length, 1, 'state should be shown as ordered'); - // no more buttons - assert.strictEqual(lunchKanban.$('.o_remove_product').length, 0, 'button to remove product should not be shown anymore'); - assert.strictEqual(lunchKanban.$('.o_add_product').length, 0, 'button to add product should not be shown anymore'); - state = 'confirmed'; - lunchKanban.reload(); - // state is updated - assert.strictEqual(lunchKanban.$('.o_lunch_confirmed').length, 1, 'state should be shown as confirmed'); - - assert.verifySteps([ - [[1], 1], - [[1], -1], - ]); - - lunchKanban.destroy(); - }); - - QUnit.test('Manager interactions', function (assert) { - assert.expect(5); - - var lunchKanban = createView({ - View: LunchKanbanView, - model: 'product', - data: this.data, - arch: '' + - '
' + - '
' + - '
', - mockRPC: function(route, args) { - if (route === '/lunch/infos') { - return $.when({ - order: 1, - wallet: 20, - username: 'Marc Demo', - userimage: '', - is_manager: true, - users: [ - {id: 1, name: 'Mitchell Admin'}, - {id: 2, name: 'Marc Demo'}, - ], - total: 7.4, - 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(); - } - return this._super(route, args); - }, - }); - - var select = lunchKanban.$('select.o_input'); - var options = select.find('option'); - - assert.strictEqual(select.length, 1, 'There should be a selection field'); - assert.strictEqual(options.length, 2, 'Select should have two options'); - assert.strictEqual(options.text().trim(), 'Mitchell AdminMarc Demo', 'Options should be the name of the users'); - assert.strictEqual($(options[0]).data('user-id'), 1, 'user id should be on option'); - assert.strictEqual($(options[1]).data('user-id'), 2, 'user id should be on option'); - - lunchKanban.destroy(); - }); -}); - -}); diff --git a/addons/lunch/tests/__init__.py b/addons/lunch/tests/__init__.py index 08959c769be..f9d697d7b2c 100644 --- a/addons/lunch/tests/__init__.py +++ b/addons/lunch/tests/__init__.py @@ -1,4 +1,3 @@ # -*- coding: utf-8 -*- -from . import common -from . import test_supplier +from . import test_lunch diff --git a/addons/lunch/tests/common.py b/addons/lunch/tests/common.py deleted file mode 100644 index 3c5a31188e3..00000000000 --- a/addons/lunch/tests/common.py +++ /dev/null @@ -1,79 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.tests import common - - -class TestsCommon(common.TransactionCase): - - def setUp(self): - super(TestsCommon, self).setUp() - - self.partner_pizza_inn = self.env['res.partner'].create({ - 'name': 'Pizza Inn', - }) - - self.supplier_pizza_inn = self.env['lunch.supplier'].create({ - 'partner_id': self.partner_pizza_inn.id, - 'send_by': 'mail', - 'automatic_email_send': True, - 'automatic_email_time': 11, - 'recurrency': 'reccurent', - 'recurrency_from': 8, - 'recurrency_to': 23, - 'recurrency_monday': True, - 'recurrency_tuesday': True, - 'recurrency_wednesday': True, - 'recurrency_thursday': True, - 'recurrency_friday': True, - }) - - self.partner_coin_gourmand = self.env['res.partner'].create({ - 'name': 'Coin Gourmand', - }) - - self.supplier_coin_gourmand = self.env['lunch.supplier'].create({ - 'partner_id': self.partner_coin_gourmand.id, - 'send_by': 'phone', - 'recurrency': 'reccurent', - 'recurrency_from': 8, - 'recurrency_to': 23, - 'recurrency_monday': True, - 'recurrency_tuesday': True, - 'recurrency_wednesday': True, - 'recurrency_thursday': True, - 'recurrency_friday': True, - }) - - self.category_pizza = self.env['lunch.product.category'].create({ - 'name': 'Pizza', - }) - - self.category_sandwich = self.env['lunch.product.category'].create({ - 'name': 'Sandwich', - }) - - self.product_pizza = self.env['lunch.product'].create({ - 'name': 'Pizza', - 'category_id': self.category_pizza.id, - 'price': 9, - 'supplier_id': self.supplier_pizza_inn.id, - }) - - self.product_sandwich_tuna = self.env['lunch.product'].create({ - 'name': 'Tuna Sandwich', - 'category_id': self.category_sandwich.id, - 'price': 3, - 'supplier_id': self.supplier_coin_gourmand.id, - }) - - self.garniture_type = self.env['lunch.topping.type'].create({ - 'name': 'Graniture', - }) - - self.topping_olives = self.env['lunch.topping'].create({ - 'name': 'Olives', - 'price': 0.3, - 'category_id': self.category_pizza.id, - 'type_id': self.garniture_type.id, - }) diff --git a/addons/lunch/tests/test_lunch.py b/addons/lunch/tests/test_lunch.py new file mode 100644 index 00000000000..c6325a3f19c --- /dev/null +++ b/addons/lunch/tests/test_lunch.py @@ -0,0 +1,143 @@ +# -*- 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 dateutil import relativedelta + +class Test_Lunch(common.TransactionCase): + + def setUp(self): + """*****setUp*****""" + super(Test_Lunch, self).setUp() + + self.demo_user = self.env['res.users'].search([('name', '=', 'Marc Demo')]) + 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.id, + 'note': '+Emmental', + 'cashmove': [], + 'price': self.product_Bolognese_id.price, + }) + + def test_00_lunch_order(self): + """Change the state of an order line from 'new' to 'ordered'. Check that there are no cashmove linked to that order line""" + self.order_one = self.new_id_order_line + #we check that our order_line is a 'new' one and that there are no cashmove linked to that order_line: + self.assertEqual(self.order_one.state, 'new') + self.assertEqual(list(self.order_one.cashmove), []) + #we order that orderline so it's state will be 'ordered' + self.order_one.order() + self.order_one = self.new_id_order_line + #we check that our order_line is a 'ordered' one and that there are no cashmove linked to that order_line: + self.assertEqual(self.order_one.state, 'ordered') + self.assertEqual(list(self.order_one.cashmove), []) + + def test_01_lunch_order(self): + """Change the state of an order line from 'new' to 'ordered' then to 'confirmed'. Check that there is a cashmove linked to the order line""" + self.test_00_lunch_order() + #We receive the order so we confirm the order line so it's state will be 'confirmed' + #A cashmove will be created and we will test that the cashmove amount equals the order line price + self.order_one.confirm() + self.order_one = self.new_id_order_line + #we check that our order_line is a 'confirmed' one and that there are a cashmove linked to that order_line with an amount equals to the order line price: + self.assertEqual(self.order_one.state, 'confirmed') + self.assertTrue(self.order_one.cashmove) + self.assertTrue(self.order_one.cashmove[0].amount == -self.order_one.price) + + def test_02_lunch_order(self): + """Change the state of an order line from 'confirmed' to 'cancelled' and check that the cashmove linked to that order line will be deleted""" + self.test_01_lunch_order() + #We have a confirmed order with its associate cashmove + #We execute the cancel function + self.order_one.cancel() + self.order_one = self.new_id_order_line + #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 + }) + wednesday = date.today() + relativedelta.relativedelta(days=7, weekday=2) + next_tuesday = datetime.today() + relativedelta.relativedelta(days=14, weekday=1) + next_wednesday = datetime.today() + relativedelta.relativedelta(days=14, weekday=2) + + # We check that we can make an order on a Tuesday + order_01 = self.env['lunch.order'].create({ + 'user_id': self.demo_user.id, + 'date': date.today() + relativedelta.relativedelta(days=7, weekday=1), + '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': wednesday, + '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': wednesday, + }) + + # should now be possible to order tomorrow + order_03 = self.env['lunch.order'].create({ + 'user_id': self.demo_user.id, + 'date': wednesday, + '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': 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': next_wednesday, + 'order_line_ids': [(0, 0, { + 'product_id': self.product_Bolognese_id.id, + })] + }) + order_05.order_line_ids.order() + + diff --git a/addons/lunch/tests/test_supplier.py b/addons/lunch/tests/test_supplier.py deleted file mode 100644 index fe9776e854b..00000000000 --- a/addons/lunch/tests/test_supplier.py +++ /dev/null @@ -1,163 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from datetime import datetime -from unittest.mock import patch - -from odoo import fields - -from odoo.addons.lunch.tests.common import TestsCommon - - -class TestSupplier(TestsCommon): - def setUp(self): - super(TestSupplier, self).setUp() - - self.monday_3am = datetime(2018, 10, 29, 3, 0, 0) - self.monday_10am = datetime(2018, 10, 29, 10, 0, 0) - self.monday_1pm = datetime(2018, 10, 29, 13, 0, 0) - self.monday_8pm = datetime(2018, 10, 29, 20, 0, 0) - - self.saturday_3am = datetime(2018, 11, 3, 3, 0, 0) - self.saturday_10am = datetime(2018, 11, 3, 10, 0, 0) - self.saturday_1pm = datetime(2018, 11, 3, 13, 0, 0) - self.saturday_8pm = datetime(2018, 11, 3, 20, 0, 0) - - def test_compute_available_today(self): - tests = [(self.monday_3am, False), (self.monday_10am, True), - (self.monday_1pm, True), (self.monday_8pm, True), - (self.saturday_3am, False), (self.saturday_10am, False), - (self.saturday_1pm, False), (self.saturday_8pm, False)] - - for value, result in tests: - with patch.object(fields.Datetime, 'now', return_value=value) as _: - assert self.supplier_pizza_inn.available_today == result,\ - 'supplier pizza inn should %s considered available on %s' % ('be' if result else 'not be', value) - - self.env['lunch.supplier'].invalidate_cache(['available_today'], [self.supplier_pizza_inn.id]) - - def test_search_available_today(self): - ''' - This test checks that _search_available_today returns a valid domain - ''' - Supplier = self.env['lunch.supplier'] - - tests = [(self.monday_3am, 3.0, 'monday'), (self.monday_10am, 10.0, 'monday'), - (self.monday_1pm, 13.0, 'monday'), (self.monday_8pm, 20.0, 'monday'), - (self.saturday_3am, 3.0, 'saturday'), (self.saturday_10am, 10.0, 'saturday'), - (self.saturday_1pm, 13.0, 'saturday'), (self.saturday_8pm, 20.0, 'saturday')] - - # It should return an empty domain if we compare to values other than datetime - assert Supplier._search_available_today('>', 7) == [] - assert Supplier._search_available_today('>', True) == [] - - for value, rvalue, dayname in tests: - with patch.object(fields.Datetime, 'now', return_value=value) as _: - assert Supplier._search_available_today('=', True) == ['|', '&', '&', ('recurrency', '=', 'once'), ('recurrency_date_from', '<=', value), - ('recurrency_date_to', '>=', value), '&', '&', ('recurrency_%s' % (dayname), '=', True), - ('recurrency_from', '<=', rvalue), ('recurrency_to', '>=', rvalue)], 'Wrong domain generated for values (%s, %s)' % (value, rvalue) - - with patch.object(fields.Datetime, 'now', return_value=self.monday_10am) as _: - assert self.supplier_pizza_inn in Supplier.search([('available_today', '=', True)]) - - def test_auto_email_send(self): - order = self.env['lunch.order'].create({}) - line = self.env['lunch.order.line'].create({ - 'order_id': order.id, - 'product_id': self.product_pizza.id, - }) - - assert len(order.supplier_ids) == 1 and self.supplier_pizza_inn in order.supplier_ids - - order.action_order() - assert order.state == 'ordered' - assert line.state == 'ordered' - - with patch.object(fields.Datetime, 'now', return_value=self.monday_1pm): - with patch.object(fields.Date, 'today', return_value=self.monday_1pm.date()): - self.supplier_pizza_inn._auto_email_send() - - assert order.state == 'confirmed' - assert line.state == 'confirmed' - assert order.mail_sent - - # Do the same but this time order contains multiple lines with products from at least two suppliers - # Check if the lines for pizza_inn have been sent, and put in state='confirmed' only those lines should be - # moreover the order should not yet be in state='confirmed' as there still are some lines to confirm - order = self.env['lunch.order'].create({}) - line = self.env['lunch.order.line'].create({ - 'order_id': order.id, - 'product_id': self.product_pizza.id, - 'topping_ids': [(6, 0, [self.topping_olives.id])], - }) - line2 = self.env['lunch.order.line'].create({ - 'order_id': order.id, - 'product_id': self.product_sandwich_tuna.id, - }) - - assert len(order.supplier_ids) == 2 and self.supplier_coin_gourmand in order.supplier_ids and self.supplier_pizza_inn in order.supplier_ids - - order.action_order() - assert order.state == 'ordered' - assert line.state == 'ordered' - assert line2.state == 'ordered' - - with patch.object(fields.Datetime, 'now', return_value=self.monday_1pm): - with patch.object(fields.Date, 'today', return_value=self.monday_1pm.date()): - self.supplier_pizza_inn._auto_email_send() - - assert order.state == 'confirmed' - assert line.state == 'confirmed' - assert line2.state == 'ordered' - - assert order.mail_sent - - # Check with multiple orders - order_1 = self.env['lunch.order'].create({}) - - line_1_1 = self.env['lunch.order.line'].create({ - 'order_id': order_1.id, - 'product_id': self.product_pizza.id, - }) - - line_1_2 = line_1_1 = self.env['lunch.order.line'].create({ - 'order_id': order_1.id, - 'product_id': self.product_pizza.id, - 'topping_ids': [(6, 0, [self.topping_olives.id])] - }) - - order_2 = self.env['lunch.order'].create({}) - - line_2_1 = self.env['lunch.order.line'].create({ - 'order_id': order_2.id, - 'product_id': self.product_sandwich_tuna.id - }) - - line_2_2 = self.env['lunch.order.line'].create({ - 'order_id': order_2.id, - 'product_id': self.product_pizza.id, - }) - - order_3 = self.env['lunch.order'].create({}) - - line_3_1 = self.env['lunch.order.line'].create({ - 'order_id': order_3.id, - 'product_id': self.product_sandwich_tuna.id - }) - - (order_1 | order_2 | order_3).action_order() - - assert all(order.state == 'ordered' for order in [order_1, order_2, order_3]) - assert all(line.state == 'ordered' for line in [line_1_1, line_1_2, line_2_1, line_2_2, line_3_1]) - - with patch.object(fields.Datetime, 'now', return_value=self.monday_1pm): - with patch.object(fields.Date, 'today', return_value=self.monday_1pm.date()): - self.supplier_pizza_inn._auto_email_send() - - assert all(order.state == 'confirmed' for order in [order_1, order_2]) - assert order_3.state == 'ordered' - assert all(line.state == 'confirmed' for line in [line_1_1, line_1_2, line_2_2]) - assert all(line.state == 'ordered' for line in [line_2_1, line_3_1]) - - assert all(order.mail_sent for order in [order_1, order_2]) - assert not order_3.mail_sent diff --git a/addons/lunch/views/lunch_alert_views.xml b/addons/lunch/views/lunch_alert_views.xml index cbd587e3065..bb6d807a9ff 100644 --- a/addons/lunch/views/lunch_alert_views.xml +++ b/addons/lunch/views/lunch_alert_views.xml @@ -6,7 +6,7 @@ - + @@ -17,9 +17,9 @@ - - - + + + @@ -28,33 +28,107 @@ lunch.alert.form lunch.alert -
+ +
+ +
- - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + +
+ Products from this vendor can not be ordered at that moment. +
+
+ Products from this vendor can be ordered at that moment. +
+
+
+ + -
+ + lunch.alert.kanban + lunch.alert + + + + + + + + +
+
+ +
+
+
+ Between: - + +
+
+ + + +
+
+
+
+
+
+
+
+ - Lunch Alerts + Alerts lunch.alert + tree,kanban,form - tree,form - + +

+ Create a new lunch alert +

+ Alerts are used to warn employee from possible issues concerning the lunch orders. + To create a lunch alert you have to define its recurrence, the time interval during which the alert should be executed and the message to display. +

+ Example:
+ Recurency: Everyday,
+ Time interval: from 00h00 am to 11h59 pm,
+ Message: "You must order before 10h30 am". +

+
diff --git a/addons/lunch/views/lunch_orders_views.xml b/addons/lunch/views/lunch_orders_views.xml index b8665398153..8cd4658a8cb 100644 --- a/addons/lunch/views/lunch_orders_views.xml +++ b/addons/lunch/views/lunch_orders_views.xml @@ -19,6 +19,7 @@
+
@@ -42,15 +43,24 @@ +
+ +
+
+ +
+ - + @@ -85,7 +95,7 @@ - + @@ -98,14 +108,15 @@ - + -
- -