diff --git a/addons/lunch/__init__.py b/addons/lunch/__init__.py index b6f07433380..88710707227 100644 --- a/addons/lunch/__init__.py +++ b/addons/lunch/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import wizard +from . import controllers from . import models +from . import wizard diff --git a/addons/lunch/__manifest__.py b/addons/lunch/__manifest__.py index e28b88a82f2..5268ecca6a7 100644 --- a/addons/lunch/__manifest__.py +++ b/addons/lunch/__manifest__.py @@ -27,16 +27,17 @@ 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/lucky_order_view.xml', - 'views/lunch_alert_views.xml', + 'wizard/lunch_line_wizard_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.xml', ], + 'qweb': ['static/src/xml/lunch_kanban.xml', ], 'installable': True, 'application': True, 'certificate': '001292377792581874189', diff --git a/addons/lunch/controllers/__init__.py b/addons/lunch/controllers/__init__.py new file mode 100644 index 00000000000..12a7e529b67 --- /dev/null +++ b/addons/lunch/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/addons/lunch/controllers/main.py b/addons/lunch/controllers/main.py new file mode 100644 index 00000000000..314cf5ad292 --- /dev/null +++ b/addons/lunch/controllers/main.py @@ -0,0 +1,67 @@ +# -*- 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']), + } + + 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 new file mode 100644 index 00000000000..19275e67f21 --- /dev/null +++ b/addons/lunch/data/ir_cron_data.xml @@ -0,0 +1,17 @@ + + + + + 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 2332d2e5272..0f269f42d0c 100644 --- a/addons/lunch/data/lunch_data.xml +++ b/addons/lunch/data/lunch_data.xml @@ -1,23 +1,12 @@ - - Lunch: Order meals - - - code - -if records: - action = records.order() - - - Lunch: Receive meals code - records.confirm() + records.action_confirm() @@ -25,7 +14,7 @@ if records: code - records.cancel() + records.action_cancel() diff --git a/addons/lunch/data/lunch_demo.xml b/addons/lunch/data/lunch_demo.xml index bd36db1f0af..f91a57a183d 100644 --- a/addons/lunch/data/lunch_demo.xml +++ b/addons/lunch/data/lunch_demo.xml @@ -28,7 +28,7 @@ Chirashi - + Coin gourmand Tirana @@ -37,7 +37,13 @@ coin.gourmand@yourcompany.example.com - + + + 8 + 23 + + + Pizza Inn New Delhi TN @@ -56,6 +62,12 @@ info@corner.com + + + 8 + 23 + + Sushi Shop Paris @@ -65,178 +77,228 @@ 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/Cooked Tuna - 1 Salmon/Eggs - 1 Salmon/Avocado - 1 Tuna/Mango + + 1 Avocado - 1 Salmon - 1 Eggs - 1 Tuna + Salmon and Avocado 9.25 - - Shrimp Tempura, Yam Tempura, Chinese Cabbage, Pickled Onions, Sesame Sauce + + 2 Tempuras, Cabbages, Onions, Sesame Sauce + + + + + + mail + + 11 + 8 + 23 + reccurent + + + + + 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 + + + + + Olives + 0.30 + + 4 Formaggi 5.50 - + Tomato sauce, Olive oil, Fresh Tomatoes, Onions, Vegetables, Parmesan + @@ -244,6 +306,7 @@ new + @@ -251,6 +314,7 @@ confirmed + @@ -258,6 +322,7 @@ confirmed + @@ -273,9 +338,9 @@ 7.70 new - - +Emmental + + 1 @@ -284,9 +349,9 @@ 7.40 confirmed - - +Mushrooms + + 1 @@ -295,9 +360,9 @@ 2.50 cancelled - - +Salad +Tomatoes +Cucumbers + + 1 @@ -306,9 +371,9 @@ 2.60 confirmed - - +Salad +Tomatoes +Eggs + + 1 @@ -317,7 +382,7 @@ 5.50 confirmed - + @@ -326,7 +391,7 @@ Pizza Italiana -7.40 - + order @@ -335,7 +400,7 @@ Chicken curry -2.60 - + order @@ -360,22 +425,8 @@ 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 f99750c87d0..58a34e7891b 100644 --- a/addons/lunch/models/__init__.py +++ b/addons/lunch/models/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -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 deleted file mode 100644 index 325a64f4385..00000000000 --- a/addons/lunch/models/lunch_alert.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import datetime - -from odoo import api, fields, models, _ - - -class LunchAlert(models.Model): - """ Alerts to display during a lunch order. An alert can be specific to a - given day, weekly or daily. The alert is displayed from start to end hour. """ - _name = 'lunch.alert' - _description = 'Lunch Alert' - _rec_name = 'message' - - display = fields.Boolean(compute='_compute_display_get') - message = fields.Text('Message', required=True) - alert_type = fields.Selection([('specific', 'Specific Day'), - ('week', 'Every Week'), - ('days', 'Every Day')], - string='Recurrence', required=True, index=True, default='specific') - partner_id = fields.Many2one('res.partner', string="Vendor", - help="If specified, the selected vendor can be ordered only on selected days") - specific_day = fields.Date('Day', default=fields.Date.context_today) - monday = fields.Boolean('Monday') - tuesday = fields.Boolean('Tuesday') - wednesday = fields.Boolean('Wednesday') - thursday = fields.Boolean('Thursday') - friday = fields.Boolean('Friday') - saturday = fields.Boolean('Saturday') - sunday = fields.Boolean('Sunday') - start_hour = fields.Float('Between', oldname='active_from', required=True, default=7) - end_hour = fields.Float('And', oldname='active_to', required=True, default=23) - active = fields.Boolean(default=True) - - @api.multi - def name_get(self): - return [(alert.id, '%s %s' % (_('Alert'), '#%d' % alert.id)) for alert in self] - - @api.depends('alert_type', 'specific_day', 'monday', 'tuesday', 'thursday', - 'friday', 'saturday', 'sunday', 'start_hour', 'end_hour') - def _compute_display_get(self): - """ - This method check if the alert can be displayed today - if alert type is specific : compare specific_day(date) with today's date - if alert type is week : check today is set as alert (checkbox true) eg. self['monday'] - if alert type is day : True - return : Message if can_display_alert is True else False - """ - days_codes = {'0': 'sunday', - '1': 'monday', - '2': 'tuesday', - '3': 'wednesday', - '4': 'thursday', - '5': 'friday', - '6': 'saturday'} - fullday = False - now = datetime.datetime.now() - if self.env.context.get('lunch_date'): - # lunch_date is a fields.Date -> 00:00:00 - lunch_date = fields.Datetime.from_string(self.env.context['lunch_date']) - # if lunch_date is in the future, planned lunch, ignore hours - fullday = lunch_date > now - now = max(lunch_date, now) - mynow = fields.Datetime.context_timestamp(self, now) - - for alert in self: - can_display_alert = { - 'specific': (str(alert.specific_day) == fields.Date.to_string(mynow)), - 'week': alert[days_codes[mynow.strftime('%w')]], - 'days': True - } - - if can_display_alert[alert.alert_type]: - hour_to = int(alert.end_hour) - min_to = int((alert.end_hour - hour_to) * 60) - to_alert = datetime.time(hour_to, min_to) - hour_from = int(alert.start_hour) - min_from = int((alert.start_hour - hour_from) * 60) - from_alert = datetime.time(hour_from, min_from) - - if fullday or (from_alert <= mynow.time() <= to_alert): - alert.display = True - else: - alert.display = False diff --git a/addons/lunch/models/lunch_cashmove.py b/addons/lunch/models/lunch_cashmove.py index 7d4a6b56ae4..2cb71a77c69 100644 --- a/addons/lunch/models/lunch_cashmove.py +++ b/addons/lunch/models/lunch_cashmove.py @@ -2,6 +2,7 @@ # 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): @@ -14,10 +15,14 @@ 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_id = fields.Many2one('lunch.order.line', 'Order', ondelete='cascade') + order_line_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 f4354913db8..623a9cada50 100644 --- a/addons/lunch/models/lunch_order.py +++ b/addons/lunch/models/lunch_order.py @@ -1,19 +1,15 @@ # -*- 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 AccessError, ValidationError +from odoo.exceptions import UserError, 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. When creating a lunch order, applicable lunch alerts are displayed. + date. """ _name = 'lunch.order' _description = 'Lunch Order' @@ -39,24 +35,29 @@ 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, - compute='_compute_order_state', store=True) - alerts = fields.Text(compute='_compute_alerts_get', string="Alerts") + 'Status', readonly=True, index=True, copy=False, default='new') company_id = fields.Many2one('res.company', related='user_id.company_id', store=True, readonly=False) currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True, store=True) cash_move_balance = fields.Monetary(compute='_compute_cash_move_balance', multi='cash_move_balance') balance_visible = fields.Boolean(compute='_compute_cash_move_balance', multi='cash_move_balance') - previous_order_ids = fields.Many2many('lunch.order.line', compute='_compute_previous_order') - previous_order_widget = fields.Text(compute='_compute_previous_order') - @api.one - @api.depends('order_line_ids') + 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') 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) @@ -64,96 +65,55 @@ 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 len(lunch_cash): + if 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.')) - @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 + 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, + }) - if isConfirmed: - self.state = 'confirmed' + 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'}) else: - self.state = 'new' - return + 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'}) class LunchOrderLine(models.Model): @@ -163,153 +123,58 @@ class LunchOrderLine(models.Model): name = fields.Char(related='product_id.name', string="Product Name", readonly=True) order_id = fields.Many2one('lunch.order', 'Order', ondelete='cascade', required=True) - product_id = fields.Many2one('lunch.product', 'Product', required=True, - domain=[('available', '=', True)]) + topping_ids = fields.Many2many('lunch.product', string='Toppings') + product_id = fields.Many2one('lunch.product', string="Product", domain=[('is_topping', '=', False)], required=True) category_id = fields.Many2one('lunch.product.category', string='Product Category', related='product_id.category_id', readonly=True, store=True) date = fields.Date(string='Date', related='order_id.date', readonly=True, store=True) - supplier = fields.Many2one('res.partner', string='Vendor', related='product_id.supplier', - readonly=True, store=True) + supplier_id = fields.Many2one('lunch.supplier', string='Vendor', related='product_id.supplier_id', + readonly=True, store=True, index=True) user_id = fields.Many2one('res.users', string='User', related='order_id.user_id', readonly=True, store=True) note = fields.Text('Note') - price = fields.Float(related='product_id.price', readonly=True, store=True, + price = fields.Float('Total Price', compute='_compute_total_price', readonly=True, store=True, digits=dp.get_precision('Account')) state = fields.Selection([('new', 'New'), - ('confirmed', 'Received'), ('ordered', 'Ordered'), + ('confirmed', 'Received'), ('cancelled', 'Cancelled')], 'Status', readonly=True, index=True, default='new') - cashmove = fields.One2many('lunch.cashmove', 'order_id', 'Cash Move') + cashmove = fields.One2many('lunch.cashmove', 'order_line_id', 'Cash Move') currency_id = fields.Many2one('res.currency', related='order_id.currency_id', readonly=False) + quantity = fields.Float('Quantity', required=True, default=1) - def _check_supplier_availibility(self): - products = self.mapped('product_id') - if not all(product.available for product in products): - supplier_name = ", ".join(product.supplier.display_name for product in products if not product.available) - raise ValidationError(_("Vendor(s) '%s' is not available today") % supplier_name) + @api.depends('topping_ids', 'product_id', 'quantity') + def _compute_total_price(self): + for line in self: + line.price = line.quantity * sum(product.price for product in line.product_id | line.topping_ids) - @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 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 write(self, vals): - """ Override as an onchange would not apply if using the history buttons """ - res = super(LunchOrderLine, self).write(vals) - if vals.get('product_id'): - for line in self: - line.with_context(lunch_date=line.order_id.date)._check_supplier_availibility() - return res + def action_confirm(self): + self.write({'state': 'confirmed'}) - 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'}) + 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}) - 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 + def action_cancel(self): + for line in self: + line.cashmove.sudo().unlink() - 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.")) + self.write({'state': 'cancelled'}) diff --git a/addons/lunch/models/lunch_product.py b/addons/lunch/models/lunch_product.py index 8ded108fc81..1751ea7a6e1 100644 --- a/addons/lunch/models/lunch_product.py +++ b/addons/lunch/models/lunch_product.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models, _ +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models, tools, _ from odoo.addons import decimal_precision as dp -from odoo.osv import expression class LunchProductCategory(models.Model): @@ -20,44 +21,52 @@ class LunchProduct(models.Model): _name = 'lunch.product' _description = 'Lunch Product' - name = fields.Char('Product', required=True) - category_id = fields.Many2one('lunch.product.category', 'Product Category', required=True) + name = fields.Char('Name', required=True) + category_id = fields.Many2one('lunch.product.category', 'Product Category') description = fields.Text('Description') - price = fields.Float('Price', digits=dp.get_precision('Account')) - supplier = fields.Many2one('res.partner', 'Vendor') + price = fields.Float('Price', digits=dp.get_precision('Account'), required=True) + supplier_id = fields.Many2one('lunch.supplier', 'Vendor', required=True) active = fields.Boolean(default=True) - available = fields.Boolean(compute='_get_available_product', search='_search_available_products') + is_topping = fields.Boolean("This product is an extra garniture") - @api.depends('supplier') - def _get_available_product(self): + # 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) for product in self: - if not product.supplier: - product.available = True - else: - alerts = self.env['lunch.alert'].search([ - ('partner_id', '=', self.supplier.id) - ]) - if alerts and not any(alert.display for alert in alerts): - # every alert is not available - product.available = False - else: - # no alert for the supplier or at least one is not available - product.available = True - - def _search_available_products(self, operator, value): - alerts = self.env['lunch.alert'].search([]) - supplier_w_alerts = alerts.mapped('partner_id') - available_suppliers = alerts.filtered(lambda a: a.display).mapped('partner_id') - available_products = self.search([ - '|', - ('supplier', 'not in', supplier_w_alerts.ids), - ('supplier', 'in', available_suppliers.ids) - ]) - - if (operator in expression.NEGATIVE_TERM_OPERATORS and value) or \ - (operator not in expression.NEGATIVE_TERM_OPERATORS and not value): - # e.g. (available = False) or (available != True) - return [('id', 'not in', available_products.ids)] - else: - # e.g. (available = True) or (available != False) - return [('id', 'in', available_products.ids)] + product.already_ordered = bool(self.env['lunch.order.line'].search_count( + [('product_id', '=', product.id), ('date', '>=', last_ordered), ('user_id', '=', self.env.user.id)])) diff --git a/addons/lunch/models/lunch_supplier.py b/addons/lunch/models/lunch_supplier.py new file mode 100644 index 00000000000..07286aa1ff0 --- /dev/null +++ b/addons/lunch/models/lunch_supplier.py @@ -0,0 +1,180 @@ +# -*- 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') + + 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='once') + recurrency_from = fields.Float('From') + recurrency_to = fields.Float('To') + recurrency_date = fields.Date('Day', default=fields.Date.today()) + recurrency_date_from = fields.Datetime('from', compute='_compute_recurrency_date_from', store=True) + recurrency_date_to = fields.Datetime('to', compute='_compute_recurrency_date_to', store=True) + recurrency_monday = fields.Boolean('Monday') + recurrency_tuesday = fields.Boolean('Tuesday') + recurrency_wednesday = fields.Boolean('Wednesday') + recurrency_thursday = fields.Boolean('Thursday') + recurrency_friday = fields.Boolean('Friday') + recurrency_saturday = fields.Boolean('Saturday') + recurrency_sunday = fields.Boolean('Sunday') + + available_today = fields.Boolean('This is True when if the supplier is available today', + compute='_compute_available_today', search='_search_available_today') + + 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.reccurrency_date_from <= now <= supplier.reccurrency_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 244fbf11f18..1efb5be6c6d 100644 --- a/addons/lunch/security/ir.model.access.csv +++ b/addons/lunch/security/ir.model.access.csv @@ -4,10 +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 -alert_user,"Alert user",model_lunch_alert,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 diff --git a/addons/lunch/static/img/4formaggio.png b/addons/lunch/static/img/4formaggio.png new file mode 100644 index 00000000000..f19c95ac934 Binary files /dev/null and b/addons/lunch/static/img/4formaggio.png differ diff --git a/addons/lunch/static/img/Coke.png b/addons/lunch/static/img/Coke.png new file mode 100644 index 00000000000..e822a2656ac Binary files /dev/null and b/addons/lunch/static/img/Coke.png differ diff --git a/addons/lunch/static/img/bacon_burger.png b/addons/lunch/static/img/bacon_burger.png new file mode 100644 index 00000000000..ad4177d8ac2 Binary files /dev/null and b/addons/lunch/static/img/bacon_burger.png differ diff --git a/addons/lunch/static/img/brie.png b/addons/lunch/static/img/brie.png new file mode 100644 index 00000000000..5b76bfbb1ee Binary files /dev/null and b/addons/lunch/static/img/brie.png differ diff --git a/addons/lunch/static/img/cheeseburger.png b/addons/lunch/static/img/cheeseburger.png new file mode 100644 index 00000000000..f9d5a3e5c01 Binary files /dev/null and b/addons/lunch/static/img/cheeseburger.png differ diff --git a/addons/lunch/static/img/chicken_curry.png b/addons/lunch/static/img/chicken_curry.png new file mode 100644 index 00000000000..09e494482d6 Binary files /dev/null and b/addons/lunch/static/img/chicken_curry.png differ diff --git a/addons/lunch/static/img/chirashi.png b/addons/lunch/static/img/chirashi.png new file mode 100644 index 00000000000..65143711848 Binary files /dev/null and b/addons/lunch/static/img/chirashi.png differ diff --git a/addons/lunch/static/img/club.png b/addons/lunch/static/img/club.png new file mode 100644 index 00000000000..5f3e36f3f37 Binary files /dev/null and b/addons/lunch/static/img/club.png differ diff --git a/addons/lunch/static/img/coke_zero.png b/addons/lunch/static/img/coke_zero.png new file mode 100644 index 00000000000..72dbba5542b Binary files /dev/null and b/addons/lunch/static/img/coke_zero.png differ diff --git a/addons/lunch/static/img/fanta.png b/addons/lunch/static/img/fanta.png new file mode 100644 index 00000000000..b7a48332e0d Binary files /dev/null and b/addons/lunch/static/img/fanta.png differ diff --git a/addons/lunch/static/img/fuze_black.png b/addons/lunch/static/img/fuze_black.png new file mode 100644 index 00000000000..1daac338d90 Binary files /dev/null and b/addons/lunch/static/img/fuze_black.png differ diff --git a/addons/lunch/static/img/fuze_green.png b/addons/lunch/static/img/fuze_green.png new file mode 100644 index 00000000000..9a274ab6796 Binary files /dev/null and b/addons/lunch/static/img/fuze_green.png differ diff --git a/addons/lunch/static/img/gouda.png b/addons/lunch/static/img/gouda.png new file mode 100644 index 00000000000..c50d6d8dfbf Binary files /dev/null and b/addons/lunch/static/img/gouda.png differ diff --git a/addons/lunch/static/img/italiana.png b/addons/lunch/static/img/italiana.png new file mode 100644 index 00000000000..2f8c4244436 Binary files /dev/null and b/addons/lunch/static/img/italiana.png differ diff --git a/addons/lunch/static/img/lipton.png b/addons/lunch/static/img/lipton.png new file mode 100644 index 00000000000..a1f8ef21f2e Binary files /dev/null and b/addons/lunch/static/img/lipton.png differ diff --git a/addons/lunch/static/img/maki.png b/addons/lunch/static/img/maki.png new file mode 100644 index 00000000000..c92dd8c83a4 Binary files /dev/null and b/addons/lunch/static/img/maki.png differ diff --git a/addons/lunch/static/img/mozza.png b/addons/lunch/static/img/mozza.png new file mode 100644 index 00000000000..8820fbb9cd5 Binary files /dev/null and b/addons/lunch/static/img/mozza.png differ diff --git a/addons/lunch/static/img/napoli.png b/addons/lunch/static/img/napoli.png new file mode 100644 index 00000000000..c7ecd65db8a Binary files /dev/null and b/addons/lunch/static/img/napoli.png differ diff --git a/addons/lunch/static/img/pasta_bolognese.png b/addons/lunch/static/img/pasta_bolognese.png new file mode 100644 index 00000000000..64f9972e5c2 Binary files /dev/null and b/addons/lunch/static/img/pasta_bolognese.png differ diff --git a/addons/lunch/static/img/pizza_funghi.png b/addons/lunch/static/img/pizza_funghi.png new file mode 100644 index 00000000000..00bceba96b6 Binary files /dev/null and b/addons/lunch/static/img/pizza_funghi.png differ diff --git a/addons/lunch/static/img/pizza_margherita.png b/addons/lunch/static/img/pizza_margherita.png new file mode 100644 index 00000000000..0e317265524 Binary files /dev/null and b/addons/lunch/static/img/pizza_margherita.png differ diff --git a/addons/lunch/static/img/pizza_veggie.png b/addons/lunch/static/img/pizza_veggie.png new file mode 100644 index 00000000000..950afb54a12 Binary files /dev/null and b/addons/lunch/static/img/pizza_veggie.png differ diff --git a/addons/lunch/static/img/salmon_sushi.png b/addons/lunch/static/img/salmon_sushi.png new file mode 100644 index 00000000000..1467b54f120 Binary files /dev/null and b/addons/lunch/static/img/salmon_sushi.png differ diff --git a/addons/lunch/static/img/temaki.png b/addons/lunch/static/img/temaki.png new file mode 100644 index 00000000000..8f47e7c035a Binary files /dev/null and b/addons/lunch/static/img/temaki.png differ diff --git a/addons/lunch/static/img/tuna_sandwich.png b/addons/lunch/static/img/tuna_sandwich.png new file mode 100644 index 00000000000..b616d6156a5 Binary files /dev/null and b/addons/lunch/static/img/tuna_sandwich.png differ diff --git a/addons/lunch/static/src/js/lunch.js b/addons/lunch/static/src/js/lunch.js deleted file mode 100644 index 8c455083b0d..00000000000 --- a/addons/lunch/static/src/js/lunch.js +++ /dev/null @@ -1,94 +0,0 @@ -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 new file mode 100644 index 00000000000..5c9a0228f46 --- /dev/null +++ b/addons/lunch/static/src/js/lunch_kanban_controller.js @@ -0,0 +1,190 @@ +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 new file mode 100644 index 00000000000..c28f5594b7a --- /dev/null +++ b/addons/lunch/static/src/js/lunch_kanban_model.js @@ -0,0 +1,19 @@ +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 new file mode 100644 index 00000000000..45a928b892d --- /dev/null +++ b/addons/lunch/static/src/js/lunch_kanban_record.js @@ -0,0 +1,22 @@ +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 new file mode 100644 index 00000000000..bffe639a655 --- /dev/null +++ b/addons/lunch/static/src/js/lunch_kanban_renderer.js @@ -0,0 +1,29 @@ +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 new file mode 100644 index 00000000000..296ebac08cd --- /dev/null +++ b/addons/lunch/static/src/js/lunch_kanban_view.js @@ -0,0 +1,42 @@ +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 new file mode 100644 index 00000000000..cfc81ab8c47 --- /dev/null +++ b/addons/lunch/static/src/js/lunch_kanban_widget.js @@ -0,0 +1,87 @@ +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 new file mode 100644 index 00000000000..00e49299189 --- /dev/null +++ b/addons/lunch/static/src/js/lunch_payment_dialog.js @@ -0,0 +1,24 @@ +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 deleted file mode 100644 index 9e36f20dbd8..00000000000 --- a/addons/lunch/static/src/scss/lunch.scss +++ /dev/null @@ -1,14 +0,0 @@ - -.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 new file mode 100644 index 00000000000..44bac01fa4b --- /dev/null +++ b/addons/lunch/static/src/scss/lunch_kanban.scss @@ -0,0 +1,48 @@ +.o_lunch_kanban { + .o_pink { + color: nth($o-colors, 10); + } + + .o_lunch_widget { + display: flex; + overflow: auto; + border-bottom: 1px solid #CED4DA; + background-color: white; + min-height: 90px; + max-height: 90px; + + .o_lunch_widget_info { + padding: 5px 12px; + margin: 4px 8px; + width: 300px; + flex: 1 1 auto; + overflow-y: auto; + + .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 new file mode 100644 index 00000000000..74a57f7cfec --- /dev/null +++ b/addons/lunch/static/src/xml/lunch_kanban.xml @@ -0,0 +1,118 @@ + + +
+
+
+ +
+
+
+ + + + + + +
+
+ + + + + + + + + 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 deleted file mode 100644 index 3ae1e3491cb..00000000000 --- a/addons/lunch/static/tests/lunch_tests.js +++ /dev/null @@ -1,145 +0,0 @@ -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 new file mode 100644 index 00000000000..b2062609974 --- /dev/null +++ b/addons/lunch/static/tests/test_lunch_kanban.js @@ -0,0 +1,207 @@ +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} + ] + }); + } 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} + ] + }); + } 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} + ] + }); + } 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 f9d697d7b2c..08959c769be 100644 --- a/addons/lunch/tests/__init__.py +++ b/addons/lunch/tests/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- -from . import test_lunch +from . import common +from . import test_supplier diff --git a/addons/lunch/tests/common.py b/addons/lunch/tests/common.py new file mode 100644 index 00000000000..f38b9c547ec --- /dev/null +++ b/addons/lunch/tests/common.py @@ -0,0 +1,75 @@ +# -*- 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.topping_olives = self.env['lunch.product'].create({ + 'name': 'Olives', + 'price': 0.3, + 'supplier_id': self.supplier_pizza_inn.id, + 'is_topping': True, + }) diff --git a/addons/lunch/tests/test_lunch.py b/addons/lunch/tests/test_lunch.py deleted file mode 100644 index c6325a3f19c..00000000000 --- a/addons/lunch/tests/test_lunch.py +++ /dev/null @@ -1,143 +0,0 @@ -# -*- 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 new file mode 100644 index 00000000000..55c57c8e744 --- /dev/null +++ b/addons/lunch/tests/test_supplier.py @@ -0,0 +1,158 @@ +# -*- 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): + 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')] + + 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) + + 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 deleted file mode 100644 index bb6d807a9ff..00000000000 --- a/addons/lunch/views/lunch_alert_views.xml +++ /dev/null @@ -1,134 +0,0 @@ - - - - lunch.alert.search - lunch.alert - - - - - - - - - - lunch.alert.tree - lunch.alert - - - - - - - - - - - - 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: - - -
-
- - - -
-
-
-
-
-
-
-
- - - Alerts - lunch.alert - tree,kanban,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 8cd4658a8cb..b8665398153 100644 --- a/addons/lunch/views/lunch_orders_views.xml +++ b/addons/lunch/views/lunch_orders_views.xml @@ -19,7 +19,6 @@
-
@@ -43,24 +42,15 @@ -
- -
-
- -
- - + @@ -95,7 +85,7 @@ - + @@ -108,15 +98,14 @@ - + -
+ +