diff options
| author | RomainLibert <rli@odoo.com> | 2018-10-25 16:43:19 +0200 |
|---|---|---|
| committer | Yannick Tivisse <yti@odoo.com> | 2018-12-04 11:56:21 +0100 |
| commit | f727b8f0b57aad49f2ea8c54600ab73bbd02570f (patch) | |
| tree | 7da43e4b2615476446a28b62c7827788f6113e2a /addons | |
| parent | ad99f09a062cd190c9863d49486e60899daed2a6 (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.
In this commit we add a new widget that comes with the kanban view and will allow to take orders
in a friendly manner.
We also add a lunch.supplier model and an integration with the hr module in order to filter what
products you can order depending on your working location, this also allows you to configure an
automatic ordering via email.
TaskID: 1856876
Diffstat (limited to 'addons')
75 files changed, 1981 insertions, 1089 deletions
diff --git a/addons/lunch/__init__.py b/addons/lunch/__init__.py index b6f07433380..88710707227 100644 --- a/addons/lunch/__init__.py +++ b/addons/lunch/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import wizard +from . import controllers from . import models +from . import wizard diff --git a/addons/lunch/__manifest__.py b/addons/lunch/__manifest__.py index e28b88a82f2..5268ecca6a7 100644 --- a/addons/lunch/__manifest__.py +++ b/addons/lunch/__manifest__.py @@ -27,16 +27,17 @@ If you want to save your employees' time and avoid them to always have coins in 'report/lunch_order_views.xml', 'report/lunch_order_templates.xml', 'views/lunch_templates.xml', - 'wizard/lucky_order_view.xml', - 'views/lunch_alert_views.xml', + 'wizard/lunch_line_wizard_views.xml', 'views/lunch_cashmove_views.xml', 'views/lunch_orders_views.xml', 'views/lunch_product_views.xml', + 'views/lunch_supplier_views.xml', 'views/lunch_views.xml', 'data/lunch_data.xml', + 'data/ir_cron_data.xml' ], 'demo': ['data/lunch_demo.xml'], - 'qweb': ['static/src/xml/lunch.xml', ], + 'qweb': ['static/src/xml/lunch_kanban.xml', ], 'installable': True, 'application': True, 'certificate': '001292377792581874189', diff --git a/addons/lunch/controllers/__init__.py b/addons/lunch/controllers/__init__.py new file mode 100644 index 00000000000..12a7e529b67 --- /dev/null +++ b/addons/lunch/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/addons/lunch/controllers/main.py b/addons/lunch/controllers/main.py new file mode 100644 index 00000000000..314cf5ad292 --- /dev/null +++ b/addons/lunch/controllers/main.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import http, fields +from odoo.http import request + + +class LunchController(http.Controller): + @http.route('/lunch/infos', type='json', auth='user') + def infos(self, user_id=None): + user = request.env['res.users'].browse(user_id) if user_id else request.env.user + + infos = { + 'order': False, + 'wallet': request.env['lunch.cashmove'].get_wallet_balance(user), + 'username': user.sudo().name, + 'userimage': '/web/image?model=res.users&id=%s&field=image_small' % user.id, + 'is_manager': request.env.user.has_group('lunch.group_lunch_manager'), + 'users': request.env['res.users'].search_read([('groups_id', 'not in', [request.env.ref('base.group_portal').id])], ['name']), + } + + order = self._get_current_order(user.id) + if order: + lines = [{'id': line.id, + 'product': (line.product_id.name, line.product_id.price), + 'toppings': [(topping.name, topping.price) for topping in line.topping_ids], + 'quantity': line.quantity, + 'price': line.price} for line in order.order_line_ids] + infos.update({ + 'order': order.id, + 'total': order.total, + 'state': order.state, + 'lines': lines, + }) + return infos + + @http.route('/lunch/trash', type='json', auth='user') + def trash(self, user_id=None): + user = request.env['res.users'].browse(user_id) if user_id else request.env.user + + order = self._get_current_order(user.id) + order.unlink() + + @http.route('/lunch/pay', type='json', auth='user') + def pay(self, user_id=None): + user = request.env['res.users'].browse(user_id) if user_id else request.env.user + + order = self._get_current_order(user.id) + if order: + wallet_balance = request.env['lunch.cashmove'].get_wallet_balance(user) + + if order.state == 'new' and order.total <= wallet_balance: + return self._make_payment(order) + + return False + + @http.route('/lunch/payment_message', type='json', auth='user') + def payment_message(self): + return {'message': request.env['ir.qweb'].render('lunch.lunch_payment_dialog', {})} + + def _get_current_order(self, user_id): + order = request.env['lunch.order'].search([('user_id', '=', user_id), ('date', '=', fields.Date.today())], limit=1) + return order + + def _make_payment(self, order): + order.action_order() + return True diff --git a/addons/lunch/data/ir_cron_data.xml b/addons/lunch/data/ir_cron_data.xml new file mode 100644 index 00000000000..19275e67f21 --- /dev/null +++ b/addons/lunch/data/ir_cron_data.xml @@ -0,0 +1,17 @@ +<?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">10</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..0f269f42d0c 100644 --- a/addons/lunch/data/lunch_data.xml +++ b/addons/lunch/data/lunch_data.xml @@ -1,23 +1,12 @@ <?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"> <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="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"> @@ -25,7 +14,7 @@ if records: <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">records.cancel()</field> + <field name="code">records.action_cancel()</field> </record> </data> <data noupdate="0"> diff --git a/addons/lunch/data/lunch_demo.xml b/addons/lunch/data/lunch_demo.xml index bd36db1f0af..f91a57a183d 100644 --- a/addons/lunch/data/lunch_demo.xml +++ b/addons/lunch/data/lunch_demo.xml @@ -28,7 +28,7 @@ <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 +37,13 @@ <field name="email">coin.gourmand@yourcompany.example.com</field> </record> - <record model="res.partner" id="partner_pizza_inn"> + <record model="lunch.supplier" id="supplier_coin_gourmand"> + <field name="partner_id" ref="partner_coin_gourmand"/> + <field name="recurrency_from">8</field> + <field name="recurrency_to">23</field> + </record> + + <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"/> @@ -56,6 +62,12 @@ <field name="email">info@corner.com</field> </record> + <record model="lunch.supplier" id="supplier_corner"> + <field name="partner_id" ref="partner_corner"/> + <field name="recurrency_from">8</field> + <field name="recurrency_to">23</field> + </record> + <record model="res.partner" id="partner_sushi_shop"> <field name="name">Sushi Shop</field> <field name="city">Paris</field> @@ -65,178 +77,228 @@ <field name="email">order@sushi.com</field> </record> + <record model="lunch.supplier" id="supplier_sushi_shop"> + <field name="partner_id" ref="partner_sushi_shop"/> + <field name="recurrency_from">8</field> + <field name="recurrency_to">23</field> + </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"/> </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"/> </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"/> </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"/> </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"/> + </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_send" eval="True"/> + <field name="automatic_email_time">11</field> + <field name="recurrency_from">8</field> + <field name="recurrency_to">23</field> + <field name="recurrency">reccurent</field> + <field name="recurrency_monday" eval="True"/> + <field name="recurrency_tuesday" eval="True"/> + <field name="recurrency_wednesday" eval="True"/> + <field name="recurrency_thursday" eval="True"/> + <field name="recurrency_friday" eval="True"/> </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.product" id="product_olives"> + <field name="name">Olives</field> + <field name="price">0.30</field> + <field name="supplier_id" eval="str(ref('supplier_pizza_inn'))"/> + <field name="is_topping" eval="True"/> </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"> @@ -244,6 +306,7 @@ <field name="date" eval="(DateTime.now() + timedelta(days=2)).strftime('%Y-%m-%d')"/> <field name="order_line_ids" eval="[]"/> <field name="state">new</field> + <field name="mail_sent" eval="True"/> </record> <record model="lunch.order" id="order_2"> @@ -251,6 +314,7 @@ <field name="date" eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d')"/> <field name="order_line_ids" eval="[]"/> <field name="state">confirmed</field> + <field name="mail_sent" eval="True"/> </record> <record model="lunch.order" id="order_3"> @@ -258,6 +322,7 @@ <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="mail_sent" eval="True"/> </record> <record model="lunch.order" id="order_4"> @@ -273,9 +338,9 @@ <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="supplier_id" ref="supplier_pizza_inn"/> <field name="order_id" ref="order_1"/> + <field name="quantity">1</field> </record> <record model="lunch.order.line" id="order_line_2"> @@ -284,9 +349,9 @@ <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="supplier_id" ref="supplier_pizza_inn"/> <field name="order_id" ref="order_2"/> + <field name="quantity">1</field> </record> <record model="lunch.order.line" id="order_line_3"> @@ -295,9 +360,9 @@ <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="supplier_id" ref="supplier_coin_gourmand"/> <field name="order_id" ref="order_3"/> + <field name="quantity">1</field> </record> <record model="lunch.order.line" id="order_line_4"> @@ -306,9 +371,9 @@ <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="supplier_id" ref="supplier_coin_gourmand"/> <field name="order_id" ref="order_3"/> + <field name="quantity">1</field> </record> <record model="lunch.order.line" id="order_line_5"> @@ -317,7 +382,7 @@ <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="supplier_id" ref="supplier_pizza_inn"/> <field name="order_id" ref="order_4"/> </record> @@ -326,7 +391,7 @@ <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="order_line_id" ref="order_line_1"/> <field name="state">order</field> </record> @@ -335,7 +400,7 @@ <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="order_line_id" ref="order_line_2"/> <field name="state">order</field> </record> @@ -360,22 +425,8 @@ <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="order_line_id" ref="order_line_3"/> <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> - </record> </data> </odoo> diff --git a/addons/lunch/models/__init__.py b/addons/lunch/models/__init__.py index f99750c87d0..58a34e7891b 100644 --- a/addons/lunch/models/__init__.py +++ b/addons/lunch/models/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from . import lunch_alert from . import lunch_cashmove from . import lunch_order from . import lunch_product +from . import lunch_supplier diff --git a/addons/lunch/models/lunch_alert.py b/addons/lunch/models/lunch_alert.py deleted file mode 100644 index 325a64f4385..00000000000 --- a/addons/lunch/models/lunch_alert.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import datetime - -from odoo import api, fields, models, _ - - -class LunchAlert(models.Model): - """ Alerts to display during a lunch order. An alert can be specific to a - given day, weekly or daily. The alert is displayed from start to end hour. """ - _name = 'lunch.alert' - _description = 'Lunch Alert' - _rec_name = 'message' - - display = fields.Boolean(compute='_compute_display_get') - message = fields.Text('Message', required=True) - alert_type = fields.Selection([('specific', 'Specific Day'), - ('week', 'Every Week'), - ('days', 'Every Day')], - string='Recurrence', required=True, index=True, default='specific') - partner_id = fields.Many2one('res.partner', string="Vendor", - help="If specified, the selected vendor can be ordered only on selected days") - specific_day = fields.Date('Day', default=fields.Date.context_today) - monday = fields.Boolean('Monday') - tuesday = fields.Boolean('Tuesday') - wednesday = fields.Boolean('Wednesday') - thursday = fields.Boolean('Thursday') - friday = fields.Boolean('Friday') - saturday = fields.Boolean('Saturday') - sunday = fields.Boolean('Sunday') - start_hour = fields.Float('Between', oldname='active_from', required=True, default=7) - end_hour = fields.Float('And', oldname='active_to', required=True, default=23) - active = fields.Boolean(default=True) - - @api.multi - def name_get(self): - return [(alert.id, '%s %s' % (_('Alert'), '#%d' % alert.id)) for alert in self] - - @api.depends('alert_type', 'specific_day', 'monday', 'tuesday', 'thursday', - 'friday', 'saturday', 'sunday', 'start_hour', 'end_hour') - def _compute_display_get(self): - """ - This method check if the alert can be displayed today - if alert type is specific : compare specific_day(date) with today's date - if alert type is week : check today is set as alert (checkbox true) eg. self['monday'] - if alert type is day : True - return : Message if can_display_alert is True else False - """ - days_codes = {'0': 'sunday', - '1': 'monday', - '2': 'tuesday', - '3': 'wednesday', - '4': 'thursday', - '5': 'friday', - '6': 'saturday'} - fullday = False - now = datetime.datetime.now() - if self.env.context.get('lunch_date'): - # lunch_date is a fields.Date -> 00:00:00 - lunch_date = fields.Datetime.from_string(self.env.context['lunch_date']) - # if lunch_date is in the future, planned lunch, ignore hours - fullday = lunch_date > now - now = max(lunch_date, now) - mynow = fields.Datetime.context_timestamp(self, now) - - for alert in self: - can_display_alert = { - 'specific': (str(alert.specific_day) == fields.Date.to_string(mynow)), - 'week': alert[days_codes[mynow.strftime('%w')]], - 'days': True - } - - if can_display_alert[alert.alert_type]: - hour_to = int(alert.end_hour) - min_to = int((alert.end_hour - hour_to) * 60) - to_alert = datetime.time(hour_to, min_to) - hour_from = int(alert.start_hour) - min_from = int((alert.start_hour - hour_from) * 60) - from_alert = datetime.time(hour_from, min_from) - - if fullday or (from_alert <= mynow.time() <= to_alert): - alert.display = True - else: - alert.display = False diff --git a/addons/lunch/models/lunch_cashmove.py b/addons/lunch/models/lunch_cashmove.py index 7d4a6b56ae4..2cb71a77c69 100644 --- a/addons/lunch/models/lunch_cashmove.py +++ b/addons/lunch/models/lunch_cashmove.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models, _ +from odoo.tools import float_round class LunchCashMove(models.Model): @@ -14,10 +15,14 @@ class LunchCashMove(models.Model): date = fields.Date('Date', required=True, default=fields.Date.context_today) amount = fields.Float('Amount', required=True, help='Can be positive (payment) or negative (order or payment if user wants to get his money back)') description = fields.Text('Description', help='Can be an order or a payment') - order_id = fields.Many2one('lunch.order.line', 'Order', ondelete='cascade') + order_line_id = fields.Many2one('lunch.order.line', 'Order', ondelete='cascade') state = fields.Selection([('order', 'Order'), ('payment', 'Payment')], 'Is an order or a payment', default='payment') @api.multi def name_get(self): return [(cashmove.id, '%s %s' % (_('Lunch Cashmove'), '#%d' % cashmove.id)) for cashmove in self] + + @api.model + def get_wallet_balance(self, user): + return float_round(sum(move['amount'] for move in self.search_read([('user_id', '=', user.id)], ['amount'])), precision_digits=2) diff --git a/addons/lunch/models/lunch_order.py b/addons/lunch/models/lunch_order.py index f4354913db8..623a9cada50 100644 --- a/addons/lunch/models/lunch_order.py +++ b/addons/lunch/models/lunch_order.py @@ -1,19 +1,15 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from collections import OrderedDict - -import json - from odoo import api, fields, models, _ -from odoo.exceptions import AccessError, ValidationError +from odoo.exceptions import UserError, ValidationError from odoo.addons import decimal_precision as dp class LunchOrder(models.Model): """ A lunch order contains one or more lunch order line(s). It is associated to a user for a given - date. When creating a lunch order, applicable lunch alerts are displayed. + date. """ _name = 'lunch.order' _description = 'Lunch Order' @@ -39,24 +35,29 @@ class LunchOrder(models.Model): states={'new': [('readonly', False)], False: [('readonly', False)]}) total = fields.Float(compute='_compute_total', string="Total", store=True) state = fields.Selection([('new', 'New'), + ('ordered', 'Ordered'), ('confirmed', 'Received'), ('cancelled', 'Cancelled')], - 'Status', readonly=True, index=True, copy=False, - compute='_compute_order_state', store=True) - alerts = fields.Text(compute='_compute_alerts_get', string="Alerts") + 'Status', readonly=True, index=True, copy=False, default='new') company_id = fields.Many2one('res.company', related='user_id.company_id', store=True, readonly=False) currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True, store=True) cash_move_balance = fields.Monetary(compute='_compute_cash_move_balance', multi='cash_move_balance') balance_visible = fields.Boolean(compute='_compute_cash_move_balance', multi='cash_move_balance') - previous_order_ids = fields.Many2many('lunch.order.line', compute='_compute_previous_order') - previous_order_widget = fields.Text(compute='_compute_previous_order') - @api.one - @api.depends('order_line_ids') + mail_sent = fields.Boolean('Mail Sent', readonly=True) + supplier_ids = fields.Many2many('lunch.supplier', compute='_compute_supplier_ids', store=True) + + @api.depends('order_line_ids', 'order_line_ids.supplier_id', 'order_line_ids.product_id') + def _compute_supplier_ids(self): + for order in self: + order.supplier_ids = order.mapped('order_line_ids.supplier_id') + + @api.depends('order_line_ids', 'order_line_ids.quantity', 'order_line_ids.price') def _compute_total(self): """ get and sum the order lines' price """ + self.ensure_one() self.total = sum( orderline.price for orderline in self.order_line_ids) @@ -64,96 +65,55 @@ class LunchOrder(models.Model): def name_get(self): return [(order.id, '%s %s' % (_('Lunch Order'), '#%d' % order.id)) for order in self] - @api.depends('state') - def _compute_alerts_get(self): - """ - get the alerts to display on the order form - """ - alert_msg = [alert.message - for alert in self.env['lunch.alert'].search([]) - if alert.display] - - if self.state == 'new': - self.alerts = alert_msg and '\n'.join(alert_msg) or False - - @api.multi - @api.depends('user_id', 'state') - def _compute_previous_order(self): - self.ensure_one() - self.previous_order_widget = json.dumps(False) - - prev_order = self.env['lunch.order.line'].search([('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='date desc, id desc') - # If we use prev_order.ids, we will have duplicates (identical orders). - # Therefore, this following part removes duplicates based on product_id and note. - self.previous_order_ids = list({ - (order.product_id, order.note): order.id - for order in prev_order - }.values()) - - if self.previous_order_ids: - lunch_data = {} - for line in self.previous_order_ids: - lunch_data[line.id] = { - 'line_id': line.id, - 'product_id': line.product_id.id, - 'product_name': line.product_id.name, - 'supplier': line.supplier.name, - 'note': line.note, - 'price': line.price, - 'date': fields.Date.to_string(line.date), - 'currency_id': line.currency_id.id, - } - # sort the old lunch orders by (date, id) - lunch_data = OrderedDict(sorted(lunch_data.items(), key=lambda t: (t[1]['date'], t[0]), reverse=True)) - self.previous_order_widget = json.dumps(lunch_data) - - @api.one @api.depends('user_id') def _compute_cash_move_balance(self): + self.ensure_one() domain = [('user_id', '=', self.user_id.id)] lunch_cash = self.env['lunch.cashmove'].read_group(domain, ['amount', 'user_id'], ['user_id']) - if len(lunch_cash): + if lunch_cash: self.cash_move_balance = lunch_cash[0]['amount'] self.balance_visible = (self.user_id == self.env.user) or self.user_has_groups('lunch.group_lunch_manager') - @api.one @api.constrains('date') def _check_date(self): """ Prevents the user to create an order in the past """ + self.ensure_one() date_order = self.date date_today = fields.Date.context_today(self) if date_order < date_today: raise ValidationError(_('The date of your order is in the past.')) - @api.one - @api.depends('order_line_ids.state') - def _compute_order_state(self): - """ - Update the state of lunch.order based on its orderlines. Here is the logic: - - if at least one order line is cancelled, the order is set as cancelled - - if no line is cancelled but at least one line is not confirmed, the order is set as new - - if all lines are confirmed, the order is set as confirmed - """ - if not self.order_line_ids: - self.state = 'new' - else: - isConfirmed = True - for orderline in self.order_line_ids: - if orderline.state == 'cancelled': - self.state = 'cancelled' - return - elif orderline.state == 'confirmed': - continue - else: - isConfirmed = False + def action_order(self): + for order in self: + for line in order.order_line_ids: + self.env['lunch.cashmove'].sudo().create({ + 'user_id': line.user_id.id, + 'amount': -line.price, + 'description': line.product_id.name, + 'order_line_id': line.id, + 'state': 'order', + 'date': line.date, + }) + + self.mapped('order_line_ids').write({'state': 'ordered'}) + self.write({'state': 'ordered'}) - if isConfirmed: - self.state = 'confirmed' + def action_confirm(self, supplier=None): + for order in self: + if supplier: + order.order_line_ids.filtered(lambda line: line.state == 'ordered' and line.supplier_id == supplier).write({'state': 'confirmed'}) else: - self.state = 'new' - return + order.order_line_ids.filtered(lambda line: line.state == 'ordered').write({'state': 'confirmed'}) + order.write({'state': 'confirmed', + 'mail_sent': True}) + + def action_cancel(self): + for order in self: + # RLI FIXME maybe we don't need to filter + order.order_line_ids.filtered(lambda line: line.state != 'confirmed').action_cancel() + order.write({'state': 'cancelled'}) class LunchOrderLine(models.Model): @@ -163,153 +123,58 @@ class LunchOrderLine(models.Model): name = fields.Char(related='product_id.name', string="Product Name", readonly=True) order_id = fields.Many2one('lunch.order', 'Order', ondelete='cascade', required=True) - product_id = fields.Many2one('lunch.product', 'Product', required=True, - domain=[('available', '=', True)]) + topping_ids = fields.Many2many('lunch.product', string='Toppings') + product_id = fields.Many2one('lunch.product', string="Product", domain=[('is_topping', '=', False)], required=True) category_id = fields.Many2one('lunch.product.category', string='Product Category', related='product_id.category_id', readonly=True, store=True) date = fields.Date(string='Date', related='order_id.date', readonly=True, store=True) - supplier = fields.Many2one('res.partner', string='Vendor', related='product_id.supplier', - readonly=True, store=True) + supplier_id = fields.Many2one('lunch.supplier', string='Vendor', related='product_id.supplier_id', + readonly=True, store=True, index=True) user_id = fields.Many2one('res.users', string='User', related='order_id.user_id', readonly=True, store=True) note = fields.Text('Note') - price = fields.Float(related='product_id.price', readonly=True, store=True, + price = fields.Float('Total Price', compute='_compute_total_price', readonly=True, store=True, digits=dp.get_precision('Account')) state = fields.Selection([('new', 'New'), - ('confirmed', 'Received'), ('ordered', 'Ordered'), + ('confirmed', 'Received'), ('cancelled', 'Cancelled')], 'Status', readonly=True, index=True, default='new') - cashmove = fields.One2many('lunch.cashmove', 'order_id', 'Cash Move') + cashmove = fields.One2many('lunch.cashmove', 'order_line_id', 'Cash Move') currency_id = fields.Many2one('res.currency', related='order_id.currency_id', readonly=False) - - 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) - - template = self.env.ref('lunch.lunch_order_mail_supplier', raise_if_not_found=False) - ctx = dict( - default_composition_mode='mass_mail', - default_use_template=bool(template), - default_template_id=template.id, - default_lang=order['supplier'].lang or self.env.user.lang, - order=order, - lines=lines, - ) - return { - 'type': 'ir.actions.act_window', - 'view_type': 'form', - 'view_mode': 'form', - 'res_model': 'mail.compose.message', - 'target': 'new', - 'context': ctx, - } - - else: - raise AccessError(_("Only your lunch manager processes the orders.")) - - @api.one - def confirm(self): - """ - confirm one or more order line, update order status and create new cashmove - """ - if self.user_has_groups("lunch.group_lunch_manager"): - if self.state != 'confirmed': - values = { - 'user_id': self.user_id.id, - 'amount': -self.price, - 'description': self.product_id.name, - 'order_id': self.id, - 'state': 'order', - 'date': self.date, - } - self.env['lunch.cashmove'].create(values) - self.state = 'confirmed' - else: - raise AccessError(_("Only your lunch manager sets the orders as received.")) - - @api.one - def cancel(self): - """ - cancel one or more order.line, update order status and unlink existing cashmoves - """ - if self.user_has_groups("lunch.group_lunch_manager"): - self.state = 'cancelled' - self.cashmove.unlink() - else: - raise AccessError(_("Only your lunch manager cancels the orders.")) + quantity = fields.Float('Quantity', required=True, default=1) + + @api.depends('topping_ids', 'product_id', 'quantity') + def _compute_total_price(self): + for line in self: + line.price = line.quantity * sum(product.price for product in line.product_id | line.topping_ids) + + def update_quantity(self, increment): + for line in self: + old_cost = line.price + line.quantity += increment + new_cost = line.price + if new_cost - old_cost > self.env['lunch.cashmove'].get_wallet_balance(line.user_id): + raise UserError(_('Your wallet does not contain enough money to order that.' + 'To add some money to your wallet, please contact your lunch manager.')) + cashmove = self.env['lunch.cashmove'].search([('order_line_id', '=', line.id)], limit=1) + if cashmove: + cashmove.amount = -new_cost + if line.quantity <= 0: + order_id = line.order_id + line.sudo().unlink() + if not order_id.order_line_ids: + order_id.sudo().unlink() + + def action_confirm(self): + self.write({'state': 'confirmed'}) + + for order in self.mapped('order_id'): + if all(line.state == 'confirmed' for line in order.order_line_ids): + order.write({'state': 'confirmed', 'mail_sent': True}) + + def action_cancel(self): + for line in self: + line.cashmove.sudo().unlink() + + self.write({'state': 'cancelled'}) diff --git a/addons/lunch/models/lunch_product.py b/addons/lunch/models/lunch_product.py index 8ded108fc81..1751ea7a6e1 100644 --- a/addons/lunch/models/lunch_product.py +++ b/addons/lunch/models/lunch_product.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models, _ +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models, tools, _ from odoo.addons import decimal_precision as dp -from odoo.osv import expression class LunchProductCategory(models.Model): @@ -20,44 +21,52 @@ class LunchProduct(models.Model): _name = 'lunch.product' _description = 'Lunch Product' - name = fields.Char('Product', required=True) - category_id = fields.Many2one('lunch.product.category', 'Product Category', required=True) + name = fields.Char('Name', required=True) + category_id = fields.Many2one('lunch.product.category', 'Product Category') description = fields.Text('Description') - price = fields.Float('Price', digits=dp.get_precision('Account')) - supplier = fields.Many2one('res.partner', 'Vendor') + price = fields.Float('Price', digits=dp.get_precision('Account'), required=True) + supplier_id = fields.Many2one('lunch.supplier', 'Vendor', required=True) active = fields.Boolean(default=True) - available = fields.Boolean(compute='_get_available_product', search='_search_available_products') + is_topping = fields.Boolean("This product is an extra garniture") + + # image: all image fields are base64 encoded and PIL-supported + image = fields.Binary( + "Image", attachment=True, + help="This field holds the image used as image for the product, limited to 1024x1024px.") + image_medium = fields.Binary( + "Medium-sized image", attachment=True, + help="Medium-sized image of the product. It is automatically " + "resized as a 128x128px image, with aspect ratio preserved, " + "only when the image exceeds one of those sizes. Use this field in form views or some kanban views.") + image_small = fields.Binary( + "Small-sized image", attachment=True, + help="Small-sized image of the product. It is automatically " + "resized as a 64x64px image, with aspect ratio preserved. " + "Use this field anywhere a small image is required.") + + already_ordered = fields.Boolean('Has Already Been Ordered', compute='_compute_already_ordered') + + @api.model + def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None): + res = super(LunchProduct, self).search_read(domain, fields, offset, limit, order) + + if not order and 'already_ordered' in fields: + res = sorted(res, key=lambda x: x['already_ordered'], reverse=True) + + return res + + @api.model_create_multi + def create(self, vals_list): + for values in vals_list: + tools.image_resize_images(values) + return super(LunchProduct, self).create(vals_list) + + def write(self, values): + tools.image_resize_images(values) + return super(LunchProduct, self).write(values) - @api.depends('supplier') - def _get_available_product(self): + def _compute_already_ordered(self): + last_ordered = fields.Date.today() - relativedelta(weeks=2) for product in self: - if not product.supplier: - product.available = True - else: - alerts = self.env['lunch.alert'].search([ - ('partner_id', '=', self.supplier.id) - ]) - if alerts and not any(alert.display for alert in alerts): - # every alert is not available - product.available = False - else: - # no alert for the supplier or at least one is not available - product.available = True - - def _search_available_products(self, operator, value): - alerts = self.env['lunch.alert'].search([]) - supplier_w_alerts = alerts.mapped('partner_id') - available_suppliers = alerts.filtered(lambda a: a.display).mapped('partner_id') - available_products = self.search([ - '|', - ('supplier', 'not in', supplier_w_alerts.ids), - ('supplier', 'in', available_suppliers.ids) - ]) - - if (operator in expression.NEGATIVE_TERM_OPERATORS and value) or \ - (operator not in expression.NEGATIVE_TERM_OPERATORS and not value): - # e.g. (available = False) or (available != True) - return [('id', 'not in', available_products.ids)] - else: - # e.g. (available = True) or (available != False) - return [('id', 'in', available_products.ids)] + product.already_ordered = bool(self.env['lunch.order.line'].search_count( + [('product_id', '=', product.id), ('date', '>=', last_ordered), ('user_id', '=', self.env.user.id)])) diff --git a/addons/lunch/models/lunch_supplier.py b/addons/lunch/models/lunch_supplier.py new file mode 100644 index 00000000000..07286aa1ff0 --- /dev/null +++ b/addons/lunch/models/lunch_supplier.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import math +import pytz + +from datetime import datetime, time + +from odoo import api, fields, models +from odoo.osv import expression +from odoo.tools import float_round + +from odoo.addons.base.models.res_partner import _tz_get + + +WEEKDAY_TO_NAME = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] + +def float_to_time(hours, tz=None): + """ Convert a number of hours into a time object. """ + if hours == 24.0: + return time.max + fractional, integral = math.modf(hours) + res = time(int(integral), int(float_round(60 * fractional, precision_digits=0)), 0) + if tz: + res = res.replace(tzinfo=pytz.timezone(tz)) + return res + +def time_to_float(t): + return float_round(t.hour + t.minute/60 + t.second/3600, precision_digits=2) + +class LunchSupplier(models.Model): + _name = 'lunch.supplier' + _description = 'Lunch Supplier' + + partner_id = fields.Many2one('res.partner', string='Linked partner', required=True) + + name = fields.Char('Name', related='partner_id.name') + description = fields.Text('Description') + + email = fields.Char(related='partner_id.email') + email_formatted = fields.Char(related='partner_id.email_formatted', readonly=True) + phone = fields.Char(related='partner_id.phone') + street = fields.Char(related='partner_id.street') + street2 = fields.Char(related='partner_id.street2') + zip_code = fields.Char(related='partner_id.zip') + city = fields.Char(related='partner_id.city') + state_id = fields.Many2one("res.country.state", related='partner_id.state_id') + country_id = fields.Many2one('res.country', related='partner_id.country_id') + + vat = fields.Char(related='partner_id.vat') + + image = fields.Binary(related='partner_id.image', readonly=False) + image_medium = fields.Binary(related='partner_id.image_medium', readonly=False) + image_small = fields.Binary(related='partner_id.image_small', readonly=False) + + send_by = fields.Selection([ + ('phone', 'Phone'), + ('mail', 'Email'), + ], 'Send Order By', default='phone') + automatic_email_send = fields.Boolean('Automatic Email Sending') + automatic_email_time = fields.Float('Hour') + + recurrency = fields.Selection([('once', 'Specific Day'), ('reccurent', 'Reccurent')], 'Recurrency', default='once') + recurrency_from = fields.Float('From') + recurrency_to = fields.Float('To') + recurrency_date = fields.Date('Day', default=fields.Date.today()) + recurrency_date_from = fields.Datetime('from', compute='_compute_recurrency_date_from', store=True) + recurrency_date_to = fields.Datetime('to', compute='_compute_recurrency_date_to', store=True) + recurrency_monday = fields.Boolean('Monday') + recurrency_tuesday = fields.Boolean('Tuesday') + recurrency_wednesday = fields.Boolean('Wednesday') + recurrency_thursday = fields.Boolean('Thursday') + recurrency_friday = fields.Boolean('Friday') + recurrency_saturday = fields.Boolean('Saturday') + recurrency_sunday = fields.Boolean('Sunday') + + available_today = fields.Boolean('This is True when if the supplier is available today', + compute='_compute_available_today', search='_search_available_today') + + tz = fields.Selection(_tz_get, string='Timezone', required=True, default='UTC') + + _sql_constraints = [ + ('automatic_email_time_range', 'CHECK(automatic_email_time >= 0 AND automatic_email_time <= 24)', 'Automatic Email Sending Time should be between 0 and 24'), + ('recurrency_from', 'CHECK(recurrency_from >= 0 AND recurrency_from <= 24)', 'Recurrency From should be between 0 and 24'), + ('recurrency_to', 'CHECK(recurrency_to >= 0 AND recurrency_to <= 24)', 'Recurrency To should be between 0 and 24') + ] + + @api.model + def _auto_email_send(self): + """ + This method is called every 10 minutes via a cron. + Its job is simply to get all the orders made for each supplier and send an email + automatically to the supplier if the supplier is configured for it and we are ready + to send it (usually at 11am or so) + """ + records = self.search([('automatic_email_send', '=', True)]) + + for supplier in records: + send_at = datetime.combine(fields.Date.today(), float_to_time(supplier.automatic_email_time, supplier.tz)).astimezone(pytz.UTC).replace(tzinfo=None) + if supplier.available_today and fields.Datetime.now() > send_at: + orders = self.env['lunch.order'].search([('mail_sent', '=', False), ('supplier_ids', 'in', [supplier.id]), ('state', '=', 'ordered')]) + + if orders: + order = { + 'company_name': orders[0].company_id.name, + 'currency_id': orders[0].currency_id.id, + 'supplier_id': supplier.partner_id.id, + 'supplier_name': supplier.name, + 'supplier_email': supplier.email_formatted + } + + lines = orders.mapped('order_line_ids').filtered(lambda line: line.supplier_id == supplier) + lines = [{'product': line.product_id.name, 'note': line.note, 'quantity': line.quantity, 'price': line.price} for line in lines] + + order['amount_total'] = sum(line['price'] for line in lines) + + self.env.ref('lunch.lunch_order_mail_supplier').with_context(order=order, lines=lines).send_mail(supplier.id) + + orders.action_confirm(supplier) + + @api.depends('recurrency_date', 'recurrency_from') + def _compute_recurrency_date_from(self): + for supplier in self: + if supplier.recurrency_date and supplier.recurrency_from: + supplier.recurrency_date_from = datetime.combine(supplier.recurrency_date, float_to_time(supplier.recurrency_from)) + + @api.depends('recurrency_date', 'recurrency_to') + def _compute_recurrency_date_to(self): + for supplier in self: + if supplier.recurrency_date and supplier.recurrency_to: + supplier.recurrency_date_to = datetime.combine(supplier.recurrency_date, float_to_time(supplier.recurrency_to)) + + @api.depends('recurrency', 'recurrency_date', 'recurrency_from', 'recurrency_to', 'recurrency_monday', + 'recurrency_tuesday', 'recurrency_wednesday', 'recurrency_thursday', + 'recurrency_friday', 'recurrency_saturday', 'recurrency_sunday') + def _compute_available_today(self): + now = fields.Datetime.now().replace(tzinfo=pytz.UTC) + + for supplier in self: + now = now.astimezone(pytz.timezone(supplier.tz)) + time_from = float_to_time(supplier.recurrency_from) + time_to = float_to_time(supplier.recurrency_to) + + if supplier.recurrency == 'once': + supplier.available_today = (supplier.reccurrency_date_from <= now <= supplier.reccurrency_date_to) + else: + fieldname = 'recurrency_%s' % (WEEKDAY_TO_NAME[now.weekday()]) + supplier.available_today = supplier[fieldname] and (time_from <= now.time() <= time_to) + + def _search_available_today(self, operator, value): + if (not operator in ['=', '!=']) or (not value in [True, False]): + return [] + + searching_for_true = (operator == '=' and value) or (operator == '!=' and not value) + now = fields.Datetime.now() + float_now = time_to_float(now.time()) + fieldname = 'recurrency_%s' % (WEEKDAY_TO_NAME[now.weekday()]) + + if searching_for_true: + specific = expression.AND([ + [('recurrency', '=', 'once')], + [('recurrency_date_from', '<=', now)], + [('recurrency_date_to', '>=', now)] + ]) + else: + specific = expression.AND([ + [('recurrency', '=', 'once')], + expression.OR([ + [('recurrency_date_from', '>=', now)], + [('recurrency_date_to', '<=', now)] + ]) + ]) + + recurrence = expression.AND([ + [(fieldname, operator, value)], + [('recurrency_from', '<=' if searching_for_true else '>=', float_now)], + [('recurrency_to', '>=' if searching_for_true else '<=', float_now)] + ]) + + return expression.OR([specific, recurrence]) diff --git a/addons/lunch/security/ir.model.access.csv b/addons/lunch/security/ir.model.access.csv index 244fbf11f18..1efb5be6c6d 100644 --- a/addons/lunch/security/ir.model.access.csv +++ b/addons/lunch/security/ir.model.access.csv @@ -4,10 +4,10 @@ order_line_manager,"Order Line user",model_lunch_order_line,group_lunch_manager, cashmove_manager,"Cashmove user",model_lunch_cashmove,group_lunch_manager,1,1,1,1 product_manager,"Product user",model_lunch_product,group_lunch_manager,1,1,1,1 product_category_manager,"Product category user",model_lunch_product_category,group_lunch_manager,1,1,1,1 -alert_manager,"Alert user",model_lunch_alert,group_lunch_manager,1,1,1,1 order_user,"Order user",model_lunch_order,group_lunch_user,1,1,1,0 order_line_user,"Order Line user",model_lunch_order_line,group_lunch_user,1,1,1,1 cashmove_user,"Cashmove user",model_lunch_cashmove,group_lunch_user,1,0,0,0 product_user,"Product user",model_lunch_product,group_lunch_user,1,0,0,0 product_category_user,"Product category user",model_lunch_product_category,group_lunch_user,1,0,0,0 -alert_user,"Alert user",model_lunch_alert,group_lunch_user,1,0,0,0 +lunch_supplier_user,"Lunch Supplier User Rights",model_lunch_supplier,group_lunch_user,1,0,0,0 +lunch_supplier_manager,"Lunch Supplier Manager Rights",model_lunch_supplier,group_lunch_manager,1,1,1,1 diff --git a/addons/lunch/static/img/4formaggio.png b/addons/lunch/static/img/4formaggio.png Binary files differnew file mode 100644 index 00000000000..f19c95ac934 --- /dev/null +++ b/addons/lunch/static/img/4formaggio.png diff --git a/addons/lunch/static/img/Coke.png b/addons/lunch/static/img/Coke.png Binary files differnew file mode 100644 index 00000000000..e822a2656ac --- /dev/null +++ b/addons/lunch/static/img/Coke.png diff --git a/addons/lunch/static/img/bacon_burger.png b/addons/lunch/static/img/bacon_burger.png Binary files differnew file mode 100644 index 00000000000..ad4177d8ac2 --- /dev/null +++ b/addons/lunch/static/img/bacon_burger.png diff --git a/addons/lunch/static/img/brie.png b/addons/lunch/static/img/brie.png Binary files differnew file mode 100644 index 00000000000..5b76bfbb1ee --- /dev/null +++ b/addons/lunch/static/img/brie.png diff --git a/addons/lunch/static/img/cheeseburger.png b/addons/lunch/static/img/cheeseburger.png Binary files differnew file mode 100644 index 00000000000..f9d5a3e5c01 --- /dev/null +++ b/addons/lunch/static/img/cheeseburger.png diff --git a/addons/lunch/static/img/chicken_curry.png b/addons/lunch/static/img/chicken_curry.png Binary files differnew file mode 100644 index 00000000000..09e494482d6 --- /dev/null +++ b/addons/lunch/static/img/chicken_curry.png diff --git a/addons/lunch/static/img/chirashi.png b/addons/lunch/static/img/chirashi.png Binary files differnew file mode 100644 index 00000000000..65143711848 --- /dev/null +++ b/addons/lunch/static/img/chirashi.png diff --git a/addons/lunch/static/img/club.png b/addons/lunch/static/img/club.png Binary files differnew file mode 100644 index 00000000000..5f3e36f3f37 --- /dev/null +++ b/addons/lunch/static/img/club.png diff --git a/addons/lunch/static/img/coke_zero.png b/addons/lunch/static/img/coke_zero.png Binary files differnew file mode 100644 index 00000000000..72dbba5542b --- /dev/null +++ b/addons/lunch/static/img/coke_zero.png diff --git a/addons/lunch/static/img/fanta.png b/addons/lunch/static/img/fanta.png Binary files differnew file mode 100644 index 00000000000..b7a48332e0d --- /dev/null +++ b/addons/lunch/static/img/fanta.png diff --git a/addons/lunch/static/img/fuze_black.png b/addons/lunch/static/img/fuze_black.png Binary files differnew file mode 100644 index 00000000000..1daac338d90 --- /dev/null +++ b/addons/lunch/static/img/fuze_black.png diff --git a/addons/lunch/static/img/fuze_green.png b/addons/lunch/static/img/fuze_green.png Binary files differnew file mode 100644 index 00000000000..9a274ab6796 --- /dev/null +++ b/addons/lunch/static/img/fuze_green.png diff --git a/addons/lunch/static/img/gouda.png b/addons/lunch/static/img/gouda.png Binary files differnew file mode 100644 index 00000000000..c50d6d8dfbf --- /dev/null +++ b/addons/lunch/static/img/gouda.png diff --git a/addons/lunch/static/img/italiana.png b/addons/lunch/static/img/italiana.png Binary files differnew file mode 100644 index 00000000000..2f8c4244436 --- /dev/null +++ b/addons/lunch/static/img/italiana.png diff --git a/addons/lunch/static/img/lipton.png b/addons/lunch/static/img/lipton.png Binary files differnew file mode 100644 index 00000000000..a1f8ef21f2e --- /dev/null +++ b/addons/lunch/static/img/lipton.png diff --git a/addons/lunch/static/img/maki.png b/addons/lunch/static/img/maki.png Binary files differnew file mode 100644 index 00000000000..c92dd8c83a4 --- /dev/null +++ b/addons/lunch/static/img/maki.png diff --git a/addons/lunch/static/img/mozza.png b/addons/lunch/static/img/mozza.png Binary files differnew file mode 100644 index 00000000000..8820fbb9cd5 --- /dev/null +++ b/addons/lunch/static/img/mozza.png diff --git a/addons/lunch/static/img/napoli.png b/addons/lunch/static/img/napoli.png Binary files differnew file mode 100644 index 00000000000..c7ecd65db8a --- /dev/null +++ b/addons/lunch/static/img/napoli.png diff --git a/addons/lunch/static/img/pasta_bolognese.png b/addons/lunch/static/img/pasta_bolognese.png Binary files differnew file mode 100644 index 00000000000..64f9972e5c2 --- /dev/null +++ b/addons/lunch/static/img/pasta_bolognese.png diff --git a/addons/lunch/static/img/pizza_funghi.png b/addons/lunch/static/img/pizza_funghi.png Binary files differnew file mode 100644 index 00000000000..00bceba96b6 --- /dev/null +++ b/addons/lunch/static/img/pizza_funghi.png diff --git a/addons/lunch/static/img/pizza_margherita.png b/addons/lunch/static/img/pizza_margherita.png Binary files differnew file mode 100644 index 00000000000..0e317265524 --- /dev/null +++ b/addons/lunch/static/img/pizza_margherita.png diff --git a/addons/lunch/static/img/pizza_veggie.png b/addons/lunch/static/img/pizza_veggie.png Binary files differnew file mode 100644 index 00000000000..950afb54a12 --- /dev/null +++ b/addons/lunch/static/img/pizza_veggie.png diff --git a/addons/lunch/static/img/salmon_sushi.png b/addons/lunch/static/img/salmon_sushi.png Binary files differnew file mode 100644 index 00000000000..1467b54f120 --- /dev/null +++ b/addons/lunch/static/img/salmon_sushi.png diff --git a/addons/lunch/static/img/temaki.png b/addons/lunch/static/img/temaki.png Binary files differnew file mode 100644 index 00000000000..8f47e7c035a --- /dev/null +++ b/addons/lunch/static/img/temaki.png diff --git a/addons/lunch/static/img/tuna_sandwich.png b/addons/lunch/static/img/tuna_sandwich.png Binary files differnew file mode 100644 index 00000000000..b616d6156a5 --- /dev/null +++ b/addons/lunch/static/img/tuna_sandwich.png diff --git a/addons/lunch/static/src/js/lunch.js b/addons/lunch/static/src/js/lunch.js deleted file mode 100644 index 8c455083b0d..00000000000 --- a/addons/lunch/static/src/js/lunch.js +++ /dev/null @@ -1,94 +0,0 @@ -odoo.define('lunch.previous_orders', function (require) { -"use strict"; - -var AbstractField = require('web.AbstractField'); -var core = require('web.core'); -var field_registry = require('web.field_registry'); -var field_utils = require('web.field_utils'); - -var QWeb = core.qweb; - -var LunchPreviousOrdersWidget = AbstractField.extend({ - events: { - 'click .o_add_button': '_onAddOrder', - }, - supportedFieldTypes: ['one2many'], - /** - * @override - */ - init: function () { - this._super.apply(this, arguments); - this.lunchData = JSON.parse(this.value); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Used by the widget to render the previous order price like a monetary. - * - * @private - * @param {Object} order - * @returns {string} the monetary formatting of order price - */ - _formatValue: function (order) { - var options = _.extend({}, this.nodeOptions, order); - return field_utils.format.monetary(order.price, this.field, options); - }, - /** - * @private - * @override - */ - _render: function () { - if (this.lunchData !== false) { - // group data by supplier for display - var categories = _.groupBy(this.lunchData, 'supplier'); - this.$el.html(QWeb.render('LunchPreviousOrdersWidgetList', { - formatValue: this._formatValue.bind(this), - categories: categories, - })); - } else { - return this.$el.html(QWeb.render('LunchPreviousOrdersWidgetNoOrder')); - } - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - * @param {MouseEvent} event - */ - _onAddOrder: function (event) { - // Get order details from line - var lineID = parseInt($(event.currentTarget).data('id')); - if (!lineID) { - return; - } - var values = { - product_id: { - id: this.lunchData[lineID].product_id, - display_name: this.lunchData[lineID].product_name, - }, - note: this.lunchData[lineID].note, - price: this.lunchData[lineID].price, - }; - - // create a new order line - this.trigger_up('field_changed', { - dataPointID: this.dataPointID, - changes: { - order_line_ids: { - operation: 'CREATE', - data: values, - }, - }, - }); - }, -}); - -field_registry.add('previous_order', LunchPreviousOrdersWidget); - -}); diff --git a/addons/lunch/static/src/js/lunch_kanban_controller.js b/addons/lunch/static/src/js/lunch_kanban_controller.js new file mode 100644 index 00000000000..5c9a0228f46 --- /dev/null +++ b/addons/lunch/static/src/js/lunch_kanban_controller.js @@ -0,0 +1,190 @@ +odoo.define('lunch.LunchKanbanController', function (require) { +"use strict"; + +/** + * This file defines the Controller for the Lunch Kanban view, which is an + * override of the KanbanController. + */ + +var core = require('web.core'); +var KanbanController = require('web.KanbanController'); +var LunchKanbanWidget = require('lunch.LunchKanbanWidget'); +var LunchPaymentDialog = require('lunch.LunchPaymentDialog'); +var session = require('web.session'); + +var qweb = core.qweb; +var _t = core._t; + +var LunchKanbanController = KanbanController.extend({ + custom_events: _.extend({}, KanbanController.prototype.custom_events, { + add_money: '_onAddMoney', + add_product: '_onAddProduct', + change_user: '_onUserChanged', + edit_order: '_onEditOrder', + open_wizard: '_onOpenWizard', + order_now: '_onOrderNow', + remove_product: '_onRemoveProduct', + save_order: '_onSaveOrder', + unlink_order: '_onUnlinkOrder', + }), + + /** + * @override + */ + init: function () { + this.userId = null; + this.editMode = false; + return this._super.apply(this, arguments); + }, + start: function () { + this.$el.addClass('o_lunch_kanban'); + return this._super.apply(this, arguments); + }, + + _fetchPaymentInfo: function (){ + return this._rpc({ + route: '/lunch/payment_message', + }); + }, + + _onAddMoney: function (ev) { + var self = this; + + ev.stopPropagation(); + + this._showPaymentDialog(_t('Your wallet is empty!')); + }, + _onAddProduct: function (ev) { + var self = this; + ev.stopPropagation(); + + this._rpc({ + model: 'lunch.order.line', + method: 'update_quantity', + args: [[ev.data.lineId], 1], + }).then(function () { + self.reload(); + }); + }, + _onEditOrder: function (ev) { + ev.stopPropagation(); + + this.editMode = true; + this.reload(); + }, + _onOpenWizard: function (ev) { + var self = this; + ev.stopPropagation(); + + var ctx = this.userId ? {default_user_id: this.userId}: {}; + + var options = { + on_close: function () { + self.reload(); + }, + }; + + this.do_action({ + res_model: 'lunch.order.line.temp', + type: 'ir.actions.act_window', + views: [[false, 'form']], + target: 'new', + context: _.extend(ctx, {default_product_id: ev.data.res_id}), + }, options); + }, + _onOrderNow: function (ev) { + var self = this; + ev.stopPropagation(); + + this._rpc({ + route: '/lunch/pay', + params: { + user_id: this.userId, + }, + }).then(function (isPaid) { + if (isPaid) { + // TODO: feedback? + self.reload(); + } else { + self._showPaymentDialog(); + self.reload(); + } + }); + }, + _onRemoveProduct: function (ev) { + var self = this; + ev.stopPropagation(); + + this._rpc({ + model: 'lunch.order.line', + method: 'update_quantity', + args: [[ev.data.lineId], -1], + }).then(function () { + self.reload(); + }); + }, + _onSaveOrder: function (ev) { + ev.stopPropagation(); + + this.editMode = false; + this.reload(); + }, + _onUserChanged: function (ev) { + ev.stopPropagation(); + + this.userId = ev.data.userId; + this.reload(); + }, + _onUnlinkOrder: function (ev) { + var self = this; + ev.stopPropagation(); + + this._rpc({ + route: '/lunch/trash', + params: { + user_id: this.userId, + }, + }).then(function () { + self.reload(); + }); + }, + _orderPaid: function () { + Dialog.alert(this, _t('Your order has been paid have a nice day'), {'title': 'Order Paid'}); + }, + _orderNotPaid: function () { + this._showPaymentDialog(); + }, + _showPaymentDialog: function (title) { + var self = this; + + title = title || ''; + + this._fetchPaymentInfo().then(function (data) { + var paymentDialog = new LunchPaymentDialog(self, _.extend(data, {title: title})); + paymentDialog.open(); + }); + }, + _update: function () { + var self = this; + + this._rpc({ + route: '/lunch/infos', + params: { + user_id: this.userId, + }, + }).then(function (data) { + if (self.widget) { + self.widget.destroy(); + } + data.wallet = parseFloat(data.wallet).toFixed(2); + self.widget = new LunchKanbanWidget(self, _.extend(data, {edit: self.editMode})); + self.widget.insertBefore(self.$('.o_kanban_view')); + }); + + return this._super.apply(this, arguments); + }, +}); + +return LunchKanbanController; + +}); diff --git a/addons/lunch/static/src/js/lunch_kanban_model.js b/addons/lunch/static/src/js/lunch_kanban_model.js new file mode 100644 index 00000000000..c28f5594b7a --- /dev/null +++ b/addons/lunch/static/src/js/lunch_kanban_model.js @@ -0,0 +1,19 @@ +odoo.define('lunch.LunchKanbanModel', function (require) { +"use strict"; + +/** + * This file defines the Model for the Lunch Kanban view, which is an + * override of the KanbanModel. + */ + +var KanbanModel = require('web.KanbanModel'); +var core = require('web.core'); + +var _t = core._t; + +var LunchKanbanModel = KanbanModel.extend({ +}); + +return LunchKanbanModel; + +}); diff --git a/addons/lunch/static/src/js/lunch_kanban_record.js b/addons/lunch/static/src/js/lunch_kanban_record.js new file mode 100644 index 00000000000..45a928b892d --- /dev/null +++ b/addons/lunch/static/src/js/lunch_kanban_record.js @@ -0,0 +1,22 @@ +odoo.define('lunch.LunchKanbanRecord', function (require) { +"use strict"; + +/** + * This file defines the KanbanRecord for the Lunch Kanban view. + */ + +var KanbanRecord = require('web.KanbanRecord'); + +var LunchKanbanRecord = KanbanRecord.extend({ + _onGlobalClick: function (ev) { + ev.preventDefault(); + // ignore clicks on oe_kanban_action elements + if (!$(ev.target).hasClass('oe_kanban_action')) { + this.trigger_up('open_wizard', {res_id: this.id}); + } + }, +}); + +return LunchKanbanRecord; + +}); diff --git a/addons/lunch/static/src/js/lunch_kanban_renderer.js b/addons/lunch/static/src/js/lunch_kanban_renderer.js new file mode 100644 index 00000000000..bffe639a655 --- /dev/null +++ b/addons/lunch/static/src/js/lunch_kanban_renderer.js @@ -0,0 +1,29 @@ +odoo.define('lunch.LunchKanbanRenderer', function (require) { +"use strict"; + +/** + * This file defines the Renderer for the Lunch Kanban view, which is an + * override of the KanbanRenderer. + */ + +var LunchKanbanRecord = require('lunch.LunchKanbanRecord'); + +var KanbanRenderer = require('web.KanbanRenderer'); + +var LunchKanbanRenderer = KanbanRenderer.extend({ + config: _.extend({}, KanbanRenderer.prototype.config, { + KanbanRecord: LunchKanbanRecord, + }), + + /** + * @override + */ + start: function () { + this.$el.addClass('o_lunch_kanban_view position-relative align-content-start flex-grow-1 flex-shrink-1'); + return this._super.apply(this, arguments); + }, +}); + +return LunchKanbanRenderer; + +}); diff --git a/addons/lunch/static/src/js/lunch_kanban_view.js b/addons/lunch/static/src/js/lunch_kanban_view.js new file mode 100644 index 00000000000..296ebac08cd --- /dev/null +++ b/addons/lunch/static/src/js/lunch_kanban_view.js @@ -0,0 +1,42 @@ +odoo.define('lunch.LunchKanbanView', function (require) { +"use strict"; + +var LunchKanbanController = require('lunch.LunchKanbanController'); +var LunchKanbanModel = require('lunch.LunchKanbanModel'); +var LunchKanbanRenderer = require('lunch.LunchKanbanRenderer'); + +var config = require('web.config'); +var core = require('web.core'); +var KanbanView = require('web.KanbanView'); +var view_registry = require('web.view_registry'); + +var _lt = core._lt; + +if (config.device.isMobile) { + // use the classical KanbanView in mobile + view_registry.add('lunch_kanban', KanbanView); + return; +} + +var LunchKanbanView = KanbanView.extend({ + config: _.extend({}, KanbanView.prototype.config, { + Controller: LunchKanbanController, + Model: LunchKanbanModel, + Renderer: LunchKanbanRenderer, + }), + display_name: _lt('Lunch Kanban'), + groupable: false, + + /** + * @override + */ + init: function () { + return this._super.apply(this, arguments); + }, +}); + +view_registry.add('lunch_kanban', LunchKanbanView); + +return LunchKanbanView; + +}); diff --git a/addons/lunch/static/src/js/lunch_kanban_widget.js b/addons/lunch/static/src/js/lunch_kanban_widget.js new file mode 100644 index 00000000000..cfc81ab8c47 --- /dev/null +++ b/addons/lunch/static/src/js/lunch_kanban_widget.js @@ -0,0 +1,87 @@ +odoo.define('lunch.LunchKanbanWidget', function (require) { +"use strict"; + +var core = require('web.core'); +var session = require('web.session'); +var Widget = require('web.Widget'); + +var qweb = core.qweb; + +var LunchKanbanWidget = Widget.extend({ + template: 'LunchKanbanWidget', + custom_events: { + }, + events: { + 'click .o_add_money': '_onAddMoney', + 'click .o_add_product': '_onAddProduct', + 'click .o_lunch_widget_edit': '_onEditOrder', + 'click .o_lunch_widget_order_button': '_onOrderNow', + 'click .o_remove_product': '_onRemoveProduct', + 'click .o_lunch_widget_save': '_onSaveOrder', + 'click .o_lunch_widget_unlink': '_onUnlinkOrder', + 'change select': '_onUserChanged', + }, + + init: function (parent, params) { + var self = this; + this._super.apply(this, arguments); + this.data = params; + this.currency = session.get_currency(session.company_currency_id); + }, + renderElement: function () { + var self = this; + this._super.apply(this, arguments); + this.$('select').val(this.data.username); + }, + + _onAddMoney: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.trigger_up('add_money', {}); + }, + _onAddProduct: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + + this.trigger_up('add_product', {lineId: $(ev.currentTarget).data('id')}); + }, + _onEditOrder: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + + this.trigger_up('edit_order', {}); + }, + _onOrderNow: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + + this.trigger_up('order_now', {}); + }, + _onRemoveProduct: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + + this.trigger_up('remove_product', {lineId: $(ev.currentTarget).data('id')}); + }, + _onSaveOrder: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + + this.trigger_up('save_order', {}); + }, + _onUnlinkOrder: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + + this.trigger_up('unlink_order', {}); + }, + _onUserChanged: function (ev) { + ev.stopPropagation(); + + this.trigger_up('change_user', {userId: $(ev.currentTarget).find(':selected').data('user-id')}); + } +}); + +return LunchKanbanWidget; + +}); diff --git a/addons/lunch/static/src/js/lunch_payment_dialog.js b/addons/lunch/static/src/js/lunch_payment_dialog.js new file mode 100644 index 00000000000..00e49299189 --- /dev/null +++ b/addons/lunch/static/src/js/lunch_payment_dialog.js @@ -0,0 +1,24 @@ +odoo.define('lunch.LunchPaymentDialog', function (require) { +"use strict"; + +var core = require('web.core'); +var Dialog = require('web.Dialog'); + +var qweb = core.qweb; + +var LunchPaymentDialog = Dialog.extend({ + template: 'lunch.LunchPaymentDialog', + + init: function (parent, options) { + var self = this; + this._super.apply(this, arguments); + + options = (options || {}); + + this.message = options.message || ''; + }, +}); + +return LunchPaymentDialog; + +}); diff --git a/addons/lunch/static/src/scss/lunch.scss b/addons/lunch/static/src/scss/lunch.scss deleted file mode 100644 index 9e36f20dbd8..00000000000 --- a/addons/lunch/static/src/scss/lunch.scss +++ /dev/null @@ -1,14 +0,0 @@ - -.o_web_client .o_lunch { - .o_lunch_last_orders { - width: 100%; - - .o_lunch_vignette { - padding: 5px 0; - - + .o_lunch_vignette { - border-top: 1px solid $o-brand-lightsecondary; - } - } - } -} diff --git a/addons/lunch/static/src/scss/lunch_kanban.scss b/addons/lunch/static/src/scss/lunch_kanban.scss new file mode 100644 index 00000000000..44bac01fa4b --- /dev/null +++ b/addons/lunch/static/src/scss/lunch_kanban.scss @@ -0,0 +1,48 @@ +.o_lunch_kanban { + .o_pink { + color: nth($o-colors, 10); + } + + .o_lunch_widget { + display: flex; + overflow: auto; + border-bottom: 1px solid #CED4DA; + background-color: white; + min-height: 90px; + max-height: 90px; + + .o_lunch_widget_info { + padding: 5px 12px; + margin: 4px 8px; + width: 300px; + flex: 1 1 auto; + overflow-y: auto; + + .o_lunch_ordered { + color: white; + background-color: #F0D970; + padding: 1px 3px; + border-radius: .25em; + } + + .o_lunch_confirmed { + color: white; + background-color: #00BA4E; + padding: 1px 3px; + border-radius: .25em; + } + + .o_lunch_widget_title { + font-weight: bold; + font-size: initial; + } + + .o_lunch_widget_order_button { + display: block; + width: 100%; + margin-bottom: 4px; + margin-top: 12px; + } + } + } +} diff --git a/addons/lunch/static/src/xml/lunch_kanban.xml b/addons/lunch/static/src/xml/lunch_kanban.xml new file mode 100644 index 00000000000..74a57f7cfec --- /dev/null +++ b/addons/lunch/static/src/xml/lunch_kanban.xml @@ -0,0 +1,118 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates> + <div class="o_lunch_widget" t-name="LunchKanbanWidget"> + <div class="o_lunch_widget_info d-flex flex-row"> + <div> + <img class="rounded-circle" t-attf-src="{{ widget.data.userimage }}"/> + </div> + <div class="w-50 ml-2 py-3"> + <div> + <t t-if="widget.data.is_manager"> + <select class="o_input"> + <t t-foreach="widget.data.users" t-as="user"> + <option t-attf-data-user-id="{{ user.id }}" t-attf-value="{{ user.name }}"><t t-esc="user.name"/></option> + </t> + </select> + </t> + <t t-else=""> + <t t-esc="widget.data.username"/> + </t> + </div> + <div class="d-flex flex-row"> + <span class="flex-grow-1"> + <i class="fa fa-money"/> + <t t-call="currency_field"> + <t t-set="value" t-value="widget.data.wallet"/> + <t t-set="currency" t-value="widget.currency"/> + </t> + </span> + <span> + <a class="o_add_money" href="#">Add money</a> + </span> + </div> + </div> + </div> + <div class="o_lunch_widget_info"> + <t t-if="widget.data.order"> + <t t-if="widget.data.state == 'ordered'"> + <t t-set="state_class" t-value="'o_lunch_ordered'"/> + </t> + <t t-else="widget.data.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.data.state != 'confirmed'" class="fa fa-trash o_lunch_widget_unlink"/></a> + <a t-if="widget.data.state == 'ordered' && !widget.data.edit" href="#" class="o_lunch_widget_edit">Edit</a> + <a t-if="widget.data.state == 'ordered' && widget.data.edit" href="#" class="o_lunch_widget_save">Save</a> + </div> + <div t-if="widget.data.state != 'new'" t-esc="widget.data.state" t-attf-class="flex-grow-0 {{ state_class }}"/> + </div> + <div t-foreach="widget.data.lines" t-as="line"> + <div class="d-flex flex-row"> + <div class="flex-grow-0"> + <a class="o_remove_product" t-if="widget.data.state == 'new' || widget.data.edit" 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.data.state == 'new' || widget.data.edit" t-attf-data-id="{{ line.id }}" href="#"><i class="fa fa-plus-circle"/></a> + </div> + <div class="flex-grow-1 pl-2"> + <span t-esc="line.product[0]"/> + </div> + <div class="flex-grow-0"> + <t t-call="currency_field"> + <t t-set="value" t-value="line.product[1]"/> + <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 ml-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> + </div> + </t> + </div> + <div class="o_lunch_widget_info"> + <t t-if="widget.data.order && widget.data.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.data.total"/> + <t t-set="currency" t-value="widget.currency"/> + </t> + </div> + </div> + <button class="btn btn-primary o_lunch_widget_order_button" t-attf-data-order-id="{{ widget.data.order }}">ORDER NOW</button> + </t> + </div> + </div> + + <t t-name="currency_field"> + <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..b2062609974 --- /dev/null +++ b/addons/lunch/static/tests/test_lunch_kanban.js @@ -0,0 +1,207 @@ +odoo.define('lunch.lunchKanbanTests', function (require) { +"use strict"; + +var LunchKanbanView = require('lunch.LunchKanbanView'); +var testUtils = require('web.test_utils'); + +var createView = testUtils.createView; + +QUnit.module('Views', { + beforeEach: function () { + this.data = { + product: { + fields: { + id: {string: 'ID', type: 'integer'}, + }, + records: [ + {id: 1, name: 'Tuna sandwich', price: 3.0}, + ] + }, + }; + }, +}, function () { + QUnit.module('LunchKanbanView'); + + QUnit.test('Simple rendering of LunchKanbanView', function (assert) { + assert.expect(8); + + var lunchKanban = createView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: '<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} + ] + }); + } else if (route.startsWith('data:image/png;base64,')) { + return $.when(); + } + return this._super(route, args); + }, + }); + + var $section = $(lunchKanban.$('.o_lunch_widget_info')[0]); + // username + assert.strictEqual($section.find('div:eq(1) div:eq(0)').text().trim(), 'Marc Demo', 'Username should have been Marc Demo'); + // wallet_balance + assert.strictEqual($section.find('div:eq(1) div:eq(1) span:eq(0)').text().trim(), '20.00', 'Wallet balance should have been 20'); + // your order section + $section = $(lunchKanban.$('.o_lunch_widget_info')[1]); + // only one line + assert.strictEqual($section.find('div:eq(2) div.d-flex').length, 1, 'There should be only one line'); + // quantity = 1 + assert.strictEqual($section.find('div:eq(2) div:eq(0) span:eq(0)').text().trim(), '1', 'The line should contain only one product'); + // buttons to remove and to add product + assert.strictEqual($section.find('.o_remove_product').length, 1, 'There should be a remove product button'); + assert.strictEqual($section.find('.o_add_product').length, 1, 'There should be a add product button'); + + $section = $(lunchKanban.$('.o_lunch_widget_info')[2]); + // total + assert.strictEqual($section.find('div:eq(0) div:eq(1)').text().trim(), '7.4', 'total should be of 7.4'); + // order now button + assert.strictEqual($section.find('button').length, 1, 'order now button should be available'); + + lunchKanban.destroy(); + }); + + QUnit.test('User interactions', function (assert) { + assert.expect(8); + + var state = 'new'; + + var lunchKanban = createView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: '<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: state, + lines: [ + {id: 1, product: ['Pizza Italiana', 7.4], toppings: [], quantity: 1.0, price: 7.4} + ] + }); + } else if (route === '/lunch/payment_message') { + return $.when({message: 'Hello'}); + } else if (route === '/lunch/pay') { + return $.when(true); + } else if (args.method === 'update_quantity') { + assert.step(args.args); + return $.when(); + } else if (route.startsWith('data:image/png;base64,')) { + return $.when(); + } + return this._super(route, args); + }, + }); + + lunchKanban.$('.o_add_money').click(); + var modal = $('.modal-dialog'); + assert.strictEqual(modal.find('.modal-body').text().trim(), 'Hello', 'Message should have been Hello'); + modal.find('.btn.btn-primary').click(); + + lunchKanban.$('.o_add_product').click(); + lunchKanban.$('.o_remove_product').click(); + + state = 'ordered'; + lunchKanban.$('.o_lunch_widget_order_button').click(); + // state is shown + assert.strictEqual(lunchKanban.$('.o_lunch_ordered').length, 1, 'state should be shown as ordered'); + // no more buttons + assert.strictEqual(lunchKanban.$('.o_remove_product').length, 0, 'button to remove product should not be shown anymore'); + assert.strictEqual(lunchKanban.$('.o_add_product').length, 0, 'button to add product should not be shown anymore'); + state = 'confirmed'; + lunchKanban.reload(); + // state is updated + assert.strictEqual(lunchKanban.$('.o_lunch_confirmed').length, 1, 'state should be shown as confirmed'); + + assert.verifySteps([ + [[1], 1], + [[1], -1], + ]); + + lunchKanban.destroy(); + }); + + QUnit.test('Manager interactions', function (assert) { + assert.expect(5); + + var lunchKanban = createView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: '<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, + state: 'new', + lines: [ + {id: 1, product: ['Pizza Italiana', 7.4], toppings: [], quantity: 1.0, price: 7.4} + ] + }); + } else if (route.startsWith('data:image/png;base64,')) { + return $.when(); + } + return this._super(route, args); + }, + }); + + var select = lunchKanban.$('select.o_input'); + var options = select.find('option'); + + assert.strictEqual(select.length, 1, 'There should be a selection field'); + assert.strictEqual(options.length, 2, 'Select should have two options'); + assert.strictEqual(options.text().trim(), 'Mitchell AdminMarc Demo', 'Options should be the name of the users'); + assert.strictEqual($(options[0]).data('user-id'), 1, 'user id should be on option'); + assert.strictEqual($(options[1]).data('user-id'), 2, 'user id should be on option'); + + lunchKanban.destroy(); + }); +}); + +}); diff --git a/addons/lunch/tests/__init__.py b/addons/lunch/tests/__init__.py index f9d697d7b2c..08959c769be 100644 --- a/addons/lunch/tests/__init__.py +++ b/addons/lunch/tests/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- -from . import test_lunch +from . import common +from . import test_supplier diff --git a/addons/lunch/tests/common.py b/addons/lunch/tests/common.py new file mode 100644 index 00000000000..f38b9c547ec --- /dev/null +++ b/addons/lunch/tests/common.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import common + + +class TestsCommon(common.TransactionCase): + + def setUp(self): + super(TestsCommon, self).setUp() + + self.partner_pizza_inn = self.env['res.partner'].create({ + 'name': 'Pizza Inn', + }) + + self.supplier_pizza_inn = self.env['lunch.supplier'].create({ + 'partner_id': self.partner_pizza_inn.id, + 'send_by': 'mail', + 'automatic_email_send': True, + 'automatic_email_time': 11, + 'recurrency': 'reccurent', + 'recurrency_from': 8, + 'recurrency_to': 23, + 'recurrency_monday': True, + 'recurrency_tuesday': True, + 'recurrency_wednesday': True, + 'recurrency_thursday': True, + 'recurrency_friday': True, + }) + + self.partner_coin_gourmand = self.env['res.partner'].create({ + 'name': 'Coin Gourmand', + }) + + self.supplier_coin_gourmand = self.env['lunch.supplier'].create({ + 'partner_id': self.partner_coin_gourmand.id, + 'send_by': 'phone', + 'recurrency': 'reccurent', + 'recurrency_from': 8, + 'recurrency_to': 23, + 'recurrency_monday': True, + 'recurrency_tuesday': True, + 'recurrency_wednesday': True, + 'recurrency_thursday': True, + 'recurrency_friday': True, + }) + + self.category_pizza = self.env['lunch.product.category'].create({ + 'name': 'Pizza', + }) + + self.category_sandwich = self.env['lunch.product.category'].create({ + 'name': 'Sandwich', + }) + + self.product_pizza = self.env['lunch.product'].create({ + 'name': 'Pizza', + 'category_id': self.category_pizza.id, + 'price': 9, + 'supplier_id': self.supplier_pizza_inn.id, + }) + + self.product_sandwich_tuna = self.env['lunch.product'].create({ + 'name': 'Tuna Sandwich', + 'category_id': self.category_sandwich.id, + 'price': 3, + 'supplier_id': self.supplier_coin_gourmand.id, + }) + + self.topping_olives = self.env['lunch.product'].create({ + 'name': 'Olives', + 'price': 0.3, + 'supplier_id': self.supplier_pizza_inn.id, + 'is_topping': True, + }) diff --git a/addons/lunch/tests/test_lunch.py b/addons/lunch/tests/test_lunch.py deleted file mode 100644 index c6325a3f19c..00000000000 --- a/addons/lunch/tests/test_lunch.py +++ /dev/null @@ -1,143 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.exceptions import ValidationError -from odoo.tests import common - -from datetime import date, datetime -from dateutil import relativedelta - -class Test_Lunch(common.TransactionCase): - - def setUp(self): - """*****setUp*****""" - super(Test_Lunch, self).setUp() - - self.demo_user = self.env['res.users'].search([('name', '=', 'Marc Demo')]) - self.product_Bolognese_id = self.env.ref('lunch.product_Bolognese') - self.new_id_order = self.env['lunch.order'].create({ - 'user_id': self.demo_user.id, - 'order_line_ids': '[]', - }) - self.new_id_order_line = self.env['lunch.order.line'].create({ - 'order_id': self.new_id_order.id, - 'product_id': self.product_Bolognese_id.id, - 'note': '+Emmental', - 'cashmove': [], - 'price': self.product_Bolognese_id.price, - }) - - def test_00_lunch_order(self): - """Change the state of an order line from 'new' to 'ordered'. Check that there are no cashmove linked to that order line""" - self.order_one = self.new_id_order_line - #we check that our order_line is a 'new' one and that there are no cashmove linked to that order_line: - self.assertEqual(self.order_one.state, 'new') - self.assertEqual(list(self.order_one.cashmove), []) - #we order that orderline so it's state will be 'ordered' - self.order_one.order() - self.order_one = self.new_id_order_line - #we check that our order_line is a 'ordered' one and that there are no cashmove linked to that order_line: - self.assertEqual(self.order_one.state, 'ordered') - self.assertEqual(list(self.order_one.cashmove), []) - - def test_01_lunch_order(self): - """Change the state of an order line from 'new' to 'ordered' then to 'confirmed'. Check that there is a cashmove linked to the order line""" - self.test_00_lunch_order() - #We receive the order so we confirm the order line so it's state will be 'confirmed' - #A cashmove will be created and we will test that the cashmove amount equals the order line price - self.order_one.confirm() - self.order_one = self.new_id_order_line - #we check that our order_line is a 'confirmed' one and that there are a cashmove linked to that order_line with an amount equals to the order line price: - self.assertEqual(self.order_one.state, 'confirmed') - self.assertTrue(self.order_one.cashmove) - self.assertTrue(self.order_one.cashmove[0].amount == -self.order_one.price) - - def test_02_lunch_order(self): - """Change the state of an order line from 'confirmed' to 'cancelled' and check that the cashmove linked to that order line will be deleted""" - self.test_01_lunch_order() - #We have a confirmed order with its associate cashmove - #We execute the cancel function - self.order_one.cancel() - self.order_one = self.new_id_order_line - #We check that the state is cancelled and that the cashmove has been deleted - self.assertEqual(self.order_one.state, 'cancelled') - self.assertFalse(self.order_one.cashmove) - - def test_03_lunch_alert(self): - """ specify a lunch alert and make sure it is not possible to make an - order that day - """ - alert_01 = self.env['lunch.alert'].create({ - 'message': 'Order Bolognese only on Tuesday', - 'partner_id': self.product_Bolognese_id.supplier.id, - 'alert_type': 'week', - 'tuesday': True - }) - wednesday = date.today() + relativedelta.relativedelta(days=7, weekday=2) - next_tuesday = datetime.today() + relativedelta.relativedelta(days=14, weekday=1) - next_wednesday = datetime.today() + relativedelta.relativedelta(days=14, weekday=2) - - # We check that we can make an order on a Tuesday - order_01 = self.env['lunch.order'].create({ - 'user_id': self.demo_user.id, - 'date': date.today() + relativedelta.relativedelta(days=7, weekday=1), - 'order_line_ids': [(0, 0, { - 'product_id': self.product_Bolognese_id.id, - })] - }) - order_01.order_line_ids.order() - - with self.assertRaises(ValidationError): - # Planning an order to an non-authorized day must not be possible - order_02 = self.env['lunch.order'].create({ - 'user_id': self.demo_user.id, - 'date': wednesday, - 'order_line_ids': [(0, 0, { - 'product_id': self.product_Bolognese_id.id, - })] - }) - - alert_02 = self.env['lunch.alert'].create({ - 'message': 'Working one more day', - 'partner_id': self.product_Bolognese_id.supplier.id, - 'alert_type': 'specific', - 'specific_day': wednesday, - }) - - # should now be possible to order tomorrow - order_03 = self.env['lunch.order'].create({ - 'user_id': self.demo_user.id, - 'date': wednesday, - 'order_line_ids': [(0, 0, { - 'product_id': self.product_Bolognese_id.id, - })] - }) - - # should be possible to planned on an allowed day - order_04 = self.env['lunch.order'].create({ - 'user_id': self.demo_user.id, - 'date': next_tuesday, - 'order_line_ids': [(0, 0, { - 'product_id': self.product_Bolognese_id.id, - })] - }) - order_04.order_line_ids.order() - - # combinaision of alerts should work - alert_03 = self.env['lunch.alert'].create({ - 'message': 'Order Bolognese also on Wednesday', - 'partner_id': self.product_Bolognese_id.supplier.id, - 'alert_type': 'week', - 'wednesday': True - }) - # Planning an order Wednesday must now be possible - order_05 = self.env['lunch.order'].create({ - 'user_id': self.demo_user.id, - 'date': next_wednesday, - 'order_line_ids': [(0, 0, { - 'product_id': self.product_Bolognese_id.id, - })] - }) - order_05.order_line_ids.order() - - diff --git a/addons/lunch/tests/test_supplier.py b/addons/lunch/tests/test_supplier.py new file mode 100644 index 00000000000..55c57c8e744 --- /dev/null +++ b/addons/lunch/tests/test_supplier.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime +from unittest.mock import patch + +from odoo import fields + +from odoo.addons.lunch.tests.common import TestsCommon + + +class TestSupplier(TestsCommon): + def setUp(self): + super(TestSupplier, self).setUp() + + self.monday_3am = datetime(2018, 10, 29, 3, 0, 0) + self.monday_10am = datetime(2018, 10, 29, 10, 0, 0) + self.monday_1pm = datetime(2018, 10, 29, 13, 0, 0) + self.monday_8pm = datetime(2018, 10, 29, 20, 0, 0) + + self.saturday_3am = datetime(2018, 11, 3, 3, 0, 0) + self.saturday_10am = datetime(2018, 11, 3, 10, 0, 0) + self.saturday_1pm = datetime(2018, 11, 3, 13, 0, 0) + self.saturday_8pm = datetime(2018, 11, 3, 20, 0, 0) + + def test_compute_available_today(self): + tests = [(self.monday_3am, False), (self.monday_10am, True), + (self.monday_1pm, True), (self.monday_8pm, True), + (self.saturday_3am, False), (self.saturday_10am, False), + (self.saturday_1pm, False), (self.saturday_8pm, False)] + + for value, result in tests: + with patch.object(fields.Datetime, 'now', return_value=value) as _: + assert self.supplier_pizza_inn.available_today == result,\ + 'supplier pizza inn should %s considered available on %s' % ('be' if result else 'not be', value) + + self.env['lunch.supplier'].invalidate_cache(['available_today'], [self.supplier_pizza_inn.id]) + + def test_search_available_today(self): + Supplier = self.env['lunch.supplier'] + + tests = [(self.monday_3am, 3.0, 'monday'), (self.monday_10am, 10.0, 'monday'), + (self.monday_1pm, 13.0, 'monday'), (self.monday_8pm, 20.0, 'monday'), + (self.saturday_3am, 3.0, 'saturday'), (self.saturday_10am, 10.0, 'saturday'), + (self.saturday_1pm, 13.0, 'saturday'), (self.saturday_8pm, 20.0, 'saturday')] + + assert Supplier._search_available_today('>', 7) == [] + assert Supplier._search_available_today('>', True) == [] + + for value, rvalue, dayname in tests: + with patch.object(fields.Datetime, 'now', return_value=value) as _: + assert Supplier._search_available_today('=', True) == ['|', '&', '&', ('recurrency', '=', 'once'), ('recurrency_date_from', '<=', value), + ('recurrency_date_to', '>=', value), '&', '&', ('recurrency_%s' % (dayname), '=', True), + ('recurrency_from', '<=', rvalue), ('recurrency_to', '>=', rvalue)], 'Wrong domain generated for values (%s, %s)' % (value, rvalue) + + assert self.supplier_pizza_inn in Supplier.search([('available_today', '=', True)]) + + def test_auto_email_send(self): + order = self.env['lunch.order'].create({}) + line = self.env['lunch.order.line'].create({ + 'order_id': order.id, + 'product_id': self.product_pizza.id, + }) + + assert len(order.supplier_ids) == 1 and self.supplier_pizza_inn in order.supplier_ids + + order.action_order() + assert order.state == 'ordered' + assert line.state == 'ordered' + + with patch.object(fields.Datetime, 'now', return_value=self.monday_1pm): + with patch.object(fields.Date, 'today', return_value=self.monday_1pm.date()): + self.supplier_pizza_inn._auto_email_send() + + assert order.state == 'confirmed' + assert line.state == 'confirmed' + assert order.mail_sent + + # Do the same but this time order contains multiple lines with products from at least two suppliers + # Check if the lines for pizza_inn have been sent, and put in state='confirmed' only those lines should be + # moreover the order should not yet be in state='confirmed' as there still are some lines to confirm + order = self.env['lunch.order'].create({}) + line = self.env['lunch.order.line'].create({ + 'order_id': order.id, + 'product_id': self.product_pizza.id, + 'topping_ids': [(6, 0, [self.topping_olives.id])], + }) + line2 = self.env['lunch.order.line'].create({ + 'order_id': order.id, + 'product_id': self.product_sandwich_tuna.id, + }) + + assert len(order.supplier_ids) == 2 and self.supplier_coin_gourmand in order.supplier_ids and self.supplier_pizza_inn in order.supplier_ids + + order.action_order() + assert order.state == 'ordered' + assert line.state == 'ordered' + assert line2.state == 'ordered' + + with patch.object(fields.Datetime, 'now', return_value=self.monday_1pm): + with patch.object(fields.Date, 'today', return_value=self.monday_1pm.date()): + self.supplier_pizza_inn._auto_email_send() + + assert order.state == 'confirmed' + assert line.state == 'confirmed' + assert line2.state == 'ordered' + + assert order.mail_sent + + # Check with multiple orders + order_1 = self.env['lunch.order'].create({}) + + line_1_1 = self.env['lunch.order.line'].create({ + 'order_id': order_1.id, + 'product_id': self.product_pizza.id, + }) + + line_1_2 = line_1_1 = self.env['lunch.order.line'].create({ + 'order_id': order_1.id, + 'product_id': self.product_pizza.id, + 'topping_ids': [(6, 0, [self.topping_olives.id])] + }) + + order_2 = self.env['lunch.order'].create({}) + + line_2_1 = self.env['lunch.order.line'].create({ + 'order_id': order_2.id, + 'product_id': self.product_sandwich_tuna.id + }) + + line_2_2 = self.env['lunch.order.line'].create({ + 'order_id': order_2.id, + 'product_id': self.product_pizza.id, + }) + + order_3 = self.env['lunch.order'].create({}) + + line_3_1 = self.env['lunch.order.line'].create({ + 'order_id': order_3.id, + 'product_id': self.product_sandwich_tuna.id + }) + + (order_1 | order_2 | order_3).action_order() + + assert all(order.state == 'ordered' for order in [order_1, order_2, order_3]) + assert all(line.state == 'ordered' for line in [line_1_1, line_1_2, line_2_1, line_2_2, line_3_1]) + + with patch.object(fields.Datetime, 'now', return_value=self.monday_1pm): + with patch.object(fields.Date, 'today', return_value=self.monday_1pm.date()): + self.supplier_pizza_inn._auto_email_send() + + assert all(order.state == 'confirmed' for order in [order_1, order_2]) + assert order_3.state == 'ordered' + assert all(line.state == 'confirmed' for line in [line_1_1, line_1_2, line_2_2]) + assert all(line.state == 'ordered' for line in [line_2_1, line_3_1]) + + assert all(order.mail_sent for order in [order_1, order_2]) + assert not order_3.mail_sent diff --git a/addons/lunch/views/lunch_alert_views.xml b/addons/lunch/views/lunch_alert_views.xml deleted file mode 100644 index bb6d807a9ff..00000000000 --- a/addons/lunch/views/lunch_alert_views.xml +++ /dev/null @@ -1,134 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<odoo> - <record id="lunch_alert_view_search" model="ir.ui.view"> - <field name="name">lunch.alert.search</field> - <field name="model">lunch.alert</field> - <field name="arch" type="xml"> - <search string="Search"> - <field name="message"/> - <filter name="inactive" string="Archived" domain="[('active','=',False)]"/> - </search> - </field> - </record> - - <record id="lunch_alert_view_tree" model="ir.ui.view"> - <field name="name">lunch.alert.tree</field> - <field name="model">lunch.alert</field> - <field name="arch" type="xml"> - <tree> - <field name="message"/> - <field name="alert_type"/> - <field name='start_hour' widget='float_time'/> - <field name='end_hour' widget='float_time'/> - </tree> - </field> - </record> - - <record id="lunch_alert_view_form" model="ir.ui.view"> - <field name="name">lunch.alert.form</field> - <field name="model">lunch.alert</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='{"terminology": "archive"}'/> - </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> - <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> - </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="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> - </record> -</odoo> diff --git a/addons/lunch/views/lunch_orders_views.xml b/addons/lunch/views/lunch_orders_views.xml index 8cd4658a8cb..b8665398153 100644 --- a/addons/lunch/views/lunch_orders_views.xml +++ b/addons/lunch/views/lunch_orders_views.xml @@ -19,7 +19,6 @@ <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> @@ -43,24 +42,15 @@ <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='supplier_id' invisible="1"/> <field name="state" invisible="1"/> </tree> <form string='Lunch Order Form'> @@ -95,7 +85,7 @@ <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> @@ -108,15 +98,14 @@ <field name="arch" type="xml"> <tree string="Order lines Tree" create="false" edit="false"> <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='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"/> + <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"/> </tree> </field> </record> @@ -126,11 +115,11 @@ <field name="model">lunch.order.line</field> <field name="arch" type="xml"> <kanban> - <field name='product_id'/> - <field name='note'/> - <field name='state'/> - <field name='user_id'/> - <field name='date'/> + <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"> diff --git a/addons/lunch/views/lunch_product_views.xml b/addons/lunch/views/lunch_product_views.xml index e7647207d6d..70a8695564e 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="supplier" string="Vendor" context="{'group_by': 'supplier_id'}"/> <filter name="category" string="Product Category" context="{'group_by': 'category_id'}"/> - <filter name="inactive" string="Archived" domain="[('active','=',False)]"/> </group> </search> </field> @@ -25,7 +25,7 @@ <tree string="Products Tree"> <field name="name"/> <field name="category_id"/> - <field name="supplier"/> + <field name="supplier_id"/> <field name="description"/> <field name="price"/> </tree> @@ -37,8 +37,6 @@ <field name="model">lunch.product</field> <field name="arch" type="xml"> <form string="Products Form"> - <header> - </header> <sheet> <div class="oe_button_box" name="button_box"> <button name="toggle_active" type="object" @@ -47,18 +45,24 @@ options='{"terminology": "archive"}'/> </button> </div> + <field name="image" widget="image" class="oe_avatar" options="{'preview_image': 'image_medium'}"/> + <label for="name"/> + <field name='name'/> + <div> + <field name="is_topping"/> + <label for="is_topping"/> + </div> <group> - <group> - <field name='name'/> - <field name='category_id'/> - </group> - <group> - <field name='supplier'/> - <field name='price'/> - </group> + <field name='category_id' attrs="{'invisible': [('is_topping', '=', True)], 'required': [('is_topping', '=', False)]}"/> + <field name='supplier_id'/> + <field name='price'/> </group> - <label for='description'/> - <field name='description'/> + <notebook> + <page string="Description" name="description"> + <label for='description'/> + <field name='description'/> + </page> + </notebook> </sheet> </form> </field> @@ -68,28 +72,37 @@ <field name="name">lunch.product.kanban</field> <field name="model">lunch.product</field> <field name="arch" type="xml"> - <kanban class="o_kanban_mobile"> + <kanban js_class="lunch_kanban" create="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="image_small"/> + <field name="already_ordered"/> <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"> + <div class="o_kanban_image_fill_left d-none d-md-block flex-grow-0" + t-attf-style="background-image:url('#{kanban_image('lunch.product', 'image_medium', record.id.raw_value)}')"/> + <div class="d-flex flex-column flex-grow-1"> + <div class="o_kanban_record_top d-flex flex-row"> + <div class="o_kanban_record_headings flex-grow-1"> + <i t-if="record.already_ordered.raw_value" class="fa fa-heart mr-1 o_pink"/><strong class="o_kanban_record_title"><t t-esc="record.name.value"/></strong> + </div> + <span class="badge badge-pill"> + Price: <t t-esc="record.price.value"/> + </span> + </div> + <div class="o_kanban_record_bottom d-flex flex-column"> + <div> + <t t-esc="record.supplier_id.value"/> + </div> + <div> + <t t-esc="record.description.value"/> + </div> </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,6 +111,16 @@ </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" editable="bottom"> + <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> @@ -125,6 +148,13 @@ </field> </record> + <record id="lunch_product_action_order" model="ir.actions.act_window"> + <field name="name">Order</field> + <field name="res_model">lunch.product</field> + <field name="view_mode">kanban</field> + <field name="domain">[('is_topping', '=', False), ('supplier_id.available_today', '=', True)]</field> + </record> + <record id="lunch_product_category_action" model="ir.actions.act_window"> <field name="name">Product Categories</field> <field name="res_model">lunch.product.category</field> diff --git a/addons/lunch/views/lunch_supplier_views.xml b/addons/lunch/views/lunch_supplier_views.xml new file mode 100644 index 00000000000..0d6ec048451 --- /dev/null +++ b/addons/lunch/views/lunch_supplier_views.xml @@ -0,0 +1,113 @@ +<?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> + <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" required="1"/></h1> + </div> + <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="vat"/> + <field name="email"/> + <field name="phone"/> + </group> + <notebook colspan="4"> + <page string="Availability"> + <group> + <field name="tz"/> + <field name="recurrency"/> + <field name="recurrency_date" attrs="{'invisible': [('recurrency', '!=', 'once')]}"/> + <field name="recurrency_monday" attrs="{'invisible': [('recurrency', '!=', 'reccurent')]}"/> + <field name="recurrency_tuesday" attrs="{'invisible': [('recurrency', '!=', 'reccurent')]}"/> + <field name="recurrency_wednesday" attrs="{'invisible': [('recurrency', '!=', 'reccurent')]}"/> + <field name="recurrency_thursday" attrs="{'invisible': [('recurrency', '!=', 'reccurent')]}"/> + <field name="recurrency_friday" attrs="{'invisible': [('recurrency', '!=', 'reccurent')]}"/> + <field name="recurrency_saturday" attrs="{'invisible': [('recurrency', '!=', 'reccurent')]}"/> + <field name="recurrency_sunday" attrs="{'invisible': [('recurrency', '!=', 'reccurent')]}"/> + <field name="recurrency_from" widget="float_time"/> + <field name="recurrency_to" widget="float_time"/> + </group> + </page> + <page string="Description"> + <label for="description" string="Type your description here" class="oe_edit_only"/> + <field name="description"/> + </page> + <page string="Order"> + <label for="send_by"/> + <field name="send_by" widget="radio"/> + <div> + <label for="automatic_email_send" string="Send Orders automatically at" attrs="{'invisible': [('send_by', '!=', 'mail')]}"/> + <field name="automatic_email_time" widget="float_time" attrs="{'invisible': [('send_by', '!=', 'mail')]}"/> + </div> + </page> + </notebook> + </sheet> + </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="image_small"/> + <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="o_kanban_image_fill_left d-none d-md-block" t-attf-style="background-image:url('#{kanban_image('res.partner', 'image_medium', record.partner_id.raw_value)}')"/> + <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..0f509cf2465 100644 --- a/addons/lunch/views/lunch_views.xml +++ b/addons/lunch/views/lunch_views.xml @@ -6,7 +6,7 @@ <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="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_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"/> @@ -15,9 +15,9 @@ <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="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="Vendors" parent="menu_lunch_config" id="lunch_vendors_menu" action="lunch_vendors_action" sequence="1"/> + <menuitem name="Products" parent="menu_lunch_config" id="lunch_product_menu" action="lunch_product_action" sequence="2"/> + <menuitem name="Product Categories" parent="menu_lunch_config" id="lunch_product_category_menu" action="lunch_product_category_action" sequence="3"/> <record id="action_server_lunch_archive_product" model="ir.actions.server"> 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..1229c4a0a07 --- /dev/null +++ b/addons/lunch/wizard/lunch_line_wizard.py @@ -0,0 +1,77 @@ +# -*- 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 UserError + + +class LunchOrderLineWizard(models.TransientModel): + _name = 'lunch.order.line.temp' + _description = 'Lunch Order Line Temp' + + 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_supplier = fields.Many2one('lunch.supplier', related='product_id.supplier_id') + topping_ids = fields.Many2many('lunch.product', string="Extra Garniture", domain="[('is_topping', '=', True), ('supplier_id', '=', product_supplier)]") + + quantity = fields.Float('Quantity', default=1) + price_total = fields.Float('Total Price', compute='_compute_price_total') + note = fields.Text('Special Instructions') + + user_id = fields.Many2one('res.users', default=lambda self: self.env.user.id) + + @api.depends('product_id', 'topping_ids', 'quantity') + def _compute_price_total(self): + for wizard in self: + wizard.price_total = wizard.quantity * sum(record.price for record in (wizard.product_id | wizard.topping_ids)) + + def find_current_order(self): + order_id = self.env['lunch.order'].search([('state', '!=', 'cancel'), + ('user_id', '=', self.user_id.id), + ('date', '=', fields.Date.today())], limit=1) + if not order_id: + order_id = self.env['lunch.order'].create({}) + return order_id + + def add_to_cart(self): + self.ensure_one() + + order_id = self.find_current_order() + + # Do not add a line to confirmed orders as they are uneditable + if order_id.state == 'confirmed': + return + + lines = order_id.order_line_ids.filtered(lambda line: line.product_id == self.product_id and line.topping_ids == self.topping_ids) + + if lines: + line = lines[0] + line.quantity += 1 + else: + line = self.env['lunch.order.line'].create({ + 'order_id': order_id.id, + 'product_id': self.product_id.id, + 'topping_ids': [(6, 0, self.topping_ids.ids)], + 'quantity': self.quantity, + }) + wallet_balance = self.env['lunch.cashmove'].get_wallet_balance(self.user_id) + if wallet_balance < line.price: + raise UserError(_('Your wallet does not contain enough money to order that.' + 'To add some money to your wallet, please contact your lunch manager.')) + + if order_id.state == 'ordered': + # Need to update cashmove + cashmove = self.env['lunch.cashmove'].search([('order_line_id', '=', line.id)], limit=1) + if cashmove: + cashmove.amount = -line.price + else: + self.env['lunch.cashmove'].create({ + 'order_line_id': line.id, + 'user_id': line.user_id.id, + 'description': line.product_id.name, + 'state': 'order', + 'date': line.date, + 'amount': -line.price, + }) 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..9b70e2c7f78 --- /dev/null +++ b/addons/lunch/wizard/lunch_line_wizard_views.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> +<data> + + <record id="view_lunch_order_line_temp" model="ir.ui.view"> + <field name="name">view.lunch.order.line.temp</field> + <field name="model">lunch.order.line.temp</field> + <field name="arch" type="xml"> + <form> + <group> + <field name="product_id" invisible="1"/> + <field name="product_supplier" invisible="1"/> + <field name="product_name"/> + <field name="product_description"/> + <field name="topping_ids" widget="many2many_tags"/> + </group> + + <group> + <field name="note"/> + </group> + + <group> + <field name="quantity"/> + <field name="price_total" widget="monetary"/> + </group> + + <footer> + <button name="add_to_cart" string="Add To Cart" type="object" class="oe_highlight"/> + </footer> + </form> + </field> + </record> + +</data> +</odoo> diff --git a/addons/lunch_hr/__init__.py b/addons/lunch_hr/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/lunch_hr/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/lunch_hr/__manifest__.py b/addons/lunch_hr/__manifest__.py new file mode 100644 index 00000000000..ff335e2c574 --- /dev/null +++ b/addons/lunch_hr/__manifest__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +{ + 'name': 'Lunch HR', + 'sequence': 120, + 'version': '1.0', + 'depends': ['lunch', 'hr'], + 'category': 'Human Resources', + 'summary': 'Handle lunch orders of your employees', + 'description': """""", + 'data': [ + 'views/lunch_product_views.xml', + 'views/lunch_supplier_views.xml', + ], + 'demo': ['data/lunch_hr_demo.xml',], + 'auto_install': True, + 'installable': True, + 'application': False, +} diff --git a/addons/lunch_hr/data/lunch_hr_demo.xml b/addons/lunch_hr/data/lunch_hr_demo.xml new file mode 100644 index 00000000000..62e70952c5a --- /dev/null +++ b/addons/lunch_hr/data/lunch_hr_demo.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data noupdate="1"> + + <record id="partner_farm_1" model="res.partner"> + <field name="name">Farm 1</field> + </record> + + <record id="partner_farm_2" model="res.partner"> + <field name="name">Farm 2</field> + </record> + + <record id="partner_brussels" model="res.partner"> + <field name="name">Brussels</field> + </record> + + <record id="hr.employee_admin" model="hr.employee"> + <field name="address_id" ref="partner_farm_1"/> + </record> + + <record id="lunch.supplier_pizza_inn" model="lunch.supplier"> + <field name="available_location_ids" eval="[ + (6, 0, [ref('lunch_hr.partner_farm_1'), ref('lunch_hr.partner_farm_2')]), + ]"/> + </record> + + </data> +</odoo> diff --git a/addons/lunch_hr/models/__init__.py b/addons/lunch_hr/models/__init__.py new file mode 100644 index 00000000000..8d16a6970f5 --- /dev/null +++ b/addons/lunch_hr/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import lunch_product +from . import lunch_supplier diff --git a/addons/lunch_hr/models/lunch_product.py b/addons/lunch_hr/models/lunch_product.py new file mode 100644 index 00000000000..8418d994d81 --- /dev/null +++ b/addons/lunch_hr/models/lunch_product.py @@ -0,0 +1,33 @@ +# -*- 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 +from odoo.tools import pycompat + + +class LunchProduct(models.Model): + _inherit = 'lunch.product' + + # This field is used only for searching + is_available_at = fields.Many2one('res.partner', 'Product Availability', compute='_compute_is_available_at', search='_search_is_available_at') + + def _compute_is_available_at(self): + for product in self: + product.is_available_at = False + + def _search_is_available_at(self, operator, value): + if operator in ['=', '!=', 'ilike', 'not ilike'] and isinstance(value, pycompat.string_types): + location_ids = self.env['res.partner'].search([('name', operator, value)]).ids + + if operator in ['='] and not value: + if self.env.user.employee_ids and self.env.user.employee_ids[0].address_id: + location_ids = self.env.user.employee_ids[0].address_id.ids + + if location_ids: + suppliers = self.env['lunch.supplier'].search([('available_location_ids', 'in', location_ids)]) + product_ids = self.env['lunch.product'].search([('supplier', 'in', suppliers.ids)]) + return [('id', 'in', product_ids.ids)] + + return expression.TRUE_DOMAIN diff --git a/addons/lunch_hr/models/lunch_supplier.py b/addons/lunch_hr/models/lunch_supplier.py new file mode 100644 index 00000000000..27434347ad1 --- /dev/null +++ b/addons/lunch_hr/models/lunch_supplier.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class LunchSupplier(models.Model): + _inherit = 'lunch.supplier' + + available_location_ids = fields.Many2many('res.partner', string='This Supplier is available at') diff --git a/addons/lunch_hr/views/lunch_product_views.xml b/addons/lunch_hr/views/lunch_product_views.xml new file mode 100644 index 00000000000..356c39fc892 --- /dev/null +++ b/addons/lunch_hr/views/lunch_product_views.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="lunch_product_view_search_inherit" model="ir.ui.view"> + <field name="name">lunch.product.search.inherit</field> + <field name="model">lunch.product</field> + <field name="inherit_id" ref="lunch.lunch_product_view_search"/> + <field name="arch" type="xml"> + <xpath expr="//filter[@name='inactive']" position="after"> + <filter name="available" string="Available at current location" domain="[('is_available_at', '=', False)]"/> + </xpath> + </field> + </record> +</odoo> diff --git a/addons/lunch_hr/views/lunch_supplier_views.xml b/addons/lunch_hr/views/lunch_supplier_views.xml new file mode 100644 index 00000000000..28a91e3bac7 --- /dev/null +++ b/addons/lunch_hr/views/lunch_supplier_views.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="lunch_supplier_view_form_inherit" model="ir.ui.view"> + <field name="name">lunch.supplier.form.view.inherit</field> + <field name="model">lunch.supplier</field> + <field name="inherit_id" ref="lunch.lunch_supplier_view_form"/> + <field name="arch" type="xml"> + <xpath expr="//field[@name='recurrency_to']" position="after"> + <field name="available_location_ids" widget="many2many_tags"/> + </xpath> + </field> + </record> +</odoo> |
