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