summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRomainLibert <rli@odoo.com>2018-10-25 14:43:19 +0000
committerYannick Tivisse <yti@odoo.com>2019-01-18 15:33:04 +0000
commit855c6dac25fccaf9312543022001ed0b360d6e13 (patch)
tree97e6943a15acd84e277ac910799e98cafb30cf22
parent84029de47cab518834674bca71898d3798e30010 (diff)
[IMP] lunch: automatize lunch orders
We would like to automatize lunch as currently all orders are manually ordered and validated, and a cash move has to be manually created each time you want to credit the wallet of some user. 1/ In this commit we add a new widget that comes with the kanban view and will allow to take orders in a friendly manner. 2/ We also add a lunch.supplier model this also allows you to configure an automatic ordering via email. 3/ Introduces new lunch.location model to replace res.partner on suppliers 4/ New mail template for automatic ordering 5/ Add the concept of new products (mark a product as new until some date) 6/ The toppings are now defined by product category 7/ Lunch.order removed, lunch.order.line renamed into lunch.order 8/ New SQL report to get the wallet content (aggregate of lunch.cashmove and lunch.order) 9/ New setting to allow negative wallet (aka overdraft) 10/ Application now remembers what notes and toppings you last ordered with your product 11/ You can change your location using the widget on the kanban view 12/ Edition of the order can be done until the order is arrived TaskID: 1856876 TaskID: 1916105 closes odoo/odoo#30028
-rw-r--r--addons/lunch/__init__.py4
-rw-r--r--addons/lunch/__manifest__.py11
-rw-r--r--addons/lunch/controllers/__init__.py4
-rw-r--r--addons/lunch/controllers/main.py119
-rw-r--r--addons/lunch/data/ir_cron_data.xml17
-rw-r--r--addons/lunch/data/lunch_data.xml84
-rw-r--r--addons/lunch/data/lunch_demo.xml250
-rw-r--r--addons/lunch/models/__init__.py6
-rw-r--r--addons/lunch/models/lunch_alert.py108
-rw-r--r--addons/lunch/models/lunch_cashmove.py18
-rw-r--r--addons/lunch/models/lunch_location.py12
-rw-r--r--addons/lunch/models/lunch_order.py352
-rw-r--r--addons/lunch/models/lunch_product.py138
-rw-r--r--addons/lunch/models/lunch_supplier.py180
-rw-r--r--addons/lunch/models/res_company.py10
-rw-r--r--addons/lunch/models/res_config_settings.py11
-rw-r--r--addons/lunch/models/res_users.py10
-rw-r--r--addons/lunch/report/__init__.py5
-rw-r--r--addons/lunch/report/lunch_cashmove_report.py51
-rw-r--r--addons/lunch/report/lunch_cashmove_report_views.xml153
-rw-r--r--addons/lunch/report/lunch_order_templates.xml79
-rw-r--r--addons/lunch/report/lunch_order_views.xml11
-rw-r--r--addons/lunch/report/lunch_product_report.py91
-rw-r--r--addons/lunch/security/ir.model.access.csv25
-rw-r--r--addons/lunch/security/lunch_security.xml6
-rw-r--r--addons/lunch/static/img/4formaggio.pngbin0 -> 8362 bytes
-rw-r--r--addons/lunch/static/img/Coke.pngbin0 -> 7469 bytes
-rw-r--r--addons/lunch/static/img/bacon_burger.pngbin0 -> 8027 bytes
-rw-r--r--addons/lunch/static/img/brie.pngbin0 -> 6794 bytes
-rw-r--r--addons/lunch/static/img/cheeseburger.pngbin0 -> 7020 bytes
-rw-r--r--addons/lunch/static/img/chicken_curry.pngbin0 -> 7147 bytes
-rw-r--r--addons/lunch/static/img/chirashi.pngbin0 -> 7221 bytes
-rw-r--r--addons/lunch/static/img/club.pngbin0 -> 7011 bytes
-rw-r--r--addons/lunch/static/img/coke_zero.pngbin0 -> 7153 bytes
-rw-r--r--addons/lunch/static/img/fanta.pngbin0 -> 5855 bytes
-rw-r--r--addons/lunch/static/img/fuze_black.pngbin0 -> 5623 bytes
-rw-r--r--addons/lunch/static/img/fuze_green.pngbin0 -> 4724 bytes
-rw-r--r--addons/lunch/static/img/gouda.pngbin0 -> 7864 bytes
-rw-r--r--addons/lunch/static/img/italiana.pngbin0 -> 11549 bytes
-rw-r--r--addons/lunch/static/img/lipton.pngbin0 -> 6784 bytes
-rw-r--r--addons/lunch/static/img/maki.pngbin0 -> 8806 bytes
-rw-r--r--addons/lunch/static/img/mozza.pngbin0 -> 6191 bytes
-rw-r--r--addons/lunch/static/img/napoli.pngbin0 -> 9099 bytes
-rw-r--r--addons/lunch/static/img/pasta_bolognese.pngbin0 -> 6727 bytes
-rw-r--r--addons/lunch/static/img/pizza_funghi.pngbin0 -> 9213 bytes
-rw-r--r--addons/lunch/static/img/pizza_margherita.pngbin0 -> 8597 bytes
-rw-r--r--addons/lunch/static/img/pizza_veggie.pngbin0 -> 7354 bytes
-rw-r--r--addons/lunch/static/img/salmon_sushi.pngbin0 -> 6885 bytes
-rw-r--r--addons/lunch/static/img/temaki.pngbin0 -> 9244 bytes
-rw-r--r--addons/lunch/static/img/tuna_sandwich.pngbin0 -> 6425 bytes
-rw-r--r--addons/lunch/static/src/js/lunch.js94
-rw-r--r--addons/lunch/static/src/js/lunch_kanban_controller.js213
-rw-r--r--addons/lunch/static/src/js/lunch_kanban_model.js82
-rw-r--r--addons/lunch/static/src/js/lunch_kanban_record.js22
-rw-r--r--addons/lunch/static/src/js/lunch_kanban_renderer.js29
-rw-r--r--addons/lunch/static/src/js/lunch_kanban_view.js41
-rw-r--r--addons/lunch/static/src/js/lunch_kanban_widget.js169
-rw-r--r--addons/lunch/static/src/js/lunch_payment_dialog.js24
-rw-r--r--addons/lunch/static/src/scss/lunch.scss14
-rw-r--r--addons/lunch/static/src/scss/lunch_kanban.scss63
-rw-r--r--addons/lunch/static/src/xml/lunch_kanban.xml112
-rw-r--r--addons/lunch/static/tests/lunch_tests.js145
-rw-r--r--addons/lunch/static/tests/test_lunch_kanban.js223
-rw-r--r--addons/lunch/tests/__init__.py4
-rw-r--r--addons/lunch/tests/common.py75
-rw-r--r--addons/lunch/tests/test_lunch.py143
-rw-r--r--addons/lunch/tests/test_supplier.py123
-rw-r--r--addons/lunch/views/lunch_alert_views.xml107
-rw-r--r--addons/lunch/views/lunch_cashmove_views.xml73
-rw-r--r--addons/lunch/views/lunch_location_views.xml54
-rw-r--r--addons/lunch/views/lunch_orders_views.xml175
-rw-r--r--addons/lunch/views/lunch_product_views.xml211
-rw-r--r--addons/lunch/views/lunch_supplier_views.xml112
-rw-r--r--addons/lunch/views/lunch_templates.xml17
-rw-r--r--addons/lunch/views/lunch_views.xml23
-rw-r--r--addons/lunch/views/res_config_settings.xml43
-rw-r--r--addons/lunch/wizard/__init__.py2
-rw-r--r--addons/lunch/wizard/lucky_order.py44
-rw-r--r--addons/lunch/wizard/lucky_order_view.xml35
-rw-r--r--addons/lunch/wizard/lunch_line_wizard.py135
-rw-r--r--addons/lunch/wizard/lunch_line_wizard_views.xml39
81 files changed, 2928 insertions, 1473 deletions
diff --git a/addons/lunch/__init__.py b/addons/lunch/__init__.py
index b6f07433380..4e9b019bd7a 100644
--- a/addons/lunch/__init__.py
+++ b/addons/lunch/__init__.py
@@ -1,5 +1,7 @@
# -*- 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 report
+from . import wizard
diff --git a/addons/lunch/__manifest__.py b/addons/lunch/__manifest__.py
index e28b88a82f2..9e5c647c04c 100644
--- a/addons/lunch/__manifest__.py
+++ b/addons/lunch/__manifest__.py
@@ -24,19 +24,22 @@ If you want to save your employees' time and avoid them to always have coins in
'data': [
'security/lunch_security.xml',
'security/ir.model.access.csv',
- 'report/lunch_order_views.xml',
- 'report/lunch_order_templates.xml',
+ 'report/lunch_cashmove_report_views.xml',
'views/lunch_templates.xml',
- 'wizard/lucky_order_view.xml',
+ 'wizard/lunch_line_wizard_views.xml',
'views/lunch_alert_views.xml',
'views/lunch_cashmove_views.xml',
+ 'views/lunch_location_views.xml',
'views/lunch_orders_views.xml',
'views/lunch_product_views.xml',
+ 'views/lunch_supplier_views.xml',
+ 'views/res_config_settings.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..5d4b25db9c0
--- /dev/null
+++ b/addons/lunch/controllers/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import main
diff --git a/addons/lunch/controllers/main.py b/addons/lunch/controllers/main.py
new file mode 100644
index 00000000000..ac645c2edc4
--- /dev/null
+++ b/addons/lunch/controllers/main.py
@@ -0,0 +1,119 @@
+# -*- 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
+from odoo.osv import expression
+from odoo.tools import float_round, float_repr
+
+
+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 = self._make_infos(user, order=False)
+
+ lines = self._get_current_lines(user.id)
+ if lines:
+ lines = [{'id': line.id,
+ 'product': (line.product_id.id, line.product_id.name, float_repr(float_round(line.product_id.price, 2), 2)),
+ 'toppings': [(topping.name, float_repr(float_round(topping.price, 2), 2))
+ for topping in line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3],
+ 'quantity': line.quantity,
+ 'price': line.price,
+ 'state': line.state, # Only used for _get_state
+ 'note': line.note} for line in lines]
+ raw_state, state = self._get_state(lines)
+ infos.update({
+ 'total': float_repr(float_round(sum(line['price'] for line in lines), 2), 2),
+ 'raw_state': raw_state,
+ 'state': 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
+
+ lines = self._get_current_lines(user.id)
+ lines.action_cancel()
+ lines.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
+
+ lines = self._get_current_lines(user.id)
+ if lines:
+ lines = lines.filtered(lambda line: line.state == 'new')
+
+ lines.action_order()
+ return True
+
+ 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', {})}
+
+ @http.route('/lunch/user_location_set', type='json', auth='user')
+ def set_user_location(self, location_id, user_id=None):
+ user = request.env['res.users'].browse(user_id) if user_id else request.env.user
+
+ user.last_lunch_location_id = request.env['lunch.location'].browse(location_id)
+ return True
+
+ @http.route('/lunch/user_location_get', type='json', auth='user')
+ def get_user_location(self, user_id=None):
+ user = request.env['res.users'].browse(user_id) if user_id else request.env.user
+
+ return user.last_lunch_location_id.id
+
+ def _make_infos(self, user, **kwargs):
+ res = dict(kwargs)
+
+ is_manager = request.env.user.has_group('lunch.group_lunch_manager')
+
+ currency = user.company_id.currency_id
+
+ res.update({
+ 'username': user.sudo().name,
+ 'userimage': '/web/image?model=res.users&id=%s&field=image_small' % user.id,
+ 'wallet': request.env['lunch.cashmove'].get_wallet_balance(user, False),
+ 'is_manager': is_manager,
+ 'locations': request.env['lunch.location'].search_read([], ['name']),
+ 'currency': {'symbol': currency.symbol, 'position': currency.position},
+ })
+
+ user_location = user.last_lunch_location_id if user.last_lunch_location_id else request.env['lunch.location'].search([], limit=1)
+
+ alert_domain = expression.AND([
+ [('available_today', '=', True)],
+ [('location_ids', 'in', user_location.id)],
+ ])
+
+ res.update({
+ 'user_location': (user_location.id, user_location.name),
+ 'alerts': request.env['lunch.alert'].search_read(alert_domain, ['message']),
+ })
+
+ return res
+
+ def _get_current_lines(self, user_id):
+ return request.env['lunch.order'].search([('user_id', '=', user_id), ('date', '=', fields.Date.today()), ('state', '!=', 'cancelled')])
+
+ def _get_state(self, lines):
+ """
+ This method returns the lowest state of the list of lines
+
+ eg: [confirmed, confirmed, new] will return ('new', 'To Order')
+ """
+ states_to_int = {'new': 0, 'ordered': 1, 'confirmed': 2, 'cancelled': 3}
+ int_to_states = ['new', 'ordered', 'confirmed', 'cancelled']
+ translated_states = dict(request.env['lunch.order']._fields['state']._description_selection(request.env))
+
+ state = int_to_states[min(states_to_int[line['state']] for line in lines)]
+
+ return (state, translated_states[state])
diff --git a/addons/lunch/data/ir_cron_data.xml b/addons/lunch/data/ir_cron_data.xml
new file mode 100644
index 00000000000..e7c34640bb5
--- /dev/null
+++ b/addons/lunch/data/ir_cron_data.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<odoo>
+ <data noupdate="1">
+ <record id="ir_cron_lunch" model="ir.cron">
+ <field name="name">Lunch: automatic email send</field>
+ <field name="model_id" ref="model_lunch_supplier"/>
+ <field name="state">code</field>
+ <field name="code">model._auto_email_send()</field>
+ <field name="active" eval="True"/>
+ <field name="user_id" ref="base.user_root"/>
+ <field name="interval_number">20</field>
+ <field name="interval_type">minutes</field>
+ <field name="numbercall">-1</field>
+ <field name="doall" eval="False"/>
+ </record>
+ </data>
+</odoo>
diff --git a/addons/lunch/data/lunch_data.xml b/addons/lunch/data/lunch_data.xml
index 2332d2e5272..05cb5d1f98d 100644
--- a/addons/lunch/data/lunch_data.xml
+++ b/addons/lunch/data/lunch_data.xml
@@ -1,38 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
- <record id="lunch_order_line_action_order" model="ir.actions.server">
- <field name="name">Lunch: Order meals</field>
- <field name="model_id" ref="model_lunch_order_line"/>
- <field name="binding_model_id" ref="model_lunch_order_line"/>
- <field name="state">code</field>
- <field name="code">
-if records:
- action = records.order()
- </field>
- </record>
-
- <record id="lunch_order_line_action_confirm" model="ir.actions.server">
+ <record id="lunch_order_action_confirm" model="ir.actions.server">
<field name="name">Lunch: Receive meals</field>
- <field name="model_id" ref="model_lunch_order_line"/>
- <field name="binding_model_id" ref="model_lunch_order_line"/>
+ <field name="model_id" ref="model_lunch_order"/>
+ <field name="binding_model_id" ref="model_lunch_order"/>
<field name="state">code</field>
- <field name="code">records.confirm()</field>
+ <field name="code">records.action_confirm()</field>
</record>
- <record id="lunch_order_line_action_cancel" model="ir.actions.server">
+ <record id="lunch_order_action_cancel" model="ir.actions.server">
<field name="name">Lunch: Cancel meals</field>
- <field name="model_id" ref="model_lunch_order_line"/>
- <field name="binding_model_id" ref="model_lunch_order_line"/>
+ <field name="model_id" ref="model_lunch_order"/>
+ <field name="binding_model_id" ref="model_lunch_order"/>
<field name="state">code</field>
- <field name="code">records.cancel()</field>
+ <field name="code">records.action_cancel()</field>
</record>
</data>
<data noupdate="0">
<record id="lunch_order_mail_supplier" model="mail.template">
<field name="name">Lunch: Send by email</field>
- <field name="model_id" ref="lunch.model_lunch_order"/>
- <field name="email_from">${ctx['order']['supplier_email'] |safe}</field>
+ <field name="model_id" ref="lunch.model_lunch_supplier"/>
+ <field name="email_from">${ctx['order']['email_from'] |safe}</field>
<field name="partner_to">${ctx['order']['supplier_id']}</field>
<field name="subject">Orders for ${ctx['order']['company_name']}</field>
<field name="body_html" type="html">
@@ -71,24 +60,43 @@ if records:
Here is, today orders for ${order['company_name']}:
</p>
- <ul>
- % for line in lines:
- <li>
- <strong>
- ${line['product']}
- (x${line['quantity']})
- </strong>
- % if line['note']:
- <em>(${line['note']})</em>
- % endif
- <div style="display: inline-block; float:right">
- ${format_amount(line['price'], currency)}
- </div>
- </li>
- % endfor
- </ul>
+ <table>
+ <thead>
+ <tr style="background-color:rgb(233,232,233);">
+ <th style="width: 100%; min-width: 96px; font-size: 13px;"><strong>Product</strong></th>
+ <th style="width: 100%; min-width: 96px; font-size: 13px;"><strong>Comments</strong></th>
+ <th style="width: 100%; min-width: 96px; font-size: 13px;"><strong>Person</strong></th>
+ <th style="width: 100%; min-width: 96px; font-size: 13px;" align="center"><strong>Qty</strong></th>
+ <th style="width: 100%; min-width: 96px; font-size: 13px;" align="center"><strong>Price</strong></th>
+ </tr>
+ </thead>
+ <tbody>
+ % for line in lines
+ <tr>
+ <td style="width: 100%; font-size: 13px;" valign="top">${line['product']}</td>
+ <td style="width: 100%; font-size: 13px;" valign="top">
+ % if line['toppings']:
+ ${line['toppings']}
+ % endif
+ % if line['note']:
+ <div style="color: rgb(173,181,189);">${line['note']}</div>
+ % endif
+ </td>
+ <td style="width: 100%; font-size: 13px;" valign="top">${line['username']}</td>
+ <td style="width: 100%; font-size: 13px;" valign="top" align="right">${line['quantity']}</td>
+ <td style="width: 100%; font-size: 13px;" valign="top" align="right">${format_amount(line['price'], currency)}</td>
+ </tr>
+ % endfor
+ <tr>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td style="width: 100%; font-size: 13px; border-top: 1px solid black;"><strong>Total</strong></td>
+ <td style="width: 100%; font-size: 13px; border-top: 1px solid black;" align="right"><strong>${format_amount(order['amount_total'], currency)}</strong></td>
+ </tr>
+ </tbody>
+ </table>
- <p>Amounting in <strong>${format_amount(order['amount_total'], currency)}</strong>.</p>
<p>Do not hesitate to contact us if you have any question.</p>
</div>
</td>
diff --git a/addons/lunch/data/lunch_demo.xml b/addons/lunch/data/lunch_demo.xml
index bd36db1f0af..4fc4ecf2fe3 100644
--- a/addons/lunch/data/lunch_demo.xml
+++ b/addons/lunch/data/lunch_demo.xml
@@ -2,7 +2,24 @@
<odoo>
<data noupdate="1">
+ <record id="location_office_1" model="lunch.location">
+ <field name="name">Office 1</field>
+ </record>
+
+ <record id="location_office_2" model="lunch.location">
+ <field name="name">Office 2</field>
+ </record>
+
+ <record id="location_office_3" model="lunch.location">
+ <field name="name">Office 3</field>
+ </record>
+
+ <record id="base.user_admin" model="res.users">
+ <field name="last_lunch_location_id" ref="location_office_2"/>
+ </record>
+
<record id="base.user_demo" model="res.users">
+ <field name="last_lunch_location_id" ref="location_office_3"/>
<field name="groups_id" eval="[(4, ref('lunch.group_lunch_user'))]"/>
</record>
@@ -28,7 +45,7 @@
<field name="name">Chirashi</field>
</record>
- <record model="res.partner" id="partner_coin_gourmand">
+ <record id="partner_coin_gourmand" model="res.partner">
<field name="name">Coin gourmand</field>
<field name="city">Tirana</field>
<field name="country_id" ref="base.al"/>
@@ -37,7 +54,7 @@
<field name="email">coin.gourmand@yourcompany.example.com</field>
</record>
- <record model="res.partner" id="partner_pizza_inn">
+ <record id="partner_pizza_inn" model="res.partner">
<field name="name">Pizza Inn</field>
<field name="city">New Delhi TN</field>
<field name="country_id" ref="base.us"/>
@@ -65,317 +82,304 @@
<field name="email">order@sushi.com</field>
</record>
+ <record model="lunch.supplier" id="supplier_coin_gourmand">
+ <field name="partner_id" ref="partner_coin_gourmand"/>
+ <field name="available_location_ids" eval="[
+ (6, 0, [ref('location_office_1'), ref('location_office_2')]),
+ ]"/>
+ </record>
+
+ <record model="lunch.supplier" id="supplier_pizza_inn">
+ <field name="partner_id" ref="partner_pizza_inn"/>
+ <field name="send_by">mail</field>
+ <field name="automatic_email_time">11</field>
+ <field name="available_location_ids" eval="[
+ (6, 0, [ref('location_office_1'), ref('location_office_2')]),
+ ]"/>
+ </record>
+
+ <record model="lunch.supplier" id="supplier_corner">
+ <field name="partner_id" ref="partner_corner"/>
+ <field name="available_location_ids" eval="[
+ (6, 0, [ref('location_office_3')]),
+ ]"/>
+ </record>
+
+ <record model="lunch.supplier" id="supplier_sushi_shop">
+ <field name="partner_id" ref="partner_sushi_shop"/>
+ <field name="available_location_ids" eval="[
+ (6, 0, [ref('location_office_1'), ref('location_office_2')]),
+ ]"/>
+ </record>
+
<record model="lunch.product" id="product_bacon">
<field name="name">Bacon</field>
<field name="category_id" eval="str(ref('categ_burger'))"/>
<field name="price">7.5</field>
- <field name="supplier" eval="str(ref('partner_corner'))"/>
+ <field name="supplier_id" ref="supplier_corner"/>
<field name="description">Beef, Bacon, Salad, Cheddar, Fried Onion, BBQ Sauce</field>
+ <field name="image" type="base64" file="lunch/static/img/bacon_burger.png"/>
+ <field name="new_until" eval="datetime.today() + relativedelta(weeks=1)"/>
</record>
<record model="lunch.product" id="product_cheeseburger">
<field name="name">Cheese Burger</field>
<field name="category_id" eval="str(ref('categ_burger'))"/>
<field name="price">7.0</field>
- <field name="supplier" eval="str(ref('partner_corner'))"/>
+ <field name="supplier_id" ref="supplier_corner"/>
<field name="description">Beef, Cheddar, Salad, Fried Onions, BBQ Sauce</field>
+ <field name="image" type="base64" file="lunch/static/img/cheeseburger.png"/>
</record>
<record model="lunch.product" id="product_chicken_curry">
<field name="name">Chicken Curry</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="price">3.0</field>
- <field name="supplier" eval="str(ref('partner_corner'))"/>
+ <field name="supplier_id" ref="supplier_corner"/>
+ <field name="image" type="base64" file="lunch/static/img/chicken_curry.png"/>
</record>
<record model="lunch.product" id="product_spicy_tuna">
<field name="name">Spicy Tuna</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="price">3.0</field>
- <field name="supplier" eval="str(ref('partner_corner'))"/>
+ <field name="supplier_id" ref="supplier_corner"/>
+ <field name="image" type="base64" file="lunch/static/img/chicken_curry.png"/>
</record>
<record model="lunch.product" id="product_mozzarella">
<field name="name">Mozzarella</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="price">3.9</field>
- <field name="supplier" eval="str(ref('partner_corner'))"/>
+ <field name="supplier_id" ref="supplier_corner"/>
<field name="description">Mozzarella, Pesto, Tomatoes</field>
+ <field name="image" type="base64" file="lunch/static/img/mozza.png"/>
</record>
<record model="lunch.product" id="product_club">
<field name="name">Club</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="price">3.4</field>
- <field name="supplier" eval="str(ref('partner_corner'))"/>
+ <field name="supplier_id" ref="supplier_corner"/>
<field name="description">Ham, Cheese, Vegetables</field>
+ <field name="image" type="base64" file="lunch/static/img/club.png"/>
</record>
<record model="lunch.product" id="product_maki">
<field name="name">Lunch Maki 18pc</field>
<field name="category_id" eval="str(ref('categ_sushi'))"/>
<field name="price">12.0</field>
- <field name="supplier" eval="str(ref('partner_sushi_shop'))"/>
+ <field name="supplier_id" ref="supplier_sushi_shop"/>
<field name="description">6 Maki Salmon - 6 Maki Tuna - 6 Maki Shrimp/Avocado</field>
+ <field name="image" type="base64" file="lunch/static/img/maki.png"/>
+ <field name="new_until" eval="datetime.today() + relativedelta(weeks=1)"/>
</record>
<record model="lunch.product" id="product_salmon">
<field name="name">Lunch Salmon 20pc</field>
<field name="category_id" eval="str(ref('categ_sushi'))"/>
<field name="price">13.80</field>
- <field name="supplier" eval="str(ref('partner_sushi_shop'))"/>
+ <field name="supplier_id" ref="supplier_sushi_shop"/>
<field name="description">4 Sushi Salmon - 6 Maki Salmon - 4 Sashimi Salmon </field>
+ <field name="image" type="base64" file="lunch/static/img/salmon_sushi.png"/>
+ <field name="new_until" eval="datetime.today() + relativedelta(weeks=1)"/>
</record>
<record model="lunch.product" id="product_temaki">
<field name="name">Lunch Temaki mix 3pc</field>
<field name="category_id" eval="str(ref('categ_temaki'))"/>
<field name="price">14.0</field>
- <field name="supplier" eval="str(ref('partner_sushi_shop'))"/>
- <field name="description">1 Avocado/Cooked Tuna - 1 Salmon/Eggs - 1 Salmon/Avocado - 1 Tuna/Mango</field>
+ <field name="supplier_id" ref="supplier_sushi_shop"/>
+ <field name="description">1 Avocado - 1 Salmon - 1 Eggs - 1 Tuna</field>
+ <field name="image" type="base64" file="lunch/static/img/temaki.png"/>
+ <field name="new_until" eval="datetime.today() + relativedelta(weeks=1)"/>
</record>
<record model="lunch.product" id="product_chirashi">
<field name="name">Salmon and Avocado</field>
<field name="category_id" eval="str(ref('categ_chirashi'))"/>
<field name="price">9.25</field>
- <field name="supplier" eval="str(ref('partner_sushi_shop'))"/>
- <field name="description">Shrimp Tempura, Yam Tempura, Chinese Cabbage, Pickled Onions, Sesame Sauce</field>
+ <field name="supplier_id" ref="supplier_sushi_shop"/>
+ <field name="description">2 Tempuras, Cabbages, Onions, Sesame Sauce</field>
+ <field name="image" type="base64" file="lunch/static/img/chirashi.png"/>
+ <field name="new_until" eval="datetime.today() + relativedelta(weeks=1)"/>
</record>
<record model="lunch.product" id="product_cheese_ham">
<field name="name">Cheese And Ham</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="price">3.30</field>
- <field name="supplier" eval="str(ref('partner_coin_gourmand'))"/>
+ <field name="supplier_id" eval="str(ref('supplier_coin_gourmand'))"/>
<field name="description">Cheese, Ham, Salad, Tomatoes, cucumbers, eggs</field>
+ <field name="image" type="base64" file="lunch/static/img/club.png"/>
</record>
<record model="lunch.product" id="product_country">
<field name="name">The Country</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="price">3.30</field>
- <field name="supplier" eval="str(ref('partner_coin_gourmand'))"/>
+ <field name="supplier_id" eval="str(ref('supplier_coin_gourmand'))"/>
<field name="description">Brie, Honey, Walnut Kernels</field>
+ <field name="image" type="base64" file="lunch/static/img/brie.png"/>
</record>
<record model="lunch.product" id="product_tuna">
<field name="name">Tuna</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="price">2.50</field>
- <field name="supplier" eval="str(ref('partner_coin_gourmand'))"/>
+ <field name="supplier_id" eval="str(ref('supplier_coin_gourmand'))"/>
<field name="description">Tuna, Mayonnaise</field>
+ <field name="image" type="base64" file="lunch/static/img/tuna_sandwich.png"/>
</record>
<record model="lunch.product" id="product_gouda">
<field name="name">Gouda Cheese</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="price">2.50</field>
- <field name="supplier" eval="str(ref('partner_coin_gourmand'))"/>
+ <field name="supplier_id" eval="str(ref('supplier_coin_gourmand'))"/>
<field name="description"></field>
+ <field name="image" type="base64" file="lunch/static/img/gouda.png"/>
</record>
<record model="lunch.product" id="product_chicken_curry">
<field name="name">Chicken Curry</field>
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
<field name="price">2.60</field>
- <field name="supplier" eval="str(ref('partner_coin_gourmand'))"/>
+ <field name="supplier_id" eval="str(ref('supplier_coin_gourmand'))"/>
<field name="description"></field>
+ <field name="image" type="base64" file="lunch/static/img/chicken_curry.png"/>
</record>
<record model="lunch.product" id="product_margherita">
<field name="name">Pizza Margherita</field>
<field name="category_id" eval="str(ref('categ_pizza'))"/>
<field name="price">6.90</field>
- <field name="supplier" eval="str(ref('partner_pizza_inn'))"/>
+ <field name="supplier_id" eval="str(ref('supplier_pizza_inn'))"/>
<field name="description">Tomatoes, Mozzarella</field>
+ <field name="image" type="base64" file="lunch/static/img/pizza_margherita.png"/>
</record>
<record model="lunch.product" id="product_funghi">
<field name="name">Pizza Funghi</field>
<field name="category_id" eval="str(ref('categ_pizza'))"/>
<field name="price">7.00</field>
- <field name="supplier" eval="str(ref('partner_pizza_inn'))"/>
+ <field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="description">Tomatoes, Mushrooms, Mozzarella</field>
+ <field name="image" type="base64" file="lunch/static/img/pizza_funghi.png"/>
</record>
<record model="lunch.product" id="product_vege">
<field name="name">Pizza Vegetarian</field>
<field name="category_id" eval="str(ref('categ_pizza'))"/>
<field name="price">7.00</field>
- <field name="supplier" eval="str(ref('partner_pizza_inn'))"/>
+ <field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="description">Tomatoes, Mozzarella, Mushrooms, Peppers, Olives</field>
+ <field name="image" type="base64" file="lunch/static/img/pizza_veggie.png"/>
</record>
<record model="lunch.product" id="product_italiana">
<field name="name">Pizza Italiana</field>
<field name="category_id" eval="str(ref('categ_pizza'))"/>
<field name="price">7.40</field>
- <field name="supplier" eval="str(ref('partner_pizza_inn'))"/>
+ <field name="supplier_id" eval="str(ref('supplier_pizza_inn'))"/>
<field name="description">Fresh Tomatoes, Basil, Mozzarella</field>
+ <field name="image" type="base64" file="lunch/static/img/italiana.png"/>
</record>
<record model="lunch.product" id="product_Bolognese">
<field name="name">Bolognese Pasta</field>
<field name="category_id" eval="str(ref('categ_pasta'))"/>
<field name="price">7.70</field>
- <field name="supplier" eval="str(ref('partner_pizza_inn'))"/>
+ <field name="supplier_id" eval="str(ref('supplier_pizza_inn'))"/>
<field name="description"></field>
+ <field name="image" type="base64" file="lunch/static/img/pasta_bolognese.png"/>
</record>
<record model="lunch.product" id="product_Napoli">
<field name="name">Napoli Pasta</field>
<field name="category_id" eval="str(ref('categ_pasta'))"/>
<field name="price">7.70</field>
- <field name="supplier" eval="str(ref('partner_pizza_inn'))"/>
+ <field name="supplier_id" eval="str(ref('supplier_pizza_inn'))"/>
<field name="description">Tomatoes, Basil</field>
+ <field name="image" type="base64" file="lunch/static/img/napoli.png"/>
+ </record>
+
+ <record model="lunch.topping" id="product_olives">
+ <field name="name">Olives</field>
+ <field name="price">0.30</field>
+ <field name="category_id" ref="categ_pizza"/>
</record>
<record model="lunch.product" id="product_4formaggi">
<field name="name">4 Formaggi</field>
<field name="category_id" eval="str(ref('categ_pasta'))"/>
<field name="price">5.50</field>
- <field name="supplier" eval="str(ref('partner_pizza_inn'))"/>
+ <field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="description">Tomato sauce, Olive oil, Fresh Tomatoes, Onions, Vegetables, Parmesan</field>
+ <field name="image" type="base64" file="lunch/static/img/4formaggio.png"/>
</record>
- <record model="lunch.order" id="order_1">
- <field name="user_id" ref="base.user_demo"/>
- <field name="date" eval="(DateTime.now() + timedelta(days=2)).strftime('%Y-%m-%d')"/>
- <field name="order_line_ids" eval="[]"/>
- <field name="state">new</field>
- </record>
-
- <record model="lunch.order" id="order_2">
- <field name="user_id" ref="base.user_demo"/>
- <field name="date" eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d')"/>
- <field name="order_line_ids" eval="[]"/>
- <field name="state">confirmed</field>
- </record>
-
- <record model="lunch.order" id="order_3">
+ <record model="lunch.cashmove" id="cashmove_1">
<field name="user_id" ref="base.user_demo"/>
<field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
- <field name="order_line_ids" eval="[]"/>
- <field name="state">confirmed</field>
+ <field name="description">Payment: 5 lunch tickets (6€)</field>
+ <field name="amount">30</field>
</record>
- <record model="lunch.order" id="order_4">
+ <record model="lunch.cashmove" id="cashmove_2">
<field name="user_id" ref="base.user_admin"/>
- <field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
- <field name="order_line_ids" eval="[]"/>
- <field name="state">confirmed</field>
+ <field name="date" eval="(DateTime.now() - timedelta(days=3)).strftime('%Y-%m-%d')"/>
+ <field name="description">Payment: 7 lunch tickets (6€)</field>
+ <field name="amount">42</field>
</record>
- <record model="lunch.order.line" id="order_line_1">
+ <record model="lunch.order" id="order_line_1">
<field name="user_id" ref="base.user_demo"/>
<field name="product_id" ref="product_Bolognese"/>
<field name="price">7.70</field>
<field name="date" eval="(DateTime.now() + timedelta(days=2)).strftime('%Y-%m-%d')"/>
<field name="state">new</field>
- <field name="supplier" ref="partner_pizza_inn"/>
- <field name="note">+Emmental</field>
- <field name="order_id" ref="order_1"/>
+ <field name="supplier_id" ref="supplier_pizza_inn"/>
+ <field name="quantity">1</field>
</record>
- <record model="lunch.order.line" id="order_line_2">
+ <record model="lunch.order" id="order_line_2">
<field name="user_id" ref="base.user_demo"/>
<field name="product_id" ref="product_italiana"/>
<field name="price">7.40</field>
<field name="date" eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d')"/>
<field name="state">confirmed</field>
- <field name="supplier" ref="partner_pizza_inn"/>
- <field name="note">+Mushrooms</field>
- <field name="order_id" ref="order_2"/>
+ <field name="supplier_id" ref="supplier_pizza_inn"/>
+ <field name="quantity">1</field>
</record>
- <record model="lunch.order.line" id="order_line_3">
+ <record model="lunch.order" id="order_line_3">
<field name="user_id" ref="base.user_demo"/>
<field name="product_id" ref="product_gouda"/>
<field name="price">2.50</field>
<field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
<field name="state">cancelled</field>
- <field name="supplier" ref="partner_coin_gourmand"/>
- <field name="note">+Salad +Tomatoes +Cucumbers</field>
- <field name="order_id" ref="order_3"/>
+ <field name="supplier_id" ref="supplier_coin_gourmand"/>
+ <field name="quantity">1</field>
</record>
- <record model="lunch.order.line" id="order_line_4">
+ <record model="lunch.order" id="order_line_4">
<field name="user_id" ref="base.user_demo"/>
<field name="product_id" ref="product_chicken_curry"/>
<field name="price">2.60</field>
<field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
<field name="state">confirmed</field>
- <field name="supplier" ref="partner_coin_gourmand"/>
- <field name="note">+Salad +Tomatoes +Eggs</field>
- <field name="order_id" ref="order_3"/>
+ <field name="supplier_id" ref="supplier_coin_gourmand"/>
+ <field name="quantity">1</field>
</record>
- <record model="lunch.order.line" id="order_line_5">
+ <record model="lunch.order" id="order_line_5">
<field name="user_id" ref="base.user_admin"/>
<field name="product_id" ref="product_4formaggi"/>
<field name="price">5.50</field>
<field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
<field name="state">confirmed</field>
- <field name="supplier" ref="partner_pizza_inn"/>
- <field name="order_id" ref="order_4"/>
- </record>
-
- <record model="lunch.cashmove" id="cashmove_1">
- <field name="user_id" ref="base.user_demo"/>
- <field name="date" eval="(DateTime.now() + timedelta(days=2)).strftime('%Y-%m-%d')"/>
- <field name="description">Pizza Italiana</field>
- <field name="amount">-7.40</field>
- <field name="order_id" ref="order_2"/>
- <field name="state">order</field>
- </record>
-
- <record model="lunch.cashmove" id="cashmove_2">
- <field name="user_id" ref="base.user_demo"/>
- <field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
- <field name="description">Chicken curry</field>
- <field name="amount">-2.60</field>
- <field name="order_id" ref="order_3"/>
- <field name="state">order</field>
- </record>
-
- <record model="lunch.cashmove" id="cashmove_3">
- <field name="user_id" ref="base.user_demo"/>
- <field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
- <field name="description">Payment: 5 lunch tickets (6€)</field>
- <field name="amount">30</field>
- <field name="state">payment</field>
- </record>
-
- <record model="lunch.cashmove" id="cashmove_4">
- <field name="user_id" ref="base.user_admin"/>
- <field name="date" eval="(DateTime.now() - timedelta(days=3)).strftime('%Y-%m-%d')"/>
- <field name="description">Cash given to HR manager</field>
- <field name="amount">40</field>
- <field name="state">payment</field>
- </record>
-
- <record model="lunch.cashmove" id="cashmove_5">
- <field name="user_id" ref="base.user_admin"/>
- <field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
- <field name="description">Pasta 4 Formaggi</field>
- <field name="amount">-5.50</field>
- <field name="order_id" ref="order_4"/>
- <field name="state">order</field>
- </record>
-
- <record model="lunch.alert" id="alert_1">
- <field name="alert_type">week</field>
- <field name="monday" eval="True"/>
- <field name="tuesday" eval="True"/>
- <field name="wednesday" eval="True"/>
- <field name="thursday" eval="True"/>
- <field name="friday" eval="True"/>
- <field name="saturday" eval="False"/>
- <field name="sunday" eval="False"/>
- <field name="start_hour">7</field>
- <field name="end_hour">23</field>
- <field name="message">Lunch must be ordered before 10:30 AM</field>
+ <field name="supplier_id" ref="supplier_pizza_inn"/>
</record>
</data>
</odoo>
diff --git a/addons/lunch/models/__init__.py b/addons/lunch/models/__init__.py
index f99750c87d0..94739d3cf9f 100644
--- a/addons/lunch/models/__init__.py
+++ b/addons/lunch/models/__init__.py
@@ -1,6 +1,12 @@
# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import lunch_alert
from . import lunch_cashmove
+from . import lunch_location
from . import lunch_order
from . import lunch_product
+from . import lunch_supplier
+from . import res_company
+from . import res_config_settings
+from . import res_users
diff --git a/addons/lunch/models/lunch_alert.py b/addons/lunch/models/lunch_alert.py
index 325a64f4385..93e0218631f 100644
--- a/addons/lunch/models/lunch_alert.py
+++ b/addons/lunch/models/lunch_alert.py
@@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-import datetime
+from odoo import api, fields, models
+from odoo.osv import expression
-from odoo import api, fields, models, _
+WEEKDAY_TO_NAME = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
class LunchAlert(models.Model):
""" Alerts to display during a lunch order. An alert can be specific to a
@@ -13,73 +14,46 @@ class LunchAlert(models.Model):
_description = 'Lunch Alert'
_rec_name = 'message'
- display = fields.Boolean(compute='_compute_display_get')
- message = fields.Text('Message', required=True)
- alert_type = fields.Selection([('specific', 'Specific Day'),
- ('week', 'Every Week'),
- ('days', 'Every Day')],
- string='Recurrence', required=True, index=True, default='specific')
- partner_id = fields.Many2one('res.partner', string="Vendor",
- help="If specified, the selected vendor can be ordered only on selected days")
- specific_day = fields.Date('Day', default=fields.Date.context_today)
- monday = fields.Boolean('Monday')
- tuesday = fields.Boolean('Tuesday')
- wednesday = fields.Boolean('Wednesday')
- thursday = fields.Boolean('Thursday')
- friday = fields.Boolean('Friday')
- saturday = fields.Boolean('Saturday')
- sunday = fields.Boolean('Sunday')
- start_hour = fields.Float('Between', oldname='active_from', required=True, default=7)
- end_hour = fields.Float('And', oldname='active_to', required=True, default=23)
- active = fields.Boolean(default=True)
+ message = fields.Html('Message', required=True)
- @api.multi
- def name_get(self):
- return [(alert.id, '%s %s' % (_('Alert'), '#%d' % alert.id)) for alert in self]
+ until = fields.Date('Show Until')
+ recurrency_monday = fields.Boolean('Monday')
+ recurrency_tuesday = fields.Boolean('Tuesday')
+ recurrency_wednesday = fields.Boolean('Wednesday')
+ recurrency_thursday = fields.Boolean('Thursday')
+ recurrency_friday = fields.Boolean('Friday')
+ recurrency_saturday = fields.Boolean('Saturday')
+ recurrency_sunday = fields.Boolean('Sunday')
- @api.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)
+ available_today = fields.Boolean('Is Displayed Today',
+ compute='_compute_available_today', search='_search_available_today')
- 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
- }
+ active = fields.Boolean(default=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)
+ location_ids = fields.Many2many('lunch.location', string='Location')
- if fullday or (from_alert <= mynow.time() <= to_alert):
- alert.display = True
- else:
- alert.display = False
+ @api.depends('recurrency_monday', 'recurrency_tuesday', 'recurrency_wednesday',
+ 'recurrency_thursday', 'recurrency_friday', 'recurrency_saturday',
+ 'recurrency_sunday')
+ def _compute_available_today(self):
+ today = fields.Date.context_today(self)
+ fieldname = 'recurrency_%s' % (WEEKDAY_TO_NAME[today.weekday()])
+
+ for alert in self:
+ alert.available_today = alert.until > today if alert.until else True and alert[fieldname]
+
+ 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)
+ today = fields.Date.context_today(self)
+ fieldname = 'recurrency_%s' % (WEEKDAY_TO_NAME[today.weekday()])
+
+ return expression.AND([
+ [(fieldname, operator, value)],
+ expression.OR([
+ [('until', '=', False)],
+ [('until', '>' if searching_for_true else '<', today)],
+ ])
+ ])
diff --git a/addons/lunch/models/lunch_cashmove.py b/addons/lunch/models/lunch_cashmove.py
index 7d4a6b56ae4..dab6faccbd1 100644
--- a/addons/lunch/models/lunch_cashmove.py
+++ b/addons/lunch/models/lunch_cashmove.py
@@ -2,22 +2,30 @@
# 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):
""" Two types of cashmoves: payment (credit) or order (debit) """
_name = 'lunch.cashmove'
_description = 'Lunch Cashmove'
+ _order = 'date desc'
+ currency_id = fields.Many2one('res.currency', default=lambda self: self.env['res.company']._company_default_get())
user_id = fields.Many2one('res.users', 'User',
default=lambda self: self.env.uid)
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')
- state = fields.Selection([('order', 'Order'), ('payment', 'Payment')],
- 'Is an order or a payment', default='payment')
+ amount = fields.Float('Amount', required=True)
+ description = fields.Text('Description')
@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, include_config=True):
+ result = float_round(sum(move['amount'] for move in self.env['lunch.cashmove.report'].search_read(
+ [('user_id', '=', user.id)], ['amount'])), precision_digits=2)
+ if include_config:
+ result += user.company_id.lunch_minimum_threshold
+ return result
diff --git a/addons/lunch/models/lunch_location.py b/addons/lunch/models/lunch_location.py
new file mode 100644
index 00000000000..73e9e07ba43
--- /dev/null
+++ b/addons/lunch/models/lunch_location.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class LunchLocation(models.Model):
+ _name = 'lunch.location'
+ _description = 'Lunch Locations'
+
+ name = fields.Char('Location Name')
+ address = fields.Text('Address')
diff --git a/addons/lunch/models/lunch_order.py b/addons/lunch/models/lunch_order.py
index f4354913db8..ee3f01c6d44 100644
--- a/addons/lunch/models/lunch_order.py
+++ b/addons/lunch/models/lunch_order.py
@@ -1,315 +1,79 @@
# -*- 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 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.
- """
_name = 'lunch.order'
_description = 'Lunch Order'
- _order = 'date desc'
-
- def _default_previous_order_ids(self):
- prev_order = self.env['lunch.order.line'].search([('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='id desc')
- # If we return return prev_order.ids, we will have duplicates (identical orders).
- # Therefore, this following part removes duplicates based on product_id and note.
- return list({
- (order.product_id, order.note): order.id
- for order in prev_order
- }.values())
-
+ _order = 'id desc'
+ _display_name = 'product_id'
+
+ name = fields.Char(related='product_id.name', string="Product Name", readonly=True) # to remove
+ topping_ids_1 = fields.Many2many('lunch.topping', 'lunch_order_topping', 'order_id', 'topping_id', string='Toppings 1', domain=[('topping_category', '=', 1)])
+ topping_ids_2 = fields.Many2many('lunch.topping', 'lunch_order_topping', 'order_id', 'topping_id', string='Toppings 2', domain=[('topping_category', '=', 2)])
+ topping_ids_3 = fields.Many2many('lunch.topping', 'lunch_order_topping', 'order_id', 'topping_id', string='Toppings 3', domain=[('topping_category', '=', 3)])
+ product_id = fields.Many2one('lunch.product', string="Product", required=True)
+ category_id = fields.Many2one('lunch.product.category', string='Product Category',
+ related='product_id.category_id', readonly=True, store=True)
+ date = fields.Date('Order Date', required=True, readonly=True,
+ states={'new': [('readonly', False)]},
+ default=fields.Date.context_today)
+ 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', 'User', readonly=True,
states={'new': [('readonly', False)]},
default=lambda self: self.env.uid)
- date = fields.Date('Date', required=True, readonly=True,
- states={'new': [('readonly', False)]},
- default=fields.Date.context_today)
- order_line_ids = fields.One2many('lunch.order.line', 'order_id', 'Products',
- readonly=True, copy=True,
- states={'new': [('readonly', False)], False: [('readonly', False)]})
- total = fields.Float(compute='_compute_total', string="Total", store=True)
- state = fields.Selection([('new', 'New'),
- ('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")
- 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')
- def _compute_total(self):
- """
- get and sum the order lines' price
- """
- self.total = sum(
- orderline.price for orderline in self.order_line_ids)
-
- @api.multi
- 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):
- 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):
- 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
- """
- 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
-
- if isConfirmed:
- self.state = 'confirmed'
- else:
- self.state = 'new'
- return
-
-
-class LunchOrderLine(models.Model):
- _name = 'lunch.order.line'
- _description = 'Lunch Order Line'
- _order = 'date desc, id desc'
-
- 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)])
- 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)
- 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,
+ note = fields.Text('Notes')
+ 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'),
+ active = fields.Boolean('Active', default=True)
+ state = fields.Selection([('new', 'To Order'),
('ordered', 'Ordered'),
+ ('confirmed', 'Received'),
('cancelled', 'Cancelled')],
'Status', readonly=True, index=True, default='new')
- cashmove = fields.One2many('lunch.cashmove', 'order_id', 'Cash Move')
- currency_id = fields.Many2one('res.currency', related='order_id.currency_id', readonly=False)
-
- 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.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
-
- @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 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'})
-
- 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
-
- 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)
+ company_id = fields.Many2one('res.company', related='user_id.company_id', store=True)
+ currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True, store=True)
+ quantity = fields.Float('Quantity', required=True, default=1)
+
+ display_toppings = fields.Text('Toppings', compute='_compute_display_toppings', store=True)
+
+ @api.depends('topping_ids_1', 'topping_ids_2', 'topping_ids_3', 'product_id', 'quantity')
+ def _compute_total_price(self):
+ for line in self:
+ line.price = line.quantity * (line.product_id.price + sum((line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3).mapped('price')))
+
+ @api.depends('topping_ids_1', 'topping_ids_2', 'topping_ids_3')
+ def _compute_display_toppings(self):
+ for line in self:
+ toppings = line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3
+ line.display_toppings = '+ '.join(toppings.mapped('name'))
+
+ def update_quantity(self, increment):
+ for line in self.filtered(lambda line: line.state != 'confirmed'):
+ if line.quantity <= -increment:
+ # TODO: maybe unlink the order?
+ line.active = False
+ else:
+ line.quantity += increment
+ self._check_wallet()
- 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,
- }
+ def _check_wallet(self):
+ for line in self:
+ if self.env['lunch.cashmove'].get_wallet_balance(line.user_id) < 0:
+ raise ValidationError(_('Your wallet does not contain enough money to order that.'
+ 'To add some money to your wallet, please contact your lunch manager.'))
- else:
- raise AccessError(_("Only your lunch manager processes the orders."))
+ def action_order(self):
+ self._check_wallet()
+ self.write({'state': 'ordered'})
- @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."))
+ def action_confirm(self):
+ self.write({'state': 'confirmed'})
- @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."))
+ def action_cancel(self):
+ self.write({'state': 'cancelled'})
diff --git a/addons/lunch/models/lunch_product.py b/addons/lunch/models/lunch_product.py
index 8ded108fc81..0bc595c135c 100644
--- a/addons/lunch/models/lunch_product.py
+++ b/addons/lunch/models/lunch_product.py
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import api, fields, models, _
+from odoo import api, fields, models, tools
from odoo.addons import decimal_precision as dp
-from odoo.osv import expression
+from odoo.tools import formatLang
class LunchProductCategory(models.Model):
@@ -13,51 +13,109 @@ class LunchProductCategory(models.Model):
_description = 'Lunch Product Category'
name = fields.Char('Product Category', required=True)
+ company_id = fields.Many2one('res.company', default=lambda self: self.env['res.company']._company_default_get())
+ currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
+ topping_label_1 = fields.Char('Topping Label 1', required=True, default='Supplements')
+ topping_label_2 = fields.Char('Topping Label 2', required=True, default='Beverages')
+ topping_label_3 = fields.Char('Topping Label 3', required=True, default='Topping Label 3')
+ topping_ids_1 = fields.One2many('lunch.topping', 'category_id', domain=[('topping_category', '=', 1)], ondelete='cascade')
+ topping_ids_2 = fields.One2many('lunch.topping', 'category_id', domain=[('topping_category', '=', 2)], ondelete='cascade')
+ topping_ids_3 = fields.One2many('lunch.topping', 'category_id', domain=[('topping_category', '=', 3)], ondelete='cascade')
+ topping_quantity_1 = fields.Selection([
+ ('0_more', 'None or More'),
+ ('1_more', 'One or More'),
+ ('1', 'Only One')], default='0_more', required=True)
+ topping_quantity_2 = fields.Selection([
+ ('0_more', 'None or More'),
+ ('1_more', 'One or More'),
+ ('1', 'Only One')], default='0_more', required=True)
+ topping_quantity_3 = fields.Selection([
+ ('0_more', 'None or More'),
+ ('1_more', 'One or More'),
+ ('1', 'Only One')], default='0_more', required=True)
+
+ @api.model
+ def create(self, vals):
+ for topping in vals.get('topping_ids_2', []):
+ topping[2].update({'topping_category': 2})
+ for topping in vals.get('topping_ids_3', []):
+ topping[2].update({'topping_category': 3})
+ return super(LunchProductCategory, self).create(vals)
+
+ def write(self, vals):
+ for topping in vals.get('topping_ids_2', []):
+ topping_values = topping[2]
+ if topping_values:
+ topping_values.update({'topping_category': 2})
+ for topping in vals.get('topping_ids_3', []):
+ topping_values = topping[2]
+ if topping_values:
+ topping_values.update({'topping_category': 3})
+ return super(LunchProductCategory, self).write(vals)
+
+
+class LunchTopping(models.Model):
+ """"""
+ _name = 'lunch.topping'
+ _description = 'Lunch Toppings'
+
+ name = fields.Char('Name', required=True)
+ company_id = fields.Many2one('res.company', default=lambda self: self.env['res.company']._company_default_get())
+ currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
+ price = fields.Float('Price', digits=dp.get_precision('Account'), required=True)
+ category_id = fields.Many2one('lunch.product.category')
+ topping_category = fields.Integer('Topping Category', help="This field is a technical field", required=True, default=1)
+
+ def name_get(self):
+ currency_id = self.env.user.company_id.currency_id
+ res = dict(super(LunchTopping, self).name_get())
+ for topping in self:
+ price = formatLang(self.env, topping.price, currency_obj=currency_id)
+ res[topping.id] = '%s %s' % (topping.name, price)
+ return list(res.items())
class LunchProduct(models.Model):
""" Products available to order. A product is linked to a specific vendor. """
_name = 'lunch.product'
_description = 'Lunch Product'
+ _order = 'name'
- name = fields.Char('Product', required=True)
+ name = fields.Char('Product Name', 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'))
- 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')
-
- @api.depends('supplier')
- def _get_available_product(self):
- 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)]
+
+ company_id = fields.Many2one('res.company', default=lambda self: self.env['res.company']._company_default_get())
+ currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
+
+ # image: all image fields are base64 encoded and PIL-supported
+ image = fields.Binary(
+ "Image",
+ help="This field holds the image used as image for the product, limited to 1024x1024px.")
+ image_medium = fields.Binary(
+ "Medium-sized image",
+ 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",
+ 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.")
+
+ new_until = fields.Date('New Until')
+ favorite_user_ids = fields.Many2many('res.users', 'lunch_product_favorite_user_rel', 'product_id', 'user_id')
+
+
+ @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)
diff --git a/addons/lunch/models/lunch_supplier.py b/addons/lunch/models/lunch_supplier.py
new file mode 100644
index 00000000000..f0ee44b853f
--- /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, moment='am', tz=None):
+ """ Convert a number of hours into a time object. """
+ if hours == 12.0 and moment == 'pm':
+ return time.max
+ fractional, integral = math.modf(hours)
+ if moment == 'pm':
+ integral += 12
+ 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'
+ _inherit = ['mail.thread', 'mail.activity.mixin']
+
+ partner_id = fields.Many2one('res.partner', string='Vendor', required=True)
+
+ name = fields.Char('Name', related='partner_id.name', readonly=False)
+
+ email = fields.Char(related='partner_id.email', readonly=False)
+ email_formatted = fields.Char(related='partner_id.email_formatted', readonly=True)
+ phone = fields.Char(related='partner_id.phone', readonly=False)
+ street = fields.Char(related='partner_id.street', readonly=False)
+ street2 = fields.Char(related='partner_id.street2', readonly=False)
+ zip_code = fields.Char(related='partner_id.zip', readonly=False)
+ city = fields.Char(related='partner_id.city', readonly=False)
+ state_id = fields.Many2one("res.country.state", related='partner_id.state_id', readonly=False)
+ country_id = fields.Many2one('res.country', related='partner_id.country_id', readonly=False)
+
+ responsible_id = fields.Many2one('res.users', string="Responsible", domain=lambda self: [('groups_id', 'in', self.env.ref('lunch.group_lunch_manager').id)],
+ default=lambda self: self.env.user.partner_id,
+ help="The responsible is the person that will order lunch for everyone"
+ "It will be used as the 'from' when sending the automatic email")
+
+ send_by = fields.Selection([
+ ('phone', 'Phone'),
+ ('mail', 'Email'),
+ ], 'Send Order By', default='phone')
+ automatic_email_time = fields.Float('Order Time', default=12.0, required=True)
+
+ recurrency_monday = fields.Boolean('Monday', default=True)
+ recurrency_tuesday = fields.Boolean('Tuesday', default=True)
+ recurrency_wednesday = fields.Boolean('Wednesday', default=True)
+ recurrency_thursday = fields.Boolean('Thursday', default=True)
+ recurrency_friday = fields.Boolean('Friday', default=True)
+ recurrency_saturday = fields.Boolean('Saturday')
+ recurrency_sunday = fields.Boolean('Sunday')
+
+ recurrency_end_date = fields.Date('Until', help="This field is used in order to ")
+
+ available_location_ids = fields.Many2many('lunch.location', string='Location')
+ 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=lambda self: self.env.user.tz or 'UTC')
+
+ active = fields.Boolean(default=True)
+
+ moment = fields.Selection([
+ ('am', 'AM'),
+ ('pm', 'PM'),
+ ], default='am', required=True)
+
+ delivery = fields.Selection([
+ ('delivery', 'Delivery'),
+ ('no_delivery', 'No Delivery')
+ ], default='no_delivery')
+
+ _sql_constraints = [
+ ('automatic_email_time_range',
+ 'CHECK(automatic_email_time >= 0 AND automatic_email_time <= 12)',
+ 'Automatic Email Sending Time should be between 0 and 12'),
+ ]
+
+ @api.multi
+ def name_get(self):
+ res = []
+ for supplier in self:
+ if supplier.phone:
+ res.append((supplier.id, '%s %s' % (supplier.name, supplier.phone)))
+ else:
+ res.append((supplier.id, supplier.name))
+ return res
+
+ @api.model
+ def _auto_email_send(self):
+ """
+ This method is called every 20 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([('send_by', '=', 'mail')])
+
+ for supplier in records:
+ send_at = datetime.combine(fields.Date.today(),
+ float_to_time(supplier.automatic_email_time, supplier.moment, supplier.tz)).astimezone(pytz.UTC).replace(tzinfo=None)
+ if supplier.available_today and fields.Datetime.now() > send_at:
+ lines = self.env['lunch.order'].search([('supplier_id', '=', supplier.id),
+ ('state', '=', 'ordered'), ('date', '=', fields.Date.today())])
+
+ if lines:
+ order = {
+ 'company_name': lines[0].company_id.name,
+ 'currency_id': lines[0].currency_id.id,
+ 'supplier_id': supplier.partner_id.id,
+ 'supplier_name': supplier.name,
+ 'email_from': supplier.responsible_id.email_formatted,
+ }
+
+ _lines = [{
+ 'product': line.product_id.name,
+ 'note': line.note,
+ 'quantity': line.quantity,
+ 'price': line.price,
+ 'toppings': line.display_toppings,
+ 'username': line.user_id.name,
+ } 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)
+
+ lines.action_confirm()
+
+ @api.depends('recurrency_end_date', '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))
+
+ if supplier.recurrency_end_date and now.date() > supplier.recurrency_end_date:
+ supplier.available_today = False
+ else:
+ fieldname = 'recurrency_%s' % (WEEKDAY_TO_NAME[now.weekday()])
+ supplier.available_today = supplier[fieldname]
+
+ 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().replace(tzinfo=pytz.UTC).astimezone(pytz.timezone(self.env.user.tz or 'UTC'))
+ fieldname = 'recurrency_%s' % (WEEKDAY_TO_NAME[now.weekday()])
+
+ recurrency_domain = expression.OR([
+ [('recurrency_end_date', '=', False)],
+ [('recurrency_end_date', '>' if searching_for_true else '<', now)]
+ ])
+
+ return expression.AND([
+ recurrency_domain,
+ [(fieldname, operator, value)]
+ ])
diff --git a/addons/lunch/models/res_company.py b/addons/lunch/models/res_company.py
new file mode 100644
index 00000000000..95d91d09596
--- /dev/null
+++ b/addons/lunch/models/res_company.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models, fields
+
+
+class Company(models.Model):
+ _inherit = 'res.company'
+
+ lunch_minimum_threshold = fields.Float()
diff --git a/addons/lunch/models/res_config_settings.py b/addons/lunch/models/res_config_settings.py
new file mode 100644
index 00000000000..faa8f256f49
--- /dev/null
+++ b/addons/lunch/models/res_config_settings.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = 'res.config.settings'
+
+ currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
+ company_lunch_minimum_threshold = fields.Float(string="Maximum Allowed Overdraft", readonly=False, related='company_id.lunch_minimum_threshold')
diff --git a/addons/lunch/models/res_users.py b/addons/lunch/models/res_users.py
new file mode 100644
index 00000000000..edd2ee47740
--- /dev/null
+++ b/addons/lunch/models/res_users.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class ResUsers(models.Model):
+ _inherit = 'res.users'
+
+ last_lunch_location_id = fields.Many2one('lunch.location')
diff --git a/addons/lunch/report/__init__.py b/addons/lunch/report/__init__.py
new file mode 100644
index 00000000000..2fc4ee848a1
--- /dev/null
+++ b/addons/lunch/report/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import lunch_cashmove_report
+from . import lunch_product_report
diff --git a/addons/lunch/report/lunch_cashmove_report.py b/addons/lunch/report/lunch_cashmove_report.py
new file mode 100644
index 00000000000..b4615823082
--- /dev/null
+++ b/addons/lunch/report/lunch_cashmove_report.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, tools, _
+
+
+class CashmoveReport(models.Model):
+ _name = "lunch.cashmove.report"
+ _description = 'Cashmoves report'
+ _auto = False
+ _order = "date desc"
+
+ id = fields.Integer('ID')
+ amount = fields.Float('Amount')
+ date = fields.Date('Date')
+ currency_id = fields.Many2one('res.currency', string='Currency')
+ user_id = fields.Many2one('res.users', string='User')
+ description = fields.Text('Description')
+
+ @api.multi
+ def name_get(self):
+ return [(cashmove.id, '%s %s' % (_('Lunch Cashmove'), '#%d' % cashmove.id)) for cashmove in self]
+
+ def init(self):
+ tools.drop_view_if_exists(self._cr, self._table)
+
+ self._cr.execute("""
+ CREATE or REPLACE view %s as (
+ SELECT
+ lc.id as id,
+ lc.amount as amount,
+ lc.date as date,
+ lc.currency_id as currency_id,
+ lc.user_id as user_id,
+ lc.description as description
+ FROM lunch_cashmove lc
+ UNION ALL
+ SELECT
+ -lol.id as id,
+ -lol.price as amount,
+ lol.date as date,
+ lol.currency_id as currency_id,
+ lol.user_id as user_id,
+ format('Order for %%s x %%s %%s', lol.quantity::text, lp.name, lol.display_toppings) as description
+ FROM lunch_order lol
+ JOIN lunch_product lp ON lp.id = lol.product_id
+ WHERE
+ lol.state in ('ordered', 'confirmed')
+ AND lol.active = True
+ );
+ """ % self._table)
diff --git a/addons/lunch/report/lunch_cashmove_report_views.xml b/addons/lunch/report/lunch_cashmove_report_views.xml
new file mode 100644
index 00000000000..73f091ccb92
--- /dev/null
+++ b/addons/lunch/report/lunch_cashmove_report_views.xml
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="lunch_cashmove_report_view_search" model="ir.ui.view">
+ <field name='name'>lunch.cashmove.report.search</field>
+ <field name='model'>lunch.cashmove.report</field>
+ <field name='arch' type='xml'>
+ <search string="lunch employee payment">
+ <field name="description"/>
+ <field name="user_id"/>
+ <filter name='is_payment' string="Payment" domain="[('amount', '>', 0)]"/>
+ <separator/>
+ <filter name='is_mine_group' string="My Account grouped" domain="[('user_id','=',uid)]" context="{'group_by':'user_id'}"/>
+ <filter name="group_by_user" string="By User" context="{'group_by':'user_id'}"/>
+ </search>
+ </field>
+ </record>
+
+ <record id="lunch_cashmove_report_view_search_2" model="ir.ui.view">
+ <field name='name'>lunch.cashmove.report.search</field>
+ <field name='model'>lunch.cashmove.report</field>
+ <field name='arch' type='xml'>
+ <search string="lunch cashmove">
+ <field name="description"/>
+ <field name="user_id"/>
+ <group expand="0" string="Group By">
+ <filter name='group_by_user' string="By Employee" context="{'group_by':'user_id'}"/>
+ </group>
+ </search>
+ </field>
+ </record>
+
+ <record id="lunch_cashmove_report_view_tree" model="ir.ui.view">
+ <field name="name">lunch.cashmove.report.tree</field>
+ <field name="model">lunch.cashmove.report</field>
+ <field name="arch" type="xml">
+ <tree string="cashmove tree">
+ <field name="currency_id" invisible="1"/>
+ <field name="date"/>
+ <field name="user_id"/>
+ <field name="description"/>
+ <field name="amount" sum="Total" widget="monetary"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="lunch_cashmove_report_view_tree_2" model="ir.ui.view">
+ <field name="name">lunch.cashmove.report.tree</field>
+ <field name="model">lunch.cashmove.report</field>
+ <field name="arch" type="xml">
+ <tree string="cashmove tree" create='false'>
+ <field name="currency_id" invisible="1"/>
+ <field name="date"/>
+ <field name="description"/>
+ <field name="amount" sum="Total" widget="monetary"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="lunch_cashmove_report_view_form" model="ir.ui.view">
+ <field name="name">lunch.cashmove.report.form</field>
+ <field name="model">lunch.cashmove.report</field>
+ <field name="arch" type="xml">
+ <form string="cashmove form">
+ <sheet>
+ <group>
+ <field name="currency_id" invisible="1"/>
+ <field name="user_id" required="1"/>
+ <field name="date"/>
+ <field name="amount" widget="montary"/>
+ </group>
+ <label for='description'/>
+ <field name="description"/>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="view_lunch_cashmove_report_kanban" model="ir.ui.view">
+ <field name="name">lunch.cashmove.report.kanban</field>
+ <field name="model">lunch.cashmove.report</field>
+ <field name="arch" type="xml">
+ <kanban class="o_kanban_mobile">
+ <field name="date"/>
+ <field name="user_id"/>
+ <field name="description"/>
+ <field name="amount"/>
+ <field name="currency_id" invisible="1"/>
+ <templates>
+ <t t-name="kanban-box">
+ <div t-attf-class="oe_kanban_global_click">
+ <div class="row mb4">
+ <div class="col-8">
+ <span>
+ <strong class="o_kanban_record_title"><t t-esc="record.description.value"/></strong>
+ </span>
+ </div>
+ <div class="col-4 text-right">
+ <span class="badge badge-pill">
+ <strong><i class="fa fa-money" role="img" aria-label="Amount" title="Amount"/> <field name="amount" widget="monetary"/></strong>
+ </span>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-6">
+ <i class="fa fa-clock-o" role="img" aria-label="Date" title="Date"/>
+ <t t-esc="record.date.value"/>
+ </div>
+ <div class="col-6">
+ <img t-att-src="kanban_image('res.users', 'image_small', record.user_id.raw_value)" t-att-title="record.user_id.value" t-att-alt="record.user_id.value" width="24" height="24" class="oe_kanban_avatar float-right"/>
+ </div>
+ </div>
+ </div>
+ </t>
+ </templates>
+ </kanban>
+ </field>
+ </record>
+
+ <record id="lunch_cashmove_report_action_account" model="ir.actions.act_window">
+ <field name="name">My Account</field>
+ <field name="res_model">lunch.cashmove.report</field>
+ <field name="view_mode">tree</field>
+ <field name="search_view_id" ref="lunch_cashmove_report_view_search"/>
+ <field name="domain">[('user_id','=',uid)]</field>
+ <field name="view_id" ref="lunch_cashmove_report_view_tree_2"/>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_empty_folder">
+ No cash move yet
+ </p><p>
+ Here you can see your cash moves.<br/>A cash move can either be an expense or a payment.
+ An expense is automatically created when an order is received while a payment is a reimbursement to the company encoded by the manager.
+ </p>
+ </field>
+ </record>
+
+ <record id="lunch_cashmove_report_action_control_accounts" model="ir.actions.act_window">
+ <field name="name">Control Accounts</field>
+ <field name="res_model">lunch.cashmove.report</field>
+ <field name="view_mode">tree,kanban,form</field>
+ <field name="search_view_id" ref="lunch_cashmove_report_view_search_2"/>
+ <field name="context">{"search_default_group_by_user":1}</field>
+ <field name="view_id" ref="lunch_cashmove_report_view_tree"/>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ Create a new payment
+ </p><p>
+ A cashmove can either be an expense or a payment.<br/>
+ An expense is automatically created at the order receipt.<br/>
+ A payment represents the employee reimbursement to the company.
+ </p>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/lunch/report/lunch_order_templates.xml b/addons/lunch/report/lunch_order_templates.xml
deleted file mode 100644
index 82290ce2310..00000000000
--- a/addons/lunch/report/lunch_order_templates.xml
+++ /dev/null
@@ -1,79 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
- <template id="report_lunch_order">
- <t t-call="web.html_container">
- <t t-call="web.external_layout">
- <div class="page">
- <div class="oe_structure"/>
-
- <div class="row">
- <div class="col-5 offset-7">
- <div t-field="user.partner_id"
- t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}' />
- </div>
- </div>
-
- <h2>Lunch Order</h2>
-
- <table class="table table-sm">
- <thead>
- <tr>
- <th>Name/Date</th>
- <th>Order</th>
- <th>Description</th>
- <th class="text-right">Unit Price</th>
- </tr>
- </thead>
- <tbody>
- <t t-foreach="docs.read_group([('id', 'in', docs.ids)],['user_id'],['user_id'])" t-as="o">
- <t t-set="user" t-value="user.browse(o['user_id'][0])"/>
- <t t-set="lines" t-value="docs.search([('user_id', '=', user.id), ('id', 'in', docs.ids)])"/>
- <tr>
- <td colspan="2">
- <strong t-field="user.name"/>
- </td>
- <td class="text-right" colspan="2">
- <strong>
- <span t-esc="sum(line.price for line in lines)" t-options="{'widget': 'monetary', 'display_currency': user.company_id.currency_id}"/>
- </strong>
- </td>
- </tr>
- <tr t-foreach="lines" t-as="line">
- <td>
- <span t-field="line.date"></span>
- </td>
- <td>
- <span t-field="line.product_id.name"/>
- </td>
- <td>
- <span t-field="line.note"/>
- </td>
- <td class="text-right">
- <span t-field="line.price"
- t-options='{"widget": "monetary", "display_currency": user.company_id.currency_id}'/>
- </td>
- </tr>
- </t>
- </tbody>
- </table>
-
- <div class="row justify-content-end">
- <div class="col-4">
- <table class="table table-sm">
- <tr class="border-black">
- <td><strong>Total</strong></td>
- <td class="text-right">
- <span t-esc="sum([line.price for line in docs]) or 0.0"/>
- <span t-field="user.company_id.currency_id.symbol"/>
- </td>
- </tr>
- </table>
- </div>
- </div>
-
- <div class="oe_structure"/>
- </div>
- </t>
- </t>
- </template>
-</odoo>
diff --git a/addons/lunch/report/lunch_order_views.xml b/addons/lunch/report/lunch_order_views.xml
deleted file mode 100644
index 7ba774d8805..00000000000
--- a/addons/lunch/report/lunch_order_views.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
- <report
- id="action_report_lunch_order"
- string="Lunch Order"
- model="lunch.order.line"
- report_type="qweb-pdf"
- name="lunch.report_lunch_order"
- file="lunch.report_lunch_order"
- />
-</odoo> \ No newline at end of file
diff --git a/addons/lunch/report/lunch_product_report.py b/addons/lunch/report/lunch_product_report.py
new file mode 100644
index 00000000000..140e5c73a0a
--- /dev/null
+++ b/addons/lunch/report/lunch_product_report.py
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, tools
+from odoo.osv import expression
+
+
+class LunchProductReport(models.Model):
+ _name = "lunch.product.report"
+ _description = 'Product report'
+ _auto = False
+ _order = 'is_favorite desc, is_new desc, last_order_date asc, product_id asc'
+
+ id = fields.Integer('ID')
+ product_id = fields.Integer('Product ID')
+ name = fields.Char('Product Name')
+ category_id = fields.Many2one('lunch.product.category', 'Product Category')
+ description = fields.Text('Description')
+ price = fields.Float('Price')
+ supplier_id = fields.Many2one('lunch.supplier', 'Vendor')
+ company_id = fields.Many2one('res.company')
+ currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
+ is_favorite = fields.Boolean('Favorite')
+ user_id = fields.Many2one('res.users')
+ is_new = fields.Boolean('New')
+ active = fields.Boolean('Active')
+ last_order_date = fields.Date('Last Order Date')
+
+ # This field is used only for searching
+ is_available_at = fields.Many2one('lunch.location', 'Product Availability', compute='_compute_is_available_at', search='_search_is_available_at')
+
+ def _compute_is_available_at(self):
+ """
+ Is available_at is always false when browsing it
+ this field is there only to search (see _search_is_available_at)
+ """
+ for product in self:
+ product.is_available_at = False
+
+ def _search_is_available_at(self, operator, value):
+ supported_operators = ['in', 'not in', '=', '!=']
+
+ if not operator in supported_operators:
+ return expression.TRUE_DOMAIN
+
+ if isinstance(value, int):
+ value = [value]
+
+ if operator in expression.NEGATIVE_TERM_OPERATORS:
+ return expression.AND([[('supplier_id.available_location_ids', 'not in', value)], [('supplier_id.available_location_ids', '!=', False)]])
+
+ return expression.OR([[('supplier_id.available_location_ids', 'in', value)], [('supplier_id.available_location_ids', '=', False)]])
+
+ @api.multi
+ def write(self, values):
+ user_id = self.env.user.id
+ if 'is_favorite' in values:
+ for report in self:
+ if values['is_favorite']:
+ self.env.cr.execute('''INSERT INTO lunch_product_favorite_user_rel(product_id, user_id) VALUES (%s, %s)''',
+ (report.product_id, user_id))
+ else:
+ self.env.cr.execute('''DELETE FROM lunch_product_favorite_user_rel WHERE product_id=%s AND user_id=%s''',
+ (report.product_id, user_id))
+
+ def init(self):
+ tools.drop_view_if_exists(self._cr, self._table)
+
+ self._cr.execute("""
+ CREATE or REPLACE view %s AS (
+ SELECT
+ row_number() over () AS id,
+ product.id AS product_id,
+ product.name,
+ product.category_id,
+ product.description,
+ product.price,
+ product.supplier_id,
+ product.company_id,
+ product.active,
+ users.id AS user_id,
+ fav.user_id IS NOT NULL AS is_favorite,
+ product.new_until >= current_date AS is_new,
+ (SELECT date FROM lunch_order WHERE product_id = product.id AND user_id = users.id AND date <= current_date ORDER BY date DESC LIMIT 1) AS last_order_date
+ FROM lunch_product product
+ RIGHT JOIN res_users users ON users.company_id = product.company_id -- multi company
+ LEFT JOIN lunch_product_favorite_user_rel fav ON fav.product_id = product.id AND fav.user_id = users.id
+ JOIN res_groups_users_rel groups ON groups.uid = users.id -- only generate for internal users
+ WHERE users.active AND product.active AND groups.gid = %%s --only take into account active products and users
+ );
+ """ % self._table, (self.env.ref('base.group_user').id,))
diff --git a/addons/lunch/security/ir.model.access.csv b/addons/lunch/security/ir.model.access.csv
index 244fbf11f18..91ea62dc3f8 100644
--- a/addons/lunch/security/ir.model.access.csv
+++ b/addons/lunch/security/ir.model.access.csv
@@ -1,13 +1,20 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
-order_manager,"Order user",model_lunch_order,group_lunch_manager,1,1,1,1
-order_line_manager,"Order Line user",model_lunch_order_line,group_lunch_manager,1,1,1,1
-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
+cashmove_manager,"Cashmove user",model_lunch_cashmove,group_lunch_manager,1,1,1,1
+order_user,"Order Line user",model_lunch_order,group_lunch_user,1,1,1,1
+order_manager,"Order Line user",model_lunch_order,group_lunch_manager,1,1,1,1
product_user,"Product user",model_lunch_product,group_lunch_user,1,0,0,0
+product_manager,"Product user",model_lunch_product,group_lunch_manager,1,1,1,1
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
+product_category_manager,"Product category user",model_lunch_product_category,group_lunch_manager,1,1,1,1
+lunch_alert_access,access_lunch_alert_user,model_lunch_alert,base.group_user,1,0,0,0
+lunch_alert_user,access_lunch_alert_lunch_user,model_lunch_alert,group_lunch_user,1,1,1,1
+lunch_alert_manager,access_lunch_alert_manager,model_lunch_alert,group_lunch_manager,1,1,1,1
+lunch_cashmove_report_manager,access_lunch_cashmove_report,model_lunch_cashmove_report,base.group_user,1,0,0,0
+lunch_location_user,access_lunch_location,model_lunch_location,group_lunch_user,1,1,0,0
+lunch_location_manager,access_lunch_location,model_lunch_location,group_lunch_manager,1,1,1,1
+lunch_topping_user,access_lunch_topping,model_lunch_topping,group_lunch_user,1,0,0,0
+lunch_topping_manager,access_lunch_topping,model_lunch_topping,group_lunch_manager,1,1,1,1
+lunch_supplier_user,"Lunch Supplier User Rights",model_lunch_supplier,group_lunch_user,1,0,0,0
+lunch_supplier_manager,"Lunch Supplier Manager Rights",model_lunch_supplier,group_lunch_manager,1,1,1,1
+lunch_product_report_user,access_lunch_product_report,model_lunch_product_report,group_lunch_user,1,1,0,0
diff --git a/addons/lunch/security/lunch_security.xml b/addons/lunch/security/lunch_security.xml
index a32a9f2c6af..bd7835a4a36 100644
--- a/addons/lunch/security/lunch_security.xml
+++ b/addons/lunch/security/lunch_security.xml
@@ -29,9 +29,9 @@
<field name="groups" eval="[(4, ref('group_lunch_manager'))]"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
- <record id="lunch_order_line_rule_delete" model="ir.rule">
- <field name="name">lunch.order.line: Only new and cancelled order lines deleted.</field>
- <field name="model_id" ref="lunch.model_lunch_order_line"/>
+ <record id="lunch_order_rule_delete" model="ir.rule">
+ <field name="name">lunch.order: Only new and cancelled order lines deleted.</field>
+ <field name="model_id" ref="lunch.model_lunch_order"/>
<field name="domain_force">[('state', 'in', ('new', 'cancelled'))]</field>
<field name="perm_read" eval="0"/>
<field name="perm_write" eval="0"/>
diff --git a/addons/lunch/static/img/4formaggio.png b/addons/lunch/static/img/4formaggio.png
new file mode 100644
index 00000000000..f19c95ac934
--- /dev/null
+++ b/addons/lunch/static/img/4formaggio.png
Binary files differ
diff --git a/addons/lunch/static/img/Coke.png b/addons/lunch/static/img/Coke.png
new file mode 100644
index 00000000000..e822a2656ac
--- /dev/null
+++ b/addons/lunch/static/img/Coke.png
Binary files 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
--- /dev/null
+++ b/addons/lunch/static/img/bacon_burger.png
Binary files differ
diff --git a/addons/lunch/static/img/brie.png b/addons/lunch/static/img/brie.png
new file mode 100644
index 00000000000..5b76bfbb1ee
--- /dev/null
+++ b/addons/lunch/static/img/brie.png
Binary files differ
diff --git a/addons/lunch/static/img/cheeseburger.png b/addons/lunch/static/img/cheeseburger.png
new file mode 100644
index 00000000000..f9d5a3e5c01
--- /dev/null
+++ b/addons/lunch/static/img/cheeseburger.png
Binary files 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
--- /dev/null
+++ b/addons/lunch/static/img/chicken_curry.png
Binary files differ
diff --git a/addons/lunch/static/img/chirashi.png b/addons/lunch/static/img/chirashi.png
new file mode 100644
index 00000000000..65143711848
--- /dev/null
+++ b/addons/lunch/static/img/chirashi.png
Binary files differ
diff --git a/addons/lunch/static/img/club.png b/addons/lunch/static/img/club.png
new file mode 100644
index 00000000000..5f3e36f3f37
--- /dev/null
+++ b/addons/lunch/static/img/club.png
Binary files 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
--- /dev/null
+++ b/addons/lunch/static/img/coke_zero.png
Binary files differ
diff --git a/addons/lunch/static/img/fanta.png b/addons/lunch/static/img/fanta.png
new file mode 100644
index 00000000000..b7a48332e0d
--- /dev/null
+++ b/addons/lunch/static/img/fanta.png
Binary files 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
--- /dev/null
+++ b/addons/lunch/static/img/fuze_black.png
Binary files 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
--- /dev/null
+++ b/addons/lunch/static/img/fuze_green.png
Binary files differ
diff --git a/addons/lunch/static/img/gouda.png b/addons/lunch/static/img/gouda.png
new file mode 100644
index 00000000000..c50d6d8dfbf
--- /dev/null
+++ b/addons/lunch/static/img/gouda.png
Binary files differ
diff --git a/addons/lunch/static/img/italiana.png b/addons/lunch/static/img/italiana.png
new file mode 100644
index 00000000000..2f8c4244436
--- /dev/null
+++ b/addons/lunch/static/img/italiana.png
Binary files differ
diff --git a/addons/lunch/static/img/lipton.png b/addons/lunch/static/img/lipton.png
new file mode 100644
index 00000000000..a1f8ef21f2e
--- /dev/null
+++ b/addons/lunch/static/img/lipton.png
Binary files differ
diff --git a/addons/lunch/static/img/maki.png b/addons/lunch/static/img/maki.png
new file mode 100644
index 00000000000..c92dd8c83a4
--- /dev/null
+++ b/addons/lunch/static/img/maki.png
Binary files differ
diff --git a/addons/lunch/static/img/mozza.png b/addons/lunch/static/img/mozza.png
new file mode 100644
index 00000000000..8820fbb9cd5
--- /dev/null
+++ b/addons/lunch/static/img/mozza.png
Binary files differ
diff --git a/addons/lunch/static/img/napoli.png b/addons/lunch/static/img/napoli.png
new file mode 100644
index 00000000000..c7ecd65db8a
--- /dev/null
+++ b/addons/lunch/static/img/napoli.png
Binary files 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
--- /dev/null
+++ b/addons/lunch/static/img/pasta_bolognese.png
Binary files 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
--- /dev/null
+++ b/addons/lunch/static/img/pizza_funghi.png
Binary files 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
--- /dev/null
+++ b/addons/lunch/static/img/pizza_margherita.png
Binary files 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
--- /dev/null
+++ b/addons/lunch/static/img/pizza_veggie.png
Binary files 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
--- /dev/null
+++ b/addons/lunch/static/img/salmon_sushi.png
Binary files differ
diff --git a/addons/lunch/static/img/temaki.png b/addons/lunch/static/img/temaki.png
new file mode 100644
index 00000000000..8f47e7c035a
--- /dev/null
+++ b/addons/lunch/static/img/temaki.png
Binary files 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
--- /dev/null
+++ b/addons/lunch/static/img/tuna_sandwich.png
Binary files 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..769aafc5b21
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_kanban_controller.js
@@ -0,0 +1,213 @@
+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 Domain = require('web.Domain');
+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_product: '_onAddProduct',
+ change_location: '_onLocationChanged',
+ 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;
+ this.updated = false;
+ this.widgetData = null;
+ 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',
+ });
+ },
+ _fetchWidgetData: function () {
+ var self = this;
+
+ return this._rpc({
+ route: '/lunch/infos',
+ params: {
+ user_id: this.userId,
+ },
+ }).then(function (data) {
+ self.widgetData = data;
+ self.model._updateLocation(data.user_location[0]);
+ });
+ },
+ _onAddProduct: function (ev) {
+ var self = this;
+ ev.stopPropagation();
+
+ this._rpc({
+ model: 'lunch.order',
+ method: 'update_quantity',
+ args: [[ev.data.lineId], 1],
+ }).then(function () {
+ self.reload();
+ });
+ },
+ _onEditOrder: function (ev) {
+ ev.stopPropagation();
+
+ this.editMode = true;
+ this.reload();
+ },
+ _onLocationChanged: function (ev) {
+ var self = this;
+
+ ev.stopPropagation();
+
+ this._rpc({
+ route: '/lunch/user_location_set',
+ params: {
+ user_id: this.userId,
+ location_id: ev.data.locationId,
+ },
+ }).then(function () {
+ self.model._updateLocation(ev.data.locationId).then(function () {
+ self.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.temp',
+ type: 'ir.actions.act_window',
+ views: [[false, 'form']],
+ target: 'new',
+ context: _.extend(ctx, {default_product_id: ev.data.productId, line_id: ev.data.lineId || false}),
+ }, 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(_t("Not enough money in your wallet"));
+ self.reload();
+ }
+ });
+ },
+ _onRemoveProduct: function (ev) {
+ var self = this;
+ ev.stopPropagation();
+
+ this._rpc({
+ model: 'lunch.order',
+ 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();
+
+ var self = this;
+
+ this.userId = ev.data.userId;
+ this.model._updateUser(ev.data.userId).then(function () {
+ self.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': _t('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._fetchWidgetData().then(function () {
+ if (self.widget) {
+ self.widget.destroy();
+ }
+ self.widgetData.wallet = parseFloat(self.widgetData.wallet).toFixed(2);
+ self.widget = new LunchKanbanWidget(self, _.extend(self.widgetData, {edit: self.editMode}));
+ self.widget.insertBefore(self.$('.o_kanban_view'));
+ });
+ return this._super.apply(self, 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..c3a5bc87c02
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_kanban_model.js
@@ -0,0 +1,82 @@
+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({
+ init: function () {
+ this.locationId = false;
+ this.userId = false;
+
+ return this._super.apply(this, arguments);
+ },
+ load: function() {
+ var self = this;
+ var args = arguments;
+ var _super = this._super;
+
+ return this._getUserLocation().then(function (locationId) {
+ var params = args[0];
+ self.locationId = locationId;
+ self._addOrUpdate(params.domain, ['is_available_at', 'in', [locationId]]);
+
+ return _super.apply(self, args);
+ });
+ },
+ reload: function (id, options) {
+ var domain = options.domain || this.localData[id].domain;
+
+ this._addOrUpdate(domain, ['is_available_at', 'in', [this.locationId]]);
+ options = _.extend(options, {domain: domain});
+
+ return this._super.apply(this, arguments);
+ },
+
+ _addOrUpdate: function (domain, subDomain) {
+ var key = subDomain[0];
+ var index = _.findIndex(domain, function (val) {
+ return val[0] === key;
+ });
+
+ if (index < 0) {
+ domain.push(subDomain);
+ } else {
+ domain[index] = subDomain;
+ }
+
+ return domain;
+ },
+ _getUserLocation: function () {
+ return this._rpc({
+ route: '/lunch/user_location_get',
+ params: {
+ user_id: this.userId,
+ },
+ });
+ },
+ _updateLocation: function (locationId) {
+ this.locationId = locationId;
+ return $.when();
+ },
+ _updateUser: function (userId) {
+ var self = this;
+
+ this.userId = userId;
+
+ return this._getUserLocation().then(function (locationId) {
+ self.locationId = locationId;
+ });
+ }
+});
+
+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..a0d60588fae
--- /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', {productId: this.recordData.product_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..ef179152013
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_kanban_view.js
@@ -0,0 +1,41 @@
+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'),
+
+ /**
+ * @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..5c3955670d9
--- /dev/null
+++ b/addons/lunch/static/src/js/lunch_kanban_widget.js
@@ -0,0 +1,169 @@
+odoo.define('lunch.LunchKanbanWidget', function (require) {
+"use strict";
+
+var core = require('web.core');
+var dialogs = require('web.view_dialogs');
+var field_registry = require('web.field_registry');
+var session = require('web.session');
+var Widget = require('web.Widget');
+
+var _t = core._t;
+var qweb = core.qweb;
+var FieldMany2One = field_registry.get('many2one');
+
+var LunchKanbanWidget = Widget.extend({
+ template: 'LunchKanbanWidget',
+ custom_events: {
+ 'field_changed': '_onFieldChanged',
+ },
+ events: {
+ 'click .o_add_money': '_onAddMoney',
+ 'click .o_add_product': '_onAddProduct',
+ 'click .o_lunch_widget_order_button': '_onOrderNow',
+ 'click .o_remove_product': '_onRemoveProduct',
+ 'click .o_lunch_widget_save': '_onSaveOrder',
+ 'click .o_lunch_widget_unlink': '_onUnlinkOrder',
+ 'click .o_lunch_open_wizard': '_onLunchOpenWizard',
+ },
+
+ init: function (parent, params) {
+ var self = this;
+ this._super.apply(this, arguments);
+
+ this.is_manager = params.is_manager || false;
+ this.userimage = params.userimage || '';
+ this.username = params.username || '';
+
+ this.lunchUserField = null;
+
+ this.group_portal_id = undefined;
+
+ self._rpc({
+ model: 'ir.model.data',
+ method: 'xmlid_to_res_id',
+ kwargs: {xmlid: 'base.group_portal'},
+ }).then(function (id) {
+ self.group_portal_id = id;
+ });
+
+ if (this.is_manager) {
+ this.lunchUserField = this._createMany2One('users', 'res.users', this.username, function (params) { return [['groups_id', 'not in', [self.group_portal_id]]]; });
+ }
+
+ this.locations = params.locations || [];
+ this.userLocation = params.user_location[1] || '';
+
+ this.lunchLocationField = this._createMany2One('locations', 'lunch.location', this.userLocation);
+
+ this.wallet = params.wallet || 0;
+ this.raw_state = params.raw_state || 'new';
+ this.state = params.state || _t('To Order');
+ this.lines = params.lines || [];
+ this.total = params.total || 0;
+
+ this.alerts = params.alerts || [];
+
+ this.currency = params.currency || session.get_currency(session.company_currency_id);
+ },
+ renderElement: function () {
+ var self = this;
+ this._super.apply(this, arguments);
+ if (this.lunchUserField) {
+ this.lunchUserField.appendTo(this.$('.o_lunch_user_field'));
+ } else {
+ this.$('.o_lunch_user_field').text(this.username);
+ }
+ this.lunchLocationField.appendTo(this.$('.o_lunch_location_field'));
+ },
+
+ _createMany2One: function (name, model, value, domain, context) {
+ var fields = {};
+ fields[name] = {type: 'many2one', relation: model, string: name};
+ var data = {};
+ data[name] = {data: {display_name: value}};
+
+ var record = {
+ id: name,
+ res_id: 1,
+ model: 'dummy',
+ fields: fields,
+ fieldsInfo: {
+ default: fields,
+ },
+ data: data,
+ getDomain: domain || function (params) { return []; },
+ getContext: context || function (params) { return {}; },
+ };
+ var options = {
+ mode: 'edit',
+ noOpen: true,
+ attrs: {
+ can_create: false,
+ can_write: false,
+ }
+ };
+ return new LunchMany2One(this, name, record, options);
+ },
+ _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')});
+ },
+ _onOrderNow: function (ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ this.trigger_up('order_now', {});
+ },
+ _onLunchOpenWizard: function (ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ var target = $(ev.currentTarget);
+ this.trigger_up('open_wizard', {productId: target.data('product-id'), lineId: target.data('id')});
+ },
+ _onFieldChanged: function (ev) {
+ ev.stopPropagation();
+
+ if (ev.data.dataPointID === 'users') {
+ this.trigger_up('change_user', {userId: ev.data.changes.users.id});
+ } else if (ev.data.dataPointID === 'locations') {
+ this.trigger_up('change_location', {locationId: ev.data.changes.locations.id});
+ }
+ },
+ _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', {});
+ },
+});
+
+var LunchMany2One = FieldMany2One.extend({
+ start: function () {
+ this.$el.addClass('w-100');
+ return this._super.apply(this, arguments);
+ }
+});
+
+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..95a02c788da
--- /dev/null
+++ b/addons/lunch/static/src/scss/lunch_kanban.scss
@@ -0,0 +1,63 @@
+.o_list_view {
+ .o_data_row {
+ .o_text_overflow {
+ @include o-text-overflow(table-cell);
+ }
+ }
+}
+
+.o_lunch_kanban {
+ .o_lunch_purple {
+ color: $o-brand-odoo;
+ }
+
+ .o_lunch_widget {
+ display: flex;
+ border-bottom: 1px solid #CED4DA;
+ background-color: white;
+ min-height: 90px;
+ max-height: 33vh;
+ overflow-y: auto;
+
+ .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;
+ }
+
+ .o_lunch_open_wizard {
+ cursor: pointer;
+ &:hover {
+ font-weight: bolder;
+ }
+ }
+ }
+ }
+}
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..d9ceabe6e6e
--- /dev/null
+++ b/addons/lunch/static/src/xml/lunch_kanban.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates>
+ <div t-name="LunchKanbanWidget">
+ <div class="o_lunch_widget">
+ <div class="o_lunch_widget_info d-flex flex-row">
+ <div>
+ <img class="rounded-circle" t-attf-src="{{ widget.userimage }}"/>
+ </div>
+ <div class="w-50 ml-2 py-3">
+ <div class="o_lunch_user_field"/>
+ <div class="o_lunch_location_field"/>
+ <div class="d-flex flex-row">
+ <span class="flex-grow-1">Your Account</span>
+ <t t-call="currency_field">
+ <t t-set="value" t-value="widget.wallet"/>
+ <t t-set="currency" t-value="widget.currency"/>
+ </t>
+ </div>
+ </div>
+ </div>
+ <div class="o_lunch_widget_info">
+ <t t-if="!_.isEmpty(widget.lines)">
+ <t t-if="widget.raw_state == 'ordered'">
+ <t t-set="state_class" t-value="'o_lunch_ordered'"/>
+ </t>
+ <t t-else="widget.raw_state == 'confirmed'">
+ <t t-set="state_class" t-value="'o_lunch_confirmed'"/>
+ </t>
+ <div class="d-flex flex-row">
+ <div class="flex-grow-1">
+ <span class="o_lunch_widget_title">Your order</span>
+ <a href="#"><i t-if="widget.raw_state != 'confirmed'" class="fa fa-trash o_lunch_widget_unlink"/></a>
+ </div>
+ <div t-if="widget.raw_state != 'new'" t-esc="widget.state" t-attf-class="flex-grow-0 {{ state_class }}"/>
+ </div>
+ <div t-foreach="widget.lines" t-as="line">
+ <div class="d-flex flex-row">
+ <div class="flex-grow-0">
+ <a class="o_remove_product" t-if="widget.raw_state != 'confirmed'" t-attf-data-id="{{ line.id }}" href="#"><i class="fa fa-minus-circle"/></a>
+ <span t-esc="line.quantity"/>
+ <a class="o_add_product" t-if="widget.raw_state != 'confirmed'" t-attf-data-id="{{ line.id }}" href="#"><i class="fa fa-plus-circle"/></a>
+ </div>
+ <div class="flex-grow-1 pl-2">
+ <a t-esc="line.product[1]" class="o_lunch_open_wizard" t-attf-data-product-id="{{ line.product[0] }}" t-attf-data-id="{{ line.id }}"/>
+ </div>
+ <div class="flex-grow-0">
+ <t t-call="currency_field">
+ <t t-set="value" t-value="line.product[2]"/>
+ <t t-set="currency" t-value="widget.currency"/>
+ </t>
+ </div>
+ </div>
+ <div t-foreach="line.toppings" t-as="topping" class="d-flex flex-row">
+ <div class="flex-grow-1 pl-5">
+ <span>+ <t t-esc="topping[0]"/></span>
+ </div>
+ <div class="flex-grow-0">
+ <t t-call="currency_field">
+ <t t-set="value" t-value="topping[1]"/>
+ <t t-set="currency" t-value="widget.currency"/>
+ </t>
+ </div>
+ </div>
+ <span t-if="line.note" t-esc="line.note" class="text-muted pl-5"/>
+ </div>
+ </t>
+ </div>
+ <div class="o_lunch_widget_info">
+ <t t-if="!_.isEmpty(widget.lines) &amp;&amp; widget.raw_state == 'new'">
+ <div class="d-flex flex-row">
+ <div class="o_lunch_widget_title flex-grow-1">
+ Total
+ </div>
+ <div class="flex-grow-0 pr-1">
+ <t t-call="currency_field">
+ <t t-set="value" t-value="widget.total"/>
+ <t t-set="currency" t-value="widget.currency"/>
+ </t>
+ </div>
+ </div>
+ <button t-if="widget.raw_state == 'new'" class="btn btn-primary o_lunch_widget_order_button">ORDER NOW</button>
+ </t>
+ </div>
+ </div>
+ <t t-foreach="widget.alerts" t-as="alert">
+ <div class="alert alert-danger" role="alert">
+ <t t-raw="alert.message"/> <!-- alert.message is coming from a fields.Html so it should be safe -->
+ </div>
+ </t>
+ </div>
+
+ <t t-name="currency_field">
+ <t t-js="ctx">
+ ctx.value = _.str.sprintf('%.2f', parseFloat(ctx.value));
+ </t>
+ <t t-if="currency">
+ <t t-if="currency.position == 'after'">
+ <t t-esc="value"/><t t-esc="currency.symbol"/>
+ </t>
+ <t t-else="">
+ <t t-esc="currency.symbol"/><t t-esc="value"/>
+ </t>
+ </t>
+ <t t-else="">
+ <t t-esc="value"/>
+ </t>
+ </t>
+
+ <div t-name="lunch.LunchPaymentDialog">
+ <span t-esc="widget.message"/>
+ </div>
+</templates>
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: '<form>' +
- '<sheet>' +
- '<field name="previous_order_widget" widget="previous_order"/>' +
- '<field name="order_line_ids"/>' +
- '</sheet>' +
- '</form>',
- data: this.data,
- model: 'order',
- archs: {
- "order_line,false,list": '<tree string="Orders">' +
- '<field name="product_id"/>' +
- '<field name="note"/>' +
- '<field name="price"/>' +
- '</tree>',
- },
- });
-
- 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: '<form>' +
- '<sheet>' +
- '<field name="previous_order_widget" widget="previous_order"/>' +
- '<field name="order_line_ids"/>' +
- '</sheet>' +
- '</form>',
- 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": '<tree string="Orders">' +
- '<field name="product_id"/>' +
- '<field name="note"/>' +
- '<field name="price"/>' +
- '</tree>',
- },
- });
-
- 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..38d27fafda7
--- /dev/null
+++ b/addons/lunch/static/tests/test_lunch_kanban.js
@@ -0,0 +1,223 @@
+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'},
+ is_available_at: {string: 'available_location_ids', type: 'integer'},
+ },
+ records: [
+ {id: 1, name: 'Tuna sandwich', price: 3.0, is_available_at: 1},
+ ]
+ },
+ };
+ },
+}, function () {
+ QUnit.module('LunchKanbanView');
+
+ QUnit.test('Simple rendering of LunchKanbanView', function (assert) {
+ assert.expect(7);
+
+ var lunchKanban = createView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: '<kanban><templates><t t-name="kanban-box">' +
+ '<div>' +
+ '</div>' +
+ '</t></templates></kanban>',
+ mockRPC: function(route, args) {
+ if (route === '/lunch/infos') {
+ return $.when({
+ order: 1,
+ wallet: 20,
+ username: 'Marc Demo',
+ userimage: '',
+ is_manager: false,
+ users: [
+ {id: 1, name: 'Mitchell Admin'},
+ {id: 2, name: 'Marc Demo'},
+ ],
+ total: 7.4,
+ state: 'new',
+ lines: [
+ {id: 1, product: ['Pizza Italiana', 7.4], toppings: [], quantity: 1.0, price: 7.4}
+ ],
+ alerts: [],
+ locations: ['hello', 'hallo'],
+ user_location: [1, 'hello'],
+ });
+ } else if (route === '/lunch/user_location_get') {
+ return $.when(1);
+ } else if (route === '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id') {
+ return $.when();
+ } 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');
+ // 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.40', 'total should be of 7.40');
+ // 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(9);
+
+ var state = 'new';
+
+ var lunchKanban = createView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: '<kanban><templates><t t-name="kanban-box">' +
+ '<div>' +
+ '</div>' +
+ '</t></templates></kanban>',
+ 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,
+ raw_state: state,
+ state: 'New',
+ lines: [
+ {id: 1, product: ['Pizza Italiana', 7.4], toppings: [], quantity: 1.0, price: 7.4}
+ ],
+ alerts: [],
+ locations: ['hello', 'hallo'],
+ user_location: [1, 'hello'],
+ });
+ } else if (route === '/lunch/payment_message') {
+ return $.when({message: 'Hello'});
+ } else if (route === '/lunch/pay') {
+ return $.when(true);
+ } else if (route === '/lunch/user_location_get') {
+ return $.when(1);
+ } else if (args.method === 'update_quantity') {
+ assert.step(args.args);
+ return $.when();
+ } else if (route === '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id') {
+ return $.when();
+ } else if (route.startsWith('data:image/png;base64,')) {
+ return $.when();
+ }
+ return this._super(route, args);
+ },
+ });
+
+ 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');
+ // buttons
+ assert.strictEqual(lunchKanban.$('.o_remove_product').length, 1, 'button to remove product should be shown');
+ assert.strictEqual(lunchKanban.$('.o_add_product').length, 1, 'button to add product should be shown');
+ state = 'confirmed';
+ lunchKanban.reload();
+ // state is updated
+ assert.strictEqual(lunchKanban.$('.o_lunch_confirmed').length, 1, 'state should be shown as confirmed');
+ // Buttons not shown anymore
+ assert.strictEqual(lunchKanban.$('.o_remove_product').length, 0, 'button to remove product should not be shown');
+ assert.strictEqual(lunchKanban.$('.o_add_product').length, 0, 'button to add product should not be shown');
+
+
+ assert.verifySteps([
+ [[1], 1],
+ [[1], -1],
+ ]);
+
+ lunchKanban.destroy();
+ });
+
+ QUnit.test('Manager interactions', function (assert) {
+ assert.expect(1);
+
+ var lunchKanban = createView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: '<kanban><templates><t t-name="kanban-box">' +
+ '<div>' +
+ '</div>' +
+ '</t></templates></kanban>',
+ 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,
+ raw_state: 'new',
+ state: 'New',
+ lines: [
+ {id: 1, product: ['Pizza Italiana', 7.4], toppings: [], quantity: 1.0, price: 7.4}
+ ],
+ alerts: [],
+ locations: ['hello', 'hallo'],
+ user_location: [1, 'hello'],
+ });
+ } else if (route === '/lunch/user_location_get') {
+ return $.when(1);
+ } else if (route === '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id') {
+ return $.when();
+ } else if (route.startsWith('data:image/png;base64,')) {
+ return $.when();
+ }
+ return this._super(route, args);
+ },
+ });
+
+ var select = lunchKanban.$('div.o_lunch_user_field div.o_field_widget.o_field_many2one');
+
+ assert.strictEqual(select.length, 1, 'There should be a user selection field');
+
+ lunchKanban.destroy();
+ });
+});
+
+});
diff --git a/addons/lunch/tests/__init__.py b/addons/lunch/tests/__init__.py
index f9d697d7b2c..27f923780e1 100644
--- a/addons/lunch/tests/__init__.py
+++ b/addons/lunch/tests/__init__.py
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
-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..61e75d3dce5
--- /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.location_office_1 = self.env['lunch.location'].create({
+ 'name' : 'Farm 1',
+ })
+
+ self.location_office_2 = self.env['lunch.location'].create({
+ 'name': 'Farm 2',
+ })
+
+ 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_time': 11,
+ 'available_location_ids': [
+ (6, 0, [self.location_office_1.id, self.location_office_2.id])
+ ],
+ })
+
+ 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',
+ 'available_location_ids': [
+ (6, 0, [self.location_office_1.id, self.location_office_2.id])
+ ],
+ })
+
+ 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.topping'].create({
+ 'name': 'Olives',
+ 'price': 0.3,
+ 'category_id': self.category_pizza.id,
+ })
+
+ self.env['lunch.cashmove'].create({
+ 'amount': 100,
+ })
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..bc1da3bb880
--- /dev/null
+++ b/addons/lunch/tests/test_supplier.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import pytz
+
+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_1am = datetime(2018, 10, 29, 1, 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_1am, True), (self.monday_10am, True),
+ (self.monday_1pm, True), (self.monday_8pm, True),
+ (self.saturday_3am, False), (self.saturday_10am, False),
+ (self.saturday_1pm, False), (self.saturday_8pm, False)]
+
+ for value, result in tests:
+ with patch.object(fields.Datetime, 'now', return_value=value) as _:
+ assert self.supplier_pizza_inn.available_today == result,\
+ 'supplier pizza inn should %s considered available on %s' % ('be' if result else 'not be', value)
+
+ self.env['lunch.supplier'].invalidate_cache(['available_today'], [self.supplier_pizza_inn.id])
+
+ def test_search_available_today(self):
+ '''
+ This test checks that _search_available_today returns a valid domain
+ '''
+ Supplier = self.env['lunch.supplier']
+
+ tests = [(self.monday_1am, 1.0, 'monday'), (self.monday_10am, 10.0, 'monday'),
+ (self.monday_1pm, 13.0, 'monday'), (self.monday_8pm, 20.0, 'monday'),
+ (self.saturday_3am, 3.0, 'saturday'), (self.saturday_10am, 10.0, 'saturday'),
+ (self.saturday_1pm, 13.0, 'saturday'), (self.saturday_8pm, 20.0, 'saturday')]
+
+ # It should return an empty domain if we compare to values other than datetime
+ assert Supplier._search_available_today('>', 7) == []
+ assert Supplier._search_available_today('>', True) == []
+
+ for value, rvalue, dayname in tests:
+ with patch.object(fields.Datetime, 'now', return_value=value) as _:
+ assert Supplier._search_available_today('=', True) == ['&', '|', ('recurrency_end_date', '=', False),
+ ('recurrency_end_date', '>', value.replace(tzinfo=pytz.UTC).astimezone(pytz.timezone(self.env.user.tz))),
+ ('recurrency_%s' % (dayname), '=', True)],\
+ 'Wrong domain generated for values (%s, %s)' % (value, rvalue)
+
+ with patch.object(fields.Datetime, 'now', return_value=self.monday_10am) as _:
+ assert self.supplier_pizza_inn in Supplier.search([('available_today', '=', True)])
+
+ def test_auto_email_send(self):
+ with patch.object(fields.Datetime, 'now', return_value=self.monday_1pm) as _:
+ with patch.object(fields.Date, 'today', return_value=self.monday_1pm.date()) as _:
+ with patch.object(fields.Date, 'context_today', return_value=self.monday_1pm.date()) as _:
+ line = self.env['lunch.order'].create({
+ 'product_id': self.product_pizza.id,
+ 'date': self.monday_1pm.date()
+ })
+
+ line.action_order()
+ assert line.state == 'ordered'
+
+ self.supplier_pizza_inn._auto_email_send()
+
+ assert line.state == 'confirmed'
+
+ line = self.env['lunch.order'].create({
+ 'product_id': self.product_pizza.id,
+ 'topping_ids_1': [(6, 0, [self.topping_olives.id])],
+ 'date': self.monday_1pm.date()
+ })
+ line2 = self.env['lunch.order'].create({
+ 'product_id': self.product_sandwich_tuna.id,
+ 'date': self.monday_1pm.date()
+ })
+
+ (line | line2).action_order()
+ assert line.state == 'ordered'
+ assert line2.state == 'ordered'
+
+ self.supplier_pizza_inn._auto_email_send()
+
+ assert line.state == 'confirmed'
+ assert line2.state == 'ordered'
+
+ line_1 = self.env['lunch.order'].create({
+ 'product_id': self.product_pizza.id,
+ 'quantity': 2,
+ 'date': self.monday_1pm.date()
+ })
+
+ line_2 = self.env['lunch.order'].create({
+ 'product_id': self.product_pizza.id,
+ 'topping_ids_1': [(6, 0, [self.topping_olives.id])],
+ 'date': self.monday_1pm.date()
+ })
+
+ line_3 = self.env['lunch.order'].create({
+ 'product_id': self.product_sandwich_tuna.id,
+ 'quantity': 2,
+ 'date': self.monday_1pm.date()
+ })
+
+ (line_1 | line_2 | line_3).action_order()
+
+ assert all(line.state == 'ordered' for line in [line_1, line_2, line_3])
+
+ self.supplier_pizza_inn._auto_email_send()
diff --git a/addons/lunch/views/lunch_alert_views.xml b/addons/lunch/views/lunch_alert_views.xml
index bb6d807a9ff..034a39e8f91 100644
--- a/addons/lunch/views/lunch_alert_views.xml
+++ b/addons/lunch/views/lunch_alert_views.xml
@@ -6,7 +6,7 @@
<field name="arch" type="xml">
<search string="Search">
<field name="message"/>
- <filter name="inactive" string="Archived" domain="[('active','=',False)]"/>
+ <filter name="inactive" string="Currently inactive" domain="[('available_today','=',False)]"/>
</search>
</field>
</record>
@@ -16,10 +16,9 @@
<field name="model">lunch.alert</field>
<field name="arch" type="xml">
<tree>
+ <field name="active"/>
<field name="message"/>
- <field name="alert_type"/>
- <field name='start_hour' widget='float_time'/>
- <field name='end_hour' widget='float_time'/>
+ <field name="available_today"/>
</tree>
</field>
</record>
@@ -28,7 +27,7 @@
<field name="name">lunch.alert.form</field>
<field name="model">lunch.alert</field>
<field name="arch" type="xml">
- <form>
+ <form string="alert form">
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object"
@@ -38,97 +37,33 @@
</button>
</div>
<group>
- <group name="schedule" string="Schedule">
- <group colspan="4">
- <field name="alert_type"/>
- <field name="specific_day"
- attrs="{'invisible': [('alert_type','!=','specific')], 'required':[('alert_type','=','specific')]}"/>
- <field name='start_hour' widget='float_time' />
- <field name='end_hour' widget='float_time' />
- </group>
- <group name="weekdays" col="4" attrs="{'invisible': [('alert_type','!=','week')]}">
- <field name="monday"/>
- <field name="tuesday"/>
- <field name="wednesday"/>
- <field name="thursday"/>
- <field name="friday"/>
- <field name="saturday"/>
- <field name="sunday"/>
- </group>
+ <group>
+ <field name="location_ids" widget="many2many_tags"/>
+ <field name="until"/>
</group>
- <group name="vendor" string="Vendor">
- <group name="partner" colspan="4">
- <field name="partner_id"/>
- <field name="display" invisible="True"/>
- </group>
- <div class="oe_grey" colspan="4"
- attrs="{'invisible': ['|', ('partner_id', '=', False), ('display', '=', True)]}">
- Products from this vendor can <strong>not</strong> be ordered at that moment.
- </div>
- <div class="oe_grey" colspan="4"
- attrs="{'invisible': ['|', ('partner_id', '=', False), ('display', '=', False)]}">
- Products from this vendor can be ordered at that moment.
- </div>
+ <group>
+ <field name="recurrency_monday"/>
+ <field name="recurrency_tuesday"/>
+ <field name="recurrency_wednesday"/>
+ <field name="recurrency_thursday"/>
+ <field name="recurrency_friday"/>
+ <field name="recurrency_saturday"/>
+ <field name="recurrency_sunday"/>
+ </group>
+ <group>
+ <field name="message"/>
</group>
- </group>
- <group string='Message'>
- <field name='message' nolabel='1' placeholder="Write the message you want to display during the defined period..."/>
</group>
</sheet>
</form>
</field>
</record>
- <record id="view_lunch_alert_kanban" model="ir.ui.view">
- <field name="name">lunch.alert.kanban</field>
- <field name="model">lunch.alert</field>
- <field name="arch" type="xml">
- <kanban class="o_kanban_mobile">
- <field name="message"/>
- <field name="alert_type"/>
- <field name='start_hour'/>
- <field name='end_hour'/>
- <templates>
- <t t-name="kanban-box">
- <div t-attf-class="oe_kanban_global_click">
- <div class="o_kanban_record_top">
- <strong class="o_kanban_record_title"><t t-esc="record.message.value"/></strong>
- </div>
- <div class="row mb4">
- <div class="col-6">
- Between: <t t-esc="record.start_hour.value"/> -
- <t t-esc="record.end_hour.value"/>
- </div>
- <div class="col-6">
- <span class="badge badge-pill float-right">
- <t t-esc="record.alert_type.value"/>
- </span>
- </div>
- </div>
- </div>
- </t>
- </templates>
- </kanban>
- </field>
- </record>
-
<record id="lunch_alert_action" model="ir.actions.act_window">
- <field name="name">Alerts</field>
+ <field name="name">Lunch Alerts</field>
<field name="res_model">lunch.alert</field>
- <field name="view_mode">tree,kanban,form</field>
<field name="search_view_id" ref="lunch_alert_view_search"/>
- <field name="help" type="html">
- <p class="o_view_nocontent_smiling_face">
- Create a new lunch alert
- </p><p>
- 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.
- </p><p>
- Example:<br/>
- Recurency: Everyday,<br/>
- Time interval: from 00h00 am to 11h59 pm,<br/>
- Message: "You must order before 10h30 am".
- </p>
- </field>
+ <field name="view_mode">tree,form</field>
+ <field name="view_id" ref="lunch_alert_view_tree"/>
</record>
</odoo>
diff --git a/addons/lunch/views/lunch_cashmove_views.xml b/addons/lunch/views/lunch_cashmove_views.xml
index b714205587a..b970cfb10ae 100644
--- a/addons/lunch/views/lunch_cashmove_views.xml
+++ b/addons/lunch/views/lunch_cashmove_views.xml
@@ -7,49 +7,22 @@
<search string="lunch employee payment">
<field name="description"/>
<field name="user_id"/>
- <filter name='is_payment' string="Payment" domain="[('state','=','payment')]"/>
- <separator/>
<filter name='is_mine_group' string="My Account grouped" domain="[('user_id','=',uid)]" context="{'group_by':'user_id'}"/>
<filter name="group_by_user" string="By User" context="{'group_by':'user_id'}"/>
</search>
</field>
</record>
- <record id="lunch_cashmove_view_search_2" model="ir.ui.view">
- <field name='name'>lunch.cashmove.search</field>
- <field name='model'>lunch.cashmove</field>
- <field name='arch' type='xml'>
- <search string="lunch cashmove">
- <field name="description"/>
- <field name="user_id"/>
- <group expand="0" string="Group By">
- <filter name='group_by_user' string="By Employee" context="{'group_by':'user_id'}"/>
- </group>
- </search>
- </field>
- </record>
-
<record id="lunch_cashmove_view_tree" model="ir.ui.view">
<field name="name">lunch.cashmove.tree</field>
<field name="model">lunch.cashmove</field>
<field name="arch" type="xml">
<tree string="cashmove tree">
+ <field name="currency_id" invisible="1"/>
<field name="date"/>
<field name="user_id"/>
<field name="description"/>
- <field name="amount" sum="Total"/>
- </tree>
- </field>
- </record>
-
- <record id="lunch_cashmove_view_tree_2" model="ir.ui.view">
- <field name="name">lunch.cashmove.tree</field>
- <field name="model">lunch.cashmove</field>
- <field name="arch" type="xml">
- <tree string="cashmove tree" create='false'>
- <field name="date"/>
- <field name="description"/>
- <field name="amount" sum="Total"/>
+ <field name="amount" sum="Total" widget="monetary"/>
</tree>
</field>
</record>
@@ -61,9 +34,10 @@
<form string="cashmove form">
<sheet>
<group>
+ <field name="currency_id" invisible="1"/>
<field name="user_id" required="1"/>
<field name="date"/>
- <field name="amount"/>
+ <field name="amount" widget="monetary"/>
</group>
<label for='description'/>
<field name="description"/>
@@ -81,6 +55,7 @@
<field name="user_id"/>
<field name="description"/>
<field name="amount"/>
+ <field name="currency_id" invisible="1"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_global_click">
@@ -92,7 +67,7 @@
</div>
<div class="col-4 text-right">
<span class="badge badge-pill">
- <strong><i class="fa fa-money" role="img" aria-label="Amount" title="Amount"/> <field name="amount"/></strong>
+ <strong><i class="fa fa-money" role="img" aria-label="Amount" title="Amount"/> <field name="amount" widget="monetary"/></strong>
</span>
</div>
</div>
@@ -112,47 +87,11 @@
</field>
</record>
- <record id="lunch_cashmove_action_account" model="ir.actions.act_window">
- <field name="name">Your Account</field>
- <field name="res_model">lunch.cashmove</field>
- <field name="view_mode">tree</field>
- <field name="search_view_id" ref="lunch_cashmove_view_search"/>
- <field name="context">{"search_default_is_mine_group":1}</field>
- <field name="view_id" ref="lunch_cashmove_view_tree_2"/>
- <field name="help" type="html">
- <p class="o_view_nocontent_empty_folder">
- No cash move yet
- </p><p>
- Here you can see your cash moves.<br/>A cash move can either be an expense or a payment.
- An expense is automatically created when an order is received while a payment is a reimbursement to the company encoded by the manager.
- </p>
- </field>
- </record>
-
- <record id="lunch_cashmove_action_control_accounts" model="ir.actions.act_window">
- <field name="name">Control Accounts</field>
- <field name="res_model">lunch.cashmove</field>
- <field name="view_mode">tree,kanban,form</field>
- <field name="search_view_id" ref="lunch_cashmove_view_search_2"/>
- <field name="context">{"search_default_group_by_user":1}</field>
- <field name="view_id" ref="lunch_cashmove_view_tree"/>
- <field name="help" type="html">
- <p class="o_view_nocontent_smiling_face">
- Create a new payment
- </p><p>
- A cashmove can either be an expense or a payment.<br/>
- An expense is automatically created at the order receipt.<br/>
- A payment represents the employee reimbursement to the company.
- </p>
- </field>
- </record>
-
<record id="lunch_cashmove_action_payment" model="ir.actions.act_window">
<field name="name">Register Cash Moves</field>
<field name="res_model">lunch.cashmove</field>
<field name="view_mode">tree,kanban,form</field>
<field name="search_view_id" ref="lunch_cashmove_view_search"/>
- <field name="context">{"search_default_is_payment":1}</field>
<field name="view_id" ref="lunch_cashmove_view_tree"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
diff --git a/addons/lunch/views/lunch_location_views.xml b/addons/lunch/views/lunch_location_views.xml
new file mode 100644
index 00000000000..1a52393ab81
--- /dev/null
+++ b/addons/lunch/views/lunch_location_views.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="lunch_location_view_search" model="ir.ui.view">
+ <field name="name">lunch.location.view.search</field>
+ <field name="model">lunch.location</field>
+ <field name="arch" type="xml">
+ <search>
+ <field name="name"/>
+ <field name="address"/>
+ </search>
+ </field>
+ </record>
+
+ <record id="lunch_location_form_view" model="ir.ui.view">
+ <field name="name">lunch.location.view.form</field>
+ <field name="model">lunch.location</field>
+ <field name="arch" type="xml">
+ <form>
+ <sheet>
+ <group>
+ <group>
+ <field name="name"/>
+ <field name="address"/>
+ </group>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="lunch_location_tree_view" model="ir.ui.view">
+ <field name="name">lunch.location.view.form</field>
+ <field name="model">lunch.location</field>
+ <field name="arch" type="xml">
+ <tree>
+ <field name="name"/>
+ <field name="address"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="lunch_location_action" model="ir.actions.act_window">
+ <field name="name">Lunch Locations</field>
+ <field name="res_model">lunch.location</field>
+ <field name="view_mode">tree,form</field>
+ <field name="search_view_id" ref="lunch_location_view_search"/>
+ <field name="help" type="html">
+ <!-- TODO: better help message -->
+ <p class="o_view_nocontent_smiling_face">
+ To see some locations, create one using the create button
+ </p>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/lunch/views/lunch_orders_views.xml b/addons/lunch/views/lunch_orders_views.xml
index 8cd4658a8cb..db39dabbc29 100644
--- a/addons/lunch/views/lunch_orders_views.xml
+++ b/addons/lunch/views/lunch_orders_views.xml
@@ -1,92 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
- <record id="lunch_order_view_tree" model="ir.ui.view">
- <field name="name">lunch.order.tree</field>
- <field name="model">lunch.order</field>
- <field name="arch" type="xml">
- <tree string="Orders Tree">
- <field name="date"/>
- <field name="order_line_ids"/>
- <field name="state"/>
- <field name="total" sum="Total"/>
- </tree>
- </field>
- </record>
- <record id="lunch_order_view_form" model="ir.ui.view">
- <field name="name">lunch.order.form</field>
+ <record id="lunch_order_view_search" model="ir.ui.view">
+ <field name="name">lunch.order.search</field>
<field name="model">lunch.order</field>
<field name="arch" type="xml">
- <form string='Orders Form' class="o_lunch">
- <header>
- <button name="%(action_lunch_order_line_lucky)d" type="action" string="Feeling Lucky" class="oe_highlight"/>
- <field name='state' widget='statusbar' statusbar_visible='new,confirmed'/>
- </header>
- <sheet>
- <field name="balance_visible" invisible="1"/>
-
- <div class="oe_button_box" name="button_box">
- <button class="oe_stat_button"
- attrs="{'invisible': [('balance_visible', '=', False)]}"
- name="%(lunch_cashmove_action_account)d"
- icon="fa-money"
- type="action">
- <div class="o_field_widget o_stat_info">
- <span class="o_stat_value"><field name="cash_move_balance" widget="monetary"/></span>
- <span class="o_stat_text">Balance</span>
- </div>
- </button>
- </div>
-
- <group>
- <group>
- <field name='user_id' class="oe_inline" required='1'/>
- <field name='date'/>
- </group>
- <div>
- <div attrs="{'invisible': ['|',('state','!=','new'),('alerts','=',False)]}" class="alert alert-danger oe_edit_only" role="alert">
- <field name='alerts'/>
- </div>
- </div>
- </group>
-
- <div>
- <field name='previous_order_widget' widget='previous_order' class='o_lunch_last_orders'/>
- </div>
-
- <separator string='Select your order'/>
- <field name='order_line_ids'>
- <tree string='List' editable='bottom'>
- <field name='product_id' context="{'lunch_date': parent.date}"/>
- <field name='note' />
- <field name='price'/>
- <field name='supplier' invisible="1"/>
- <field name="state" invisible="1"/>
- </tree>
- <form string='Lunch Order Form'>
- <group>
- <field name='product_id' context="{'lunch_date': parent.date}"/>
- <field name='note' />
- <field name='price'/>
- </group>
- </form>
- </field>
- <group class='oe_subtotal_footer oe_right'>
- <field name='total' widget='monetary' options="{'currency_field': 'currency_id'}"/>
- </group>
- </sheet>
- </form>
- </field>
- </record>
-
- <record id="lunch_order_line_view_search" model="ir.ui.view">
- <field name="name">lunch.order.line.search</field>
- <field name="model">lunch.order.line</field>
- <field name="arch" type="xml">
<search string="Search">
+ <field name="name" string="Product" filter_domain="['|', ('name', 'ilike', self), ('note', 'ilike', self)]"/>
<filter name='is_mine' string="My Orders" domain="[('user_id','=',uid)]"/>
<separator/>
- <field name="name" string="Product" filter_domain="['|', ('name', 'ilike', self), ('note', 'ilike', self)]"/>
<filter name="not_confirmed" string="Not Received" domain="[('state','!=',('confirmed'))]"/>
<filter name="confirmed" string="Received" domain="[('state','=','confirmed')]"/>
<filter name="cancelled" string="Cancelled" domain="[('state','=','cancelled')]"/>
@@ -94,43 +16,43 @@
<filter name="date_filter" string="Date" date="date" default_period="today"/>
<field name="user_id"/>
<group expand="0" string="Group By">
- <filter name="group_by_order_id" string="By Order" context="{'group_by':'order_id'}"/>
- <filter name="group_by_supplier" string="By Vendor" context="{'group_by':'supplier'}"/>
+ <filter name="group_by_supplier" string="By Vendor" context="{'group_by':'supplier_id'}"/>
<filter name="group_by_date" string="Order Date" context="{'group_by':'date'}" help="Vendor Orders by Date"/>
</group>
</search>
</field>
</record>
- <record id="lunch_order_line_view_tree" model="ir.ui.view">
- <field name="name">lunch.order.line.tree</field>
- <field name="model">lunch.order.line</field>
+ <record id="lunch_order_view_tree" model="ir.ui.view">
+ <field name="name">lunch.order.tree</field>
+ <field name="model">lunch.order</field>
<field name="arch" type="xml">
- <tree string="Order lines Tree" create="false" edit="false">
+ <tree string="Order lines Tree" create="false" edit="false" decoration-muted="state == 'cancelled'">
<field name='date'/>
- <field name='supplier' invisible='1'/>
+ <field name='supplier_id' invisible='1'/>
<field name='product_id'/>
- <field name='note'/>
- <field name='state'/>
+ <field name="display_toppings" class="o_text_overflow"/>
+ <field name='note' class="o_text_overflow"/>
<field name='user_id'/>
- <field name='price' sum="Total" string="Price"/>
- <button name="order" string="Order" type="object" icon="fa-phone text-success" attrs="{'invisible': ['|',('state','=','confirmed'),('state','=','ordered')]}" groups="lunch.group_lunch_manager"/>
- <button name="confirm" string="Receive" type="object" icon="fa-check text-success" attrs="{'invisible': [('state','!=','ordered')]}" groups="lunch.group_lunch_manager"/>
- <button name="cancel" string="Cancel" type="object" icon="fa-times-circle text-danger" attrs="{'invisible': [('state','=','cancelled')]}" groups="lunch.group_lunch_manager"/>
+ <field name="currency_id" invisible="1"/>
+ <field name='price' sum="Total" string="Price" widget="monetary"/>
+ <button name="action_confirm" string="Confirm" type="object" icon="fa-phone text-success" attrs="{'invisible': [('state', '!=', 'ordered')]}" groups="lunch.group_lunch_manager"/>
+ <button name="action_cancel" string="Cancel" type="object" icon="fa-times-circle text-danger" attrs="{'invisible': [('state', 'in', ['cancelled', 'confirmed'])]}" groups="lunch.group_lunch_manager"/>
+ <field name='state'/>
</tree>
</field>
</record>
- <record id='lunch_order_line_view_kanban' model='ir.ui.view'>
- <field name="name">lunch.order.line.kanban</field>
- <field name="model">lunch.order.line</field>
+ <record id='lunch_order_view_kanban' model='ir.ui.view'>
+ <field name="name">lunch.order.kanban</field>
+ <field name="model">lunch.order</field>
<field name="arch" type="xml">
- <kanban>
- <field name='product_id'/>
- <field name='note'/>
- <field name='state'/>
- <field name='user_id'/>
- <field name='date'/>
+ <kanban create="false" edit="false">
+ <field name="product_id"/>
+ <field name="note"/>
+ <field name="state"/>
+ <field name="user_id"/>
+ <field name="date"/>
<field name="price"/>
<templates>
<t t-name="kanban-box">
@@ -175,17 +97,32 @@
</field>
</record>
- <record id="lunch_order_action_form" model="ir.actions.act_window">
- <field name="name">New Order</field>
- <field name="res_model">lunch.order</field>
- <field name="view_mode">form</field>
+ <record id="lunch_order_view_pivot" model="ir.ui.view">
+ <field name="name">lunch.order.pivot</field>
+ <field name="model">lunch.order</field>
+ <field name="arch" type="xml">
+ <pivot>
+ <field name="date" type="col"/>
+ <field name="supplier_id" type="row"/>
+ </pivot>
+ </field>
+ </record>
+
+ <record id="lunch_order_view_graph" model="ir.ui.view">
+ <field name="name">lunch.order.graph</field>
+ <field name="model">lunch.order</field>
+ <field name="arch" type="xml">
+ <graph>
+ <field name="product_id" type="col"/>
+ </graph>
+ </field>
</record>
- <record id="lunch_order_line_action" model="ir.actions.act_window">
+ <record id="lunch_order_action" model="ir.actions.act_window">
<field name="name">Your Orders</field>
- <field name="res_model">lunch.order.line</field>
- <field name="view_mode">tree,kanban,form</field>
- <field name="search_view_id" ref="lunch_order_line_view_search"/>
+ <field name="res_model">lunch.order</field>
+ <field name="view_mode">tree,kanban,pivot</field>
+ <field name="search_view_id" ref="lunch_order_view_search"/>
<field name="context">{"search_default_is_mine":1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
@@ -196,11 +133,11 @@
</field>
</record>
- <record id="lunch_order_line_action_by_supplier" model="ir.actions.act_window">
+ <record id="lunch_order_action_by_supplier" model="ir.actions.act_window">
<field name="name">Orders by Vendor</field>
- <field name="res_model">lunch.order.line</field>
+ <field name="res_model">lunch.order</field>
<field name="view_mode">tree,kanban</field>
- <field name="search_view_id" ref="lunch_order_line_view_search"/>
+ <field name="search_view_id" ref="lunch_order_view_search"/>
<field name="context">{"search_default_group_by_supplier":1, "search_default_date_filter":1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
@@ -215,12 +152,12 @@
</field>
</record>
- <record id="lunch_order_line_action_control_suppliers" model="ir.actions.act_window">
+ <record id="lunch_order_action_control_suppliers" model="ir.actions.act_window">
<field name="name">Control Vendors</field>
- <field name="res_model">lunch.order.line</field>
- <field name="view_mode">tree,kanban</field>
- <field name="search_view_id" ref="lunch_order_line_view_search"/>
- <field name="context">{"search_default_group_by_date":1, "search_default_group_by_supplier":1}</field>
+ <field name="res_model">lunch.order</field>
+ <field name="view_mode">tree,kanban,pivot</field>
+ <field name="search_view_id" ref="lunch_order_view_search"/>
+ <field name="context">{"search_default_group_by_supplier":1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
No lunch order yet
diff --git a/addons/lunch/views/lunch_product_views.xml b/addons/lunch/views/lunch_product_views.xml
index e7647207d6d..43db92bb2ac 100644
--- a/addons/lunch/views/lunch_product_views.xml
+++ b/addons/lunch/views/lunch_product_views.xml
@@ -7,12 +7,12 @@
<search string="Product Search">
<field name="name"/>
<field name="category_id"/>
- <field name="supplier"/>
+ <field name="supplier_id"/>
<field name="description"/>
+ <filter name="inactive" string="Archived" domain="[('active','=',False)]"/>
<group expand="0" string="Group By">
- <filter name="supplier" string="Vendor" context="{'group_by': 'supplier'}"/>
- <filter name="category" string="Product Category" context="{'group_by': 'category_id'}"/>
- <filter name="inactive" string="Archived" domain="[('active','=',False)]"/>
+ <filter name="group_by_supplier" string="Vendor" context="{'group_by': 'supplier_id'}"/>
+ <filter name="group_by_category" string="Product Category" context="{'group_by': 'category_id'}"/>
</group>
</search>
</field>
@@ -23,11 +23,12 @@
<field name="model">lunch.product</field>
<field name="arch" type="xml">
<tree string="Products Tree">
+ <field name="currency_id" invisible="1"/>
<field name="name"/>
<field name="category_id"/>
- <field name="supplier"/>
+ <field name="supplier_id"/>
<field name="description"/>
- <field name="price"/>
+ <field name="price" widget="monetary"/>
</tree>
</field>
</record>
@@ -37,8 +38,7 @@
<field name="model">lunch.product</field>
<field name="arch" type="xml">
<form string="Products Form">
- <header>
- </header>
+ <field name="currency_id" invisible="1"/>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object"
@@ -47,49 +47,116 @@
options='{"terminology": "archive"}'/>
</button>
</div>
+ <field name="image" widget="image" class="oe_avatar" options="{'preview_image': 'image_medium'}"/>
+ <div class="oe_title">
+ <label for="name" class="oe-edit-only"/>
+ <h1>
+ <field name='name'/>
+ </h1>
+ </div>
<group>
<group>
- <field name='name'/>
<field name='category_id'/>
+ <field name='supplier_id'/>
+ <field name='price' widget="monetary"/>
</group>
<group>
- <field name='supplier'/>
- <field name='price'/>
+ <field name="new_until"/>
+ <field name='description'/>
</group>
</group>
- <label for='description'/>
- <field name='description'/>
</sheet>
</form>
</field>
</record>
+ <record id="view_lunch_product_kanban_order" model="ir.ui.view">
+ <field name="name">lunch.product.kanban</field>
+ <field name="model">lunch.product.report</field>
+ <field name="priority">999</field>
+ <field name="arch" type="xml">
+ <kanban js_class="lunch_kanban" create="0" edit="0" class="o_kanban_mobile">
+ <field name="id"/>
+ <field name="name"/>
+ <field name="category_id"/>
+ <field name="supplier_id"/>
+ <field name="description"/>
+ <field name="currency_id"/>
+ <field name="user_id"/>
+ <field name="product_id"/>
+ <field name="is_new"/>
+ <templates>
+ <t t-name="kanban-box">
+ <div class="oe_kanban_global_click o_kanban_record_has_image_fill o_kanban_record">
+ <div class="o_kanban_image_fill_left d-none d-md-block"
+ t-attf-style="background-image:url('#{kanban_image('lunch.product', 'image_medium', record.product_id.raw_value)}')"/>
+ <div class="oe_kanban_details">
+ <div class="o_kanban_record_top">
+ <div class="o_kanban_record_headings">
+ <strong class="o_kanban_record_title">
+ <div>
+ <span class="pr-1"><field name="is_favorite" widget="boolean_favorite" nolabel="1"/></span>
+ <div class="float-right o_lunch_purple">
+ <field name="price" widget="monetary"/>
+ </div>
+ <div t-if="record.is_new.raw_value" class="float-right o_lunch_new_product mr-1 badge-pill badge-success">
+ New
+ </div>
+ <strong><span t-esc="record.name.value"/></strong>
+ </div>
+ </strong>
+ <span class="o_kanban_record_subtitle"><span t-esc="record.supplier_id.value"/></span>
+ </div>
+ </div>
+ <div class="o_kanban_record_bottom">
+ <ul>
+ <li t-esc="record.description.value" class="text-muted"/>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </t>
+ </templates>
+ </kanban>
+ </field>
+ </record>
+
<record id="view_lunch_product_kanban" model="ir.ui.view">
<field name="name">lunch.product.kanban</field>
<field name="model">lunch.product</field>
+ <field name="priority">5</field>
<field name="arch" type="xml">
- <kanban class="o_kanban_mobile">
+ <kanban create="0" edit="0" class="o_kanban_mobile">
+ <field name="id"/>
<field name="name"/>
<field name="category_id"/>
- <field name="supplier"/>
+ <field name="supplier_id"/>
<field name="description"/>
- <field name="price"/>
+ <field name="currency_id"/>
<templates>
<t t-name="kanban-box">
- <div t-attf-class="oe_kanban_global_click">
- <div class="o_kanban_record_top">
- <div class="o_kanban_record_headings">
- <strong class="o_kanban_record_title"><t t-esc="record.name.value"/></strong>
+ <div class="oe_kanban_global_click o_kanban_record_has_image_fill o_kanban_record">
+ <div class="o_kanban_image_fill_left d-none d-md-block"
+ t-attf-style="background-image:url('#{kanban_image('lunch.product', 'image_medium', record.id.raw_value)}')"/>
+ <div class="oe_kanban_details">
+ <div class="o_kanban_record_top">
+ <div class="o_kanban_record_headings">
+ <strong class="o_kanban_record_title">
+ <div>
+ <div class="float-right">
+ <field name="price" widget="monetary"/>
+ </div>
+ <strong><span t-esc="record.name.value"/></strong>
+ </div>
+ </strong>
+ <span class="o_kanban_record_subtitle"><span t-esc="record.supplier_id.value"/></span>
+ </div>
+ </div>
+ <div class="o_kanban_record_bottom">
+ <ul>
+ <li t-esc="record.description.value" class="text-muted"/>
+ </ul>
</div>
- <span class="badge badge-pill float-right">
- Price: <t t-esc="record.price.value"/>
- </span>
- </div>
- <div>
- <t t-esc="record.supplier.value"/>
- </div>
- <div>
- <t t-esc="record.description.value"/>
</div>
</div>
</t>
@@ -98,14 +165,68 @@
</field>
</record>
+ <record id="lunch_product_category_view_tree" model="ir.ui.view">
+ <field name="name">Product category Tree</field>
+ <field name="model">lunch.product.category</field>
+ <field name="arch" type="xml">
+ <tree string="Products List">
+ <field name='name' string="Product Category: "/>
+ </tree>
+ </field>
+ </record>
+
<record id="lunch_product_category_view_form" model="ir.ui.view">
<field name="name">Product category Form</field>
<field name="model">lunch.product.category</field>
<field name="arch" type="xml">
- <form string="Products Form">
+ <form string="Product Categories Form">
<sheet>
+ <div class="oe_title">
+ <label for="name" class="oe-edit-only"/>
+ <h1>
+ <field name="name"/>
+ </h1>
+ </div>
<group>
- <field name='name' string="Product Category: "/>
+ <group>
+ <field name="topping_label_1"/>
+ <field name="topping_quantity_1"/>
+ </group>
+ <div>
+ <field name="topping_ids_1" nolabel="1">
+ <tree editable="bottom">
+ <field name="name"/>
+ <field name="currency_id" invisible="1"/>
+ <field name="price" widget="monetary"/>
+ </tree>
+ </field>
+ </div>
+ <group>
+ <field name="topping_label_2"/>
+ <field name="topping_quantity_2"/>
+ </group>
+ <div>
+ <field name="topping_ids_2" nolabel="1">
+ <tree editable="bottom">
+ <field name="name"/>
+ <field name="currency_id" invisible="1"/>
+ <field name="price" widget="monetary"/>
+ </tree>
+ </field>
+ </div>
+ <group>
+ <field name="topping_label_3"/>
+ <field name="topping_quantity_3"/>
+ </group>
+ <div>
+ <field name="topping_ids_3" nolabel="1">
+ <tree editable="bottom">
+ <field name="name"/>
+ <field name="currency_id" invisible="1"/>
+ <field name="price" widget="monetary"/>
+ </tree>
+ </field>
+ </div>
</group>
</sheet>
</form>
@@ -117,11 +238,27 @@
<field name="res_model">lunch.product</field>
<field name="view_mode">tree,kanban,form</field>
<field name="help" type="html">
- <p class="o_view_nocontent_smiling_face">
+ <p class="o_view_nocontent_smiling_face">
Create a new product for lunch
- </p><p>
+ </p><p>
A product is defined by its name, category, price and vendor.
- </p>
+ </p>
+ </field>
+ </record>
+
+ <record id="lunch_product_action_order" model="ir.actions.act_window">
+ <field name="name">Order Your Lunch</field>
+ <field name="res_model">lunch.product.report</field>
+ <field name="view_mode">kanban</field>
+ <field name="view_id" ref="view_lunch_product_kanban_order"/>
+ <field name="search_view_id" ref="lunch_product_view_search"/>
+ <field name="domain">[('supplier_id.available_today', '=', True), '|', ('user_id', '=', False), ('user_id', '=', uid)]</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ There is no product available today
+ </p><p>
+ To see some products, check if your vendors are available today and that you have configured some products
+ </p>
</field>
</record>
@@ -130,11 +267,11 @@
<field name="res_model">lunch.product.category</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
- <p class="o_view_nocontent_smiling_face">
+ <p class="o_view_nocontent_smiling_face">
Create a new lunch category
- </p><p>
+ </p><p>
Here you can access all categories for the lunch products.
- </p>
+ </p>
</field>
</record>
</odoo>
diff --git a/addons/lunch/views/lunch_supplier_views.xml b/addons/lunch/views/lunch_supplier_views.xml
new file mode 100644
index 00000000000..9d0803e33ca
--- /dev/null
+++ b/addons/lunch/views/lunch_supplier_views.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="lunch_supplier_view_tree" model="ir.ui.view">
+ <field name="name">lunch.supplier.view.tree</field>
+ <field name="model">lunch.supplier</field>
+ <field name="arch" type="xml">
+ <tree>
+ <field name="name"/>
+ <field name="phone"/>
+ <field name="email"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="lunch_supplier_view_form" model="ir.ui.view">
+ <field name="name">lunch.supplier.view.form</field>
+ <field name="model">lunch.supplier</field>
+ <field name="arch" type="xml">
+ <form>
+ <sheet>
+ <div class="oe_button_box" name="button_box">
+ <button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
+ <field name="active" widget="boolean_button" options="{&quot;terminology&quot;: &quot;archive&quot;}"/>
+ </button>
+ </div>
+ <div class="oe_title">
+ <label for="name" class="oe_edit_only"/>
+ <h1><field name="name" required="1"/></h1>
+ </div>
+ <group>
+ <group>
+ <field name="partner_id"/>
+ <label for="street" string="Address"/>
+ <div class="o_address_format">
+ <field name="street" placeholder="Street..." class="o_address_street"/>
+ <field name="street2" placeholder="Street 2..." class="o_address_street"/>
+ <field name="city" placeholder="City" class="o_address_city"/>
+ <field name="state_id" class="o_address_state" placeholder="State" options="{'no_open': True}" context="{'country_id': country_id, 'zip': zip_code}"/>
+ <field name="zip_code" placeholder="ZIP" class="o_address_zip"/>
+ <field name="country_id" placeholder="Country" class="o_address_country" options="{'no_open': True, 'no_create': True}"/>
+ </div>
+ </group>
+ <group>
+ <field name="email" attrs="{'required': [('send_by', '=', 'mail')]}"/>
+ <field name="phone" attrs="{'required': [('send_by', '=', 'phone')]}"/>
+ <field name="responsible_id" attrs="{'required': [('send_by', '=', 'mail')]}" groups="base.group_no_one"/>
+ </group>
+ </group>
+ <group>
+ <group string="Availability">
+ <field name="tz" groups="base.group_no_one"/>
+ <field name="recurrency_monday"/>
+ <field name="recurrency_tuesday"/>
+ <field name="recurrency_wednesday"/>
+ <field name="recurrency_thursday"/>
+ <field name="recurrency_friday"/>
+ <field name="recurrency_saturday"/>
+ <field name="recurrency_sunday"/>
+ <field name="recurrency_end_date" groups="base.group_no_one"/>
+ </group>
+ <group string="Orders">
+ <field name="delivery"/>
+ <field name="available_location_ids" widget="many2many_tags"/>
+ <field name="send_by" widget="radio"/>
+ <label for="automatic_email_time"/>
+ <div class="o_row"><field name="automatic_email_time" widget="float_time"/> <field name="moment"/></div>
+ </group>
+ </group>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/>
+ <field name="activity_ids" widget="mail_activity"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </form>
+ </field>
+ </record>
+
+ <record id="lunch_supplier_view_kanban" model="ir.ui.view">
+ <field name="name">lunch.supplier.view.kanban</field>
+ <field name="model">lunch.supplier</field>
+ <field name="arch" type="xml">
+ <kanban>
+ <field name="partner_id"/>
+ <field name="city"/>
+ <field name="country_id"/>
+ <field name="email"/>
+ <templates>
+ <t t-name="kanban-box">
+ <div class="oe_kanban_global_click o_kanban_record_has_image_fill o_res_partner_kanban">
+ <div class="oe_kanban_details">
+ <strong class="o_kanban_record_title oe_partner_heading"><field name="display_name"/></strong>
+ <ul>
+ <li t-if="record.city.raw_value and !record.country_id.raw_value"><field name="city"/></li>
+ <li t-if="!record.city.raw_value and record.country_id.raw_value"><field name="country_id"/></li>
+ <li t-if="record.city.raw_value and record.country_id.raw_value"><field name="city"/>, <field name="country_id"/></li>
+ <li t-if="record.email.raw_value" class="o_text_overflow"><field name="email"/></li>
+ </ul>
+ </div>
+ </div>
+ </t>
+ </templates>
+ </kanban>
+ </field>
+ </record>
+
+ <record id="lunch_vendors_action" model="ir.actions.act_window">
+ <field name="name">Vendors</field>
+ <field name="res_model">lunch.supplier</field>
+ <field name="view_mode">kanban,tree,form</field>
+ </record>
+</odoo>
diff --git a/addons/lunch/views/lunch_templates.xml b/addons/lunch/views/lunch_templates.xml
index a5d8c6e6935..a076bbb2261 100644
--- a/addons/lunch/views/lunch_templates.xml
+++ b/addons/lunch/views/lunch_templates.xml
@@ -2,14 +2,25 @@
<odoo>
<template id="assets_backend" name="lunch assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
- <link rel="stylesheet" type="text/scss" href="/lunch/static/src/scss/lunch.scss"/>
- <script type="text/javascript" src="/lunch/static/src/js/lunch.js"></script>
+ <link rel="stylesheet" type="text/scss" href="/lunch/static/src/scss/lunch_kanban.scss"/>
+
+ <script type="text/javascript" src="/lunch/static/src/js/lunch_kanban_widget.js"></script>
+ <script type="text/javascript" src="/lunch/static/src/js/lunch_payment_dialog.js"></script>
+ <script type="text/javascript" src="/lunch/static/src/js/lunch_kanban_view.js"></script>
+ <script type="text/javascript" src="/lunch/static/src/js/lunch_kanban_controller.js"></script>
+ <script type="text/javascript" src="/lunch/static/src/js/lunch_kanban_renderer.js"></script>
+ <script type="text/javascript" src="/lunch/static/src/js/lunch_kanban_record.js"></script>
+ <script type="text/javascript" src="/lunch/static/src/js/lunch_kanban_model.js"></script>
</xpath>
</template>
<template id="qunit_suite" name="lunch tests" inherit_id="web.qunit_suite">
<xpath expr="//t[@t-set='head']" position="inside">
- <script type="text/javascript" src="/lunch/static/tests/lunch_tests.js"/>
+ <script type="text/javascript" src="/lunch/static/tests/test_lunch_kanban.js"></script>
</xpath>
</template>
+
+ <template id="lunch_payment_dialog" name="Lunch Payment Dialog">
+ To add some money to your wallet, please contact your lunch manager.
+ </template>
</odoo>
diff --git a/addons/lunch/views/lunch_views.xml b/addons/lunch/views/lunch_views.xml
index 454085fb92b..e3a262ca3d1 100644
--- a/addons/lunch/views/lunch_views.xml
+++ b/addons/lunch/views/lunch_views.xml
@@ -6,18 +6,21 @@
<menuitem name="Manager" parent="menu_lunch" id="menu_lunch_admin" sequence="51" groups="group_lunch_manager"/>
<menuitem name="Configuration" parent="menu_lunch" id="menu_lunch_config" sequence="53" groups="group_lunch_manager"/>
- <menuitem name="New Order" parent="menu_lunch_title" id="lunch_order_menu_form" action="lunch_order_action_form" sequence="1"/>
- <menuitem name="Previous Orders" parent="menu_lunch_title" id="lunch_order_menu_tree" action="lunch_order_line_action" sequence="2"/>
- <menuitem name="Your Lunch Account" parent="menu_lunch_title" id="lunch_cashmove_menu_form" action="lunch_cashmove_action_account" sequence="3"/>
+ <menuitem name="New Order" parent="menu_lunch_title" id="lunch_order_menu_form" action="lunch.lunch_product_action_order" sequence="1"/>
+ <menuitem name="Previous Orders" parent="menu_lunch_title" id="lunch_order_menu_tree" action="lunch_order_action" sequence="2"/>
+ <menuitem name="My Lunch Account" parent="menu_lunch_title" id="lunch_cashmove_report_menu_form" action="lunch_cashmove_report_action_account" sequence="3"/>
- <menuitem name="Today's Orders" parent="menu_lunch_admin" id="lunch_order_line_menu_by_supplier" action="lunch_order_line_action_by_supplier" />
- <menuitem name="Orders by Vendor" parent="menu_lunch_admin" id="lunch_order_line_menu_control_suppliers" action="lunch_order_line_action_control_suppliers" />
- <menuitem name="Control Accounts" parent="menu_lunch_admin" id="lunch_cashmove_menu_control_accounts" action="lunch_cashmove_action_control_accounts"/>
- <menuitem name="Employee Payments" parent="menu_lunch_admin" id="lunch_cashmove_menu_payment" action="lunch_cashmove_action_payment"/>
+ <menuitem name="Today's Orders" parent="menu_lunch_admin" id="lunch_order_menu_by_supplier" action="lunch_order_action_by_supplier" />
+ <menuitem name="Orders by Vendor" parent="menu_lunch_admin" id="lunch_order_menu_control_suppliers" action="lunch_order_action_control_suppliers" />
+ <menuitem name="Control Accounts" parent="menu_lunch_admin" id="lunch_cashmove_report_menu_control_accounts" action="lunch_cashmove_report_action_control_accounts"/>
+ <menuitem name="Employee Payments" parent="menu_lunch_admin" id="lunch_cashmove_report_menu_payment" action="lunch_cashmove_action_payment"/>
- <menuitem name="Products" parent="menu_lunch_config" id="lunch_product_menu" action="lunch_product_action"/>
- <menuitem name="Product Categories" parent="menu_lunch_config" id="lunch_product_category_menu" action="lunch_product_category_action" />
- <menuitem name="Alerts" parent="menu_lunch_config" id="lunch_alert_menu" action="lunch_alert_action" />
+ <menuitem name="Settings" parent="menu_lunch_config" id="lunch_settings_menu" action="lunch_config_settings_action" sequence="1"/>
+ <menuitem name="Vendors" parent="menu_lunch_config" id="lunch_vendors_menu" action="lunch_vendors_action" sequence="2"/>
+ <menuitem name="Locations" parent="menu_lunch_config" id="lunch_location_menu" action="lunch_location_action" sequence="3"/>
+ <menuitem name="Products" parent="menu_lunch_config" id="lunch_product_menu" action="lunch_product_action" sequence="4"/>
+ <menuitem name="Product Categories" parent="menu_lunch_config" id="lunch_product_category_menu" action="lunch_product_category_action" sequence="5"/>
+ <menuitem name="Alerts" parent="menu_lunch_config" id="lunch_alert_menu" action="lunch_alert_action" sequence="6"/>
<record id="action_server_lunch_archive_product" model="ir.actions.server">
diff --git a/addons/lunch/views/res_config_settings.xml b/addons/lunch/views/res_config_settings.xml
new file mode 100644
index 00000000000..bd99a708b4c
--- /dev/null
+++ b/addons/lunch/views/res_config_settings.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="res_config_settings_view_form" model="ir.ui.view">
+ <field name="name">res.config.settings.view.form.inherit.lunch</field>
+ <field name="model">res.config.settings</field>
+ <field name="inherit_id" ref="base.res_config_settings_view_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//div[hasclass('settings')]" position="inside">
+ <div class="app_settings_block" data-string="Lunch" string="Lunch" data-key="lunch" groups="lunch.group_lunch_manager">
+ <field name="currency_id" invisible="1"/>
+ <h2>Lunch</h2>
+ <div class="row mt16 o_settings_container">
+ <div class="col-12 col-lg-6 o_setting_box" id="lunch_minimum_threshold"
+ title="None">
+ <div class="o_setting_right_pane">
+ <span class="o_form_label">Lunch Overdraft</span>
+ <div class="text-muted">
+ Maximum overdraft that your employees can reach
+ </div>
+ <div class="content-group">
+ <div class="mt16 row">
+ <label for="company_lunch_minimum_threshold" string="Overdraft" class="col-3 col-lg-3 o_light_label"/>
+ <field name="company_lunch_minimum_threshold" widget="monetary"/>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="lunch_config_settings_action" model="ir.actions.act_window">
+ <field name="name">Settings</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">res.config.settings</field>
+ <field name="view_id" ref="res_config_settings_view_form"/>
+ <field name="view_mode">form</field>
+ <field name="target">inline</field>
+ <field name="context">{'module' : 'lunch'}</field>
+ </record>
+</odoo>
diff --git a/addons/lunch/wizard/__init__.py b/addons/lunch/wizard/__init__.py
index 80c2ca03500..9fa7ccafa50 100644
--- a/addons/lunch/wizard/__init__.py
+++ b/addons/lunch/wizard/__init__.py
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from . import lucky_order
+from . import lunch_line_wizard
diff --git a/addons/lunch/wizard/lucky_order.py b/addons/lunch/wizard/lucky_order.py
deleted file mode 100644
index 5836a418e56..00000000000
--- a/addons/lunch/wizard/lucky_order.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-import random
-
-from odoo import api, fields, models, _
-from odoo.exceptions import UserError
-
-
-class LunchOrderLineLucky(models.TransientModel):
- _name = 'lunch.order.line.lucky'
- _description = 'Lunch Order Line Lucky'
-
- def _default_supplier(self):
- suppliers_obj = self.env['lunch.product'].search([]).mapped("supplier")
- return [(4, supplier.id) for supplier in suppliers_obj]
-
- product_id = fields.Many2one('lunch.product', 'Product', store=True)
- supplier_ids = fields.Many2many(comodel_name='res.partner', string='Vendor', domain=lambda self: [("id", "in", self.env['lunch.product'].search([]).mapped("supplier").ids)])
- is_max_budget = fields.Boolean("I'm not feeling rich", help="Enable this option to set a maximal budget for your lucky order.", store=True)
- max_budget = fields.Float('Max Budget', store=True)
-
- @api.multi
- def random_pick(self):
- """
- To pick a random product from the selected suppliers, and create an order with this one
- """
- self.ensure_one()
- if self.is_max_budget:
- products_obj = self.env['lunch.product'].search([('supplier', "in", self.supplier_ids.ids), ('price', '<=', self.max_budget)])
- else:
- products_obj = self.env['lunch.product'].search([('supplier', "in", self.supplier_ids.ids)])
- if len(products_obj) != 0:
- random_product_obj = self.env['lunch.product'].browse([random.choice(products_obj.ids)])
- order_line = self.env['lunch.order.line'].create({
- 'product_id': random_product_obj.id,
- 'order_id': self._context['active_id']
- })
- else:
- raise UserError(_('No product is matching your request. Now you will starve to death.'))
-
-
-
-
diff --git a/addons/lunch/wizard/lucky_order_view.xml b/addons/lunch/wizard/lucky_order_view.xml
deleted file mode 100644
index 2aafbb762b9..00000000000
--- a/addons/lunch/wizard/lucky_order_view.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-<data>
-
- <record id="view_lunch_order_line_lucky" model="ir.ui.view">
- <field name="name">view.lunch.order.line.lucky</field>
- <field name="model">lunch.order.line.lucky</field>
- <field name="arch" type="xml">
- <form string="I'm feeling lucky today !">
- <group string="Select your vendor">
- <field name="is_max_budget"/>
- <field name="max_budget" attrs="{'required': [('is_max_budget', '=', True)], 'invisible': [('is_max_budget', '=', False)]}"/>
- <field name="supplier_ids" widget="many2many_checkboxes"/>
- </group>
- <footer>
- <button name="random_pick" string="I'm feeling lucky" type="object" class="oe_highlight"/>
- <button string="Cancel" class="btn btn-secondary" special="cancel" />
- </footer>
- </form>
- </field>
- </record>
-
- <record id="action_lunch_order_line_lucky" model="ir.actions.act_window">
- <field name="name">I'm feeling lucky today !</field>
- <field name="type">ir.actions.act_window</field>
- <field name="res_model">lunch.order.line.lucky</field>
- <field name="view_type">form</field>
- <field name="view_mode">form</field>
- <field name="view_id" ref="view_lunch_order_line_lucky"/>
- <field name="context">{'record_id' : active_id}</field>
- <field name="target">new</field>
- </record>
-
-</data>
-</odoo>
diff --git a/addons/lunch/wizard/lunch_line_wizard.py b/addons/lunch/wizard/lunch_line_wizard.py
new file mode 100644
index 00000000000..2006144e582
--- /dev/null
+++ b/addons/lunch/wizard/lunch_line_wizard.py
@@ -0,0 +1,135 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, _
+
+from odoo.exceptions import ValidationError
+
+
+class LunchOrderWizard(models.TransientModel):
+ _name = 'lunch.order.temp'
+ _description = 'Lunch Order Temp'
+
+ def _default_order_line(self):
+ line_id = self.env.context.get('line_id')
+
+ if line_id:
+ last_time_ordered = self.env['lunch.order'].browse(line_id)
+ else:
+ last_time_ordered = self.env['lunch.order'].search([('product_id', '=', self.env.context.get('default_product_id', 0)),
+ ('user_id', '=', self.env.context.get('default_user_id', self.env.user.id))],
+ order="date desc, id desc", limit=1)
+ return last_time_ordered
+
+ currency_id = fields.Many2one('res.currency', default=lambda self: self.env.user.company_id.currency_id)
+
+ product_id = fields.Many2one('lunch.product', string='Product ID')
+ product_description = fields.Text('Description', related='product_id.description')
+ product_name = fields.Char('Product', related='product_id.name')
+ product_category = fields.Many2one('lunch.product.category', related='product_id.category_id')
+ topping_label_1 = fields.Char(related='product_id.category_id.topping_label_1')
+ topping_label_2 = fields.Char(related='product_id.category_id.topping_label_2')
+ topping_label_3 = fields.Char(related='product_id.category_id.topping_label_3')
+ topping_quantity_1 = fields.Selection(related='product_id.category_id.topping_quantity_1')
+ topping_quantity_2 = fields.Selection(related='product_id.category_id.topping_quantity_2')
+ topping_quantity_3 = fields.Selection(related='product_id.category_id.topping_quantity_3')
+ topping_ids_1 = fields.Many2many('lunch.topping', 'lunch_order_temp_topping', 'order_id', 'topping_id', string="Extra Garniture",
+ domain="[('category_id', '=', product_category), ('topping_category', '=', 1)]",
+ default=lambda self: self._default_order_line().topping_ids_1)
+ topping_ids_2 = fields.Many2many('lunch.topping', 'lunch_order_temp_topping', 'order_id', 'topping_id', string="Extra Garniture 2",
+ domain="[('category_id', '=', product_category), ('topping_category', '=', 2)]",
+ default=lambda self: self._default_order_line().topping_ids_2)
+ topping_ids_3 = fields.Many2many('lunch.topping', 'lunch_order_temp_topping', 'order_id', 'topping_id', string="Extra Garniture 3",
+ domain="[('category_id', '=', product_category), ('topping_category', '=', 3)]",
+ default=lambda self: self._default_order_line().topping_ids_3)
+
+ available_toppings_1 = fields.Boolean(help='Are toppings available for this product', compute='_compute_available_toppings')
+ available_toppings_2 = fields.Boolean(help='Are toppings available for this product', compute='_compute_available_toppings')
+ available_toppings_3 = fields.Boolean(help='Are toppings available for this product', compute='_compute_available_toppings')
+
+ quantity = fields.Float('Quantity', default=1)
+ price_total = fields.Float('Total Price', compute='_compute_price_total')
+ note = fields.Text('Special Instructions', default=lambda self: self._default_order_line().note)
+
+ user_id = fields.Many2one('res.users', default=lambda self: self.env.user.id)
+ edit = fields.Boolean('Edit Mode', default=lambda self: bool(self.env.context.get('line_id')))
+
+ @api.depends('product_id')
+ def _compute_available_toppings(self):
+ for wizard in self:
+ wizard.available_toppings_1 = bool(wizard.env['lunch.topping'].search_count([('category_id', '=', wizard.product_category.id), ('topping_category', '=', 1)]))
+ wizard.available_toppings_2 = bool(wizard.env['lunch.topping'].search_count([('category_id', '=', wizard.product_category.id), ('topping_category', '=', 2)]))
+ wizard.available_toppings_3 = bool(wizard.env['lunch.topping'].search_count([('category_id', '=', wizard.product_category.id), ('topping_category', '=', 3)]))
+
+ @api.constrains('topping_ids_1', 'topping_ids_2', 'topping_ids_3')
+ def _check_topping_quantity(self):
+ errors = {
+ '1_more': _('You should order at least one %s'),
+ '1': _('You have to order one and only one %s'),
+ }
+ for wizard in self:
+ for index in range(1, 4):
+ availability = wizard['available_toppings_%s' % index]
+ quantity = wizard['topping_quantity_%s' % index]
+ toppings = wizard['topping_ids_%s' % index].filtered(lambda x: x.topping_category == index)
+ label = wizard['topping_label_%s' % index]
+
+ if availability and quantity != '0_more':
+ check = bool(len(toppings) == 1 if quantity == '1' else toppings)
+ if not check:
+ raise ValidationError(errors[quantity] % label)
+
+ @api.depends('product_id', 'topping_ids_1', 'topping_ids_2', 'topping_ids_3', 'quantity')
+ def _compute_price_total(self):
+ for wizard in self:
+ wizard.price_total = wizard.quantity * (wizard.product_id.price +
+ sum((wizard.topping_ids_1 | wizard.topping_ids_2 | wizard.topping_ids_3).mapped('price')))
+
+ def _get_matching_lines(self):
+ domain = [('product_id', '=', self.product_id.id), ('date', '=', fields.Date.today()), ('note', '=', self._get_note())]
+ lines = self.env['lunch.order'].search(domain)
+ return lines.filtered(lambda line: (line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3) == self.topping_ids_1)
+
+ def _get_note(self):
+ """
+ returns self.note, but make sure that if it is an empty string it becomes False
+ """
+ return self.note if self.note else False
+
+ def add_to_cart(self):
+ self.ensure_one()
+ line_id = self.env.context.get('line_id')
+
+ matching_line = False
+ matching_lines = self._get_matching_lines()
+
+ if matching_lines:
+ matching_line = matching_lines[0]
+ quantity = 1
+
+ if matching_line.id != line_id:
+ if self.edit:
+ line = self.env['lunch.order'].browse(line_id)
+ quantity = line.quantity
+ line.sudo().unlink()
+ else:
+ quantity = 0
+
+ matching_line.quantity += quantity
+ else:
+ if self.edit:
+ line = self.env['lunch.order'].browse(line_id)
+
+ line.topping_ids_1 = self.topping_ids_1
+ line.topping_ids_2 = self.topping_ids_2
+ line.topping_ids_3 = self.topping_ids_3
+ line.note = self._get_note()
+ else:
+ self.env['lunch.order'].create({
+ 'product_id': self.product_id.id,
+ 'topping_ids_1': [(6, 0, self.topping_ids_1.ids)],
+ 'topping_ids_2': [(6, 0, self.topping_ids_2.ids)],
+ 'topping_ids_3': [(6, 0, self.topping_ids_3.ids)],
+ 'quantity': self.quantity,
+ 'note': self._get_note()
+ })
diff --git a/addons/lunch/wizard/lunch_line_wizard_views.xml b/addons/lunch/wizard/lunch_line_wizard_views.xml
new file mode 100644
index 00000000000..2e3a6990caa
--- /dev/null
+++ b/addons/lunch/wizard/lunch_line_wizard_views.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+<data>
+ <record id="view_lunch_order_temp" model="ir.ui.view">
+ <field name="name">view.lunch.order.temp</field>
+ <field name="model">lunch.order.temp</field>
+ <field name="arch" type="xml">
+ <form>
+ <field name="currency_id" invisible="1"/>
+ <field name="quantity" invisible="1"/>
+ <field name="product_id" invisible="1"/>
+ <field name="product_category" invisible="1"/>
+ <field name="available_toppings_1" invisible="1"/>
+ <field name="available_toppings_2" invisible="1"/>
+ <field name="available_toppings_3" invisible="1"/>
+ <field name="edit" invisible="1"/>
+ <h2 class="d-flex">
+ <div class="flex-grow-1 pr-5"><field name="product_name"/></div>
+ <div class="flex-grow-0"><field name="price_total" widget="monetary"/></div>
+ </h2>
+ <group col="4">
+ <field name="topping_label_1" nolabel="1" attrs="{'invisible': [('available_toppings_1', '=', False)]}" class="o_form_label"/>
+ <field name="topping_ids_1" attrs="{'invisible': [('available_toppings_1', '=', False)]}" widget="many2many_checkboxes" nolabel="1" colspan="3"/>
+ <field name="topping_label_2" nolabel="1" attrs="{'invisible': [('available_toppings_2', '=', False)]}" class="o_form_label"/>
+ <field name="topping_ids_2" attrs="{'invisible': [('available_toppings_2', '=', False)]}" widget="many2many_checkboxes" nolabel="1" colspan="3"/>
+ <field name="topping_label_3" nolabel="1" attrs="{'invisible': [('available_toppings_3', '=', False)]}" class="o_form_label"/>
+ <field name="topping_ids_3" attrs="{'invisible': [('available_toppings_3', '=', False)]}" widget="many2many_checkboxes" nolabel="1" colspan="3"/>
+ <field name="note" placeholder="Informations, allergens, ..." colspan="3"/>
+ </group>
+ <footer>
+ <button name="add_to_cart" string="Add To Cart" type="object" class="oe_highlight" attrs="{'invisible': [('edit', '=', True)]}"/>
+ <button name="add_to_cart" string="Save" type="object" class="oe_highlight" attrs="{'invisible': [('edit', '=', False)]}"/>
+ <button string="Discard" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+</data>
+</odoo>