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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
-
-
-
-
-
-
- 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 @@