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 5268ecca6a7..e28b88a82f2 100644
--- a/addons/lunch/__manifest__.py
+++ b/addons/lunch/__manifest__.py
@@ -27,17 +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 314cf5ad292..00000000000
--- a/addons/lunch/controllers/main.py
+++ /dev/null
@@ -1,67 +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']),
- }
-
- 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 f91a57a183d..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,228 +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
-
-
-
-
- Olives
- 0.30
-
-
4 Formaggi
5.50
-
+
Tomato sauce, Olive oil, Fresh Tomatoes, Onions, Vegetables, Parmesan
-
@@ -306,7 +244,6 @@
new
-
@@ -314,7 +251,6 @@
confirmed
-
@@ -322,7 +258,6 @@
confirmed
-
@@ -338,9 +273,9 @@
7.70
new
-
+
+ +Emmental
- 1
@@ -349,9 +284,9 @@
7.40
confirmed
-
+
+ +Mushrooms
- 1
@@ -360,9 +295,9 @@
2.50
cancelled
-
+
+ +Salad +Tomatoes +Cucumbers
- 1
@@ -371,9 +306,9 @@
2.60
confirmed
-
+
+ +Salad +Tomatoes +Eggs
- 1
@@ -382,7 +317,7 @@
5.50
confirmed
-
+
@@ -391,7 +326,7 @@
Pizza Italiana
-7.40
-
+
order
@@ -400,7 +335,7 @@
Chicken curry
-2.60
-
+
order
@@ -425,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 58a34e7891b..f99750c87d0 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
new file mode 100644
index 00000000000..325a64f4385
--- /dev/null
+++ b/addons/lunch/models/lunch_alert.py
@@ -0,0 +1,85 @@
+# -*- 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 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 623a9cada50..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.product', string='Toppings')
- product_id = fields.Many2one('lunch.product', string="Product", domain=[('is_topping', '=', False)], 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 * sum(product.price for product in line.product_id | line.topping_ids)
+ 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 1751ea7a6e1..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):
@@ -21,52 +20,44 @@ class LunchProduct(models.Model):
_name = 'lunch.product'
_description = 'Lunch Product'
- name = fields.Char('Name', required=True)
- category_id = fields.Many2one('lunch.product.category', 'Product Category')
+ 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)
- is_topping = fields.Boolean("This product is an extra garniture")
+ 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 07286aa1ff0..00000000000
--- a/addons/lunch/models/lunch_supplier.py
+++ /dev/null
@@ -1,180 +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')
-
- 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 1efb5be6c6d..244fbf11f18 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
-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
+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 44bac01fa4b..00000000000
--- a/addons/lunch/static/src/scss/lunch_kanban.scss
+++ /dev/null
@@ -1,48 +0,0 @@
-.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
deleted file mode 100644
index 74a57f7cfec..00000000000
--- a/addons/lunch/static/src/xml/lunch_kanban.xml
+++ /dev/null
@@ -1,118 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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 b2062609974..00000000000
--- a/addons/lunch/static/tests/test_lunch_kanban.js
+++ /dev/null
@@ -1,207 +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}
- ]
- });
- } 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 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 f38b9c547ec..00000000000
--- a/addons/lunch/tests/common.py
+++ /dev/null
@@ -1,75 +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.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
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 55c57c8e744..00000000000
--- a/addons/lunch/tests/test_supplier.py
+++ /dev/null
@@ -1,158 +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):
- 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
new file mode 100644
index 00000000000..bb6d807a9ff
--- /dev/null
+++ b/addons/lunch/views/lunch_alert_views.xml
@@ -0,0 +1,134 @@
+
+
+
+ lunch.alert.search
+ lunch.alert
+
+
+
+
+
+
+
+
+
+ lunch.alert.tree
+ lunch.alert
+
+
+
+
+
+
+
+
+
+
+
+ lunch.alert.form
+ lunch.alert
+
+
+
+
+
+
+ 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 b8665398153..8cd4658a8cb 100644
--- a/addons/lunch/views/lunch_orders_views.xml
+++ b/addons/lunch/views/lunch_orders_views.xml
@@ -19,6 +19,7 @@