[MERGE] lunch, lunch_hr: automatize lunch flow
We would like to automatize lunch as currently all orders are manually ordered and validated, and a cash move has to be manually created each time you want to credit the wallet of some user. In this commit we add a new widget that comes with the kanban view and will allow to take orders in a friendly manner. We also add a lunch.supplier model and an integration with the hr module in order to filter what products you can order depending on your working location, this also allows you to configure an automatic ordering via email. -- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr closes odoo/odoo#28442
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import wizard
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
||||
@@ -27,12 +27,17 @@ If you want to save your employees' time and avoid them to always have coins in
|
||||
'report/lunch_order_views.xml',
|
||||
'report/lunch_order_templates.xml',
|
||||
'views/lunch_templates.xml',
|
||||
'wizard/lucky_order_view.xml',
|
||||
'wizard/lunch_line_wizard_views.xml',
|
||||
'views/lunch_cashmove_views.xml',
|
||||
'views/lunch_orders_views.xml',
|
||||
'views/lunch_product_views.xml',
|
||||
'views/lunch_supplier_views.xml',
|
||||
'views/lunch_views.xml',
|
||||
'data/lunch_data.xml',
|
||||
'data/ir_cron_data.xml'
|
||||
],
|
||||
'demo': ['data/lunch_demo.xml'],
|
||||
'qweb': ['static/src/xml/lunch.xml', ],
|
||||
'qweb': ['static/src/xml/lunch_kanban.xml', ],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'certificate': '001292377792581874189',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import main
|
||||
@@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http, fields
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class LunchController(http.Controller):
|
||||
@http.route('/lunch/infos', type='json', auth='user')
|
||||
def infos(self, user_id=None):
|
||||
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
|
||||
|
||||
infos = {
|
||||
'order': False,
|
||||
'wallet': request.env['lunch.cashmove'].get_wallet_balance(user),
|
||||
'username': user.sudo().name,
|
||||
'userimage': '/web/image?model=res.users&id=%s&field=image_small' % user.id,
|
||||
'is_manager': request.env.user.has_group('lunch.group_lunch_manager'),
|
||||
'users': request.env['res.users'].search_read([('groups_id', 'not in', [request.env.ref('base.group_portal').id])], ['name']),
|
||||
}
|
||||
|
||||
order = self._get_current_order(user.id)
|
||||
if order:
|
||||
lines = [{'id': line.id,
|
||||
'product': (line.product_id.name, line.product_id.price),
|
||||
'toppings': [(topping.name, topping.price) for topping in line.topping_ids],
|
||||
'quantity': line.quantity,
|
||||
'price': line.price} for line in order.order_line_ids]
|
||||
infos.update({
|
||||
'order': order.id,
|
||||
'total': order.total,
|
||||
'state': order.state,
|
||||
'lines': lines,
|
||||
})
|
||||
return infos
|
||||
|
||||
@http.route('/lunch/trash', type='json', auth='user')
|
||||
def trash(self, user_id=None):
|
||||
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
|
||||
|
||||
order = self._get_current_order(user.id)
|
||||
order.unlink()
|
||||
|
||||
@http.route('/lunch/pay', type='json', auth='user')
|
||||
def pay(self, user_id=None):
|
||||
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
|
||||
|
||||
order = self._get_current_order(user.id)
|
||||
if order:
|
||||
wallet_balance = request.env['lunch.cashmove'].get_wallet_balance(user)
|
||||
|
||||
if order.state == 'new' and order.total <= wallet_balance:
|
||||
return self._make_payment(order)
|
||||
|
||||
return False
|
||||
|
||||
@http.route('/lunch/payment_message', type='json', auth='user')
|
||||
def payment_message(self):
|
||||
return {'message': request.env['ir.qweb'].render('lunch.lunch_payment_dialog', {})}
|
||||
|
||||
def _get_current_order(self, user_id):
|
||||
order = request.env['lunch.order'].search([('user_id', '=', user_id), ('date', '=', fields.Date.today())], limit=1)
|
||||
return order
|
||||
|
||||
def _make_payment(self, order):
|
||||
order.action_order()
|
||||
return True
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="ir_cron_lunch" model="ir.cron">
|
||||
<field name="name">Lunch: automatic email send</field>
|
||||
<field name="model_id" ref="model_lunch_supplier"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._auto_email_send()</field>
|
||||
<field name="active" eval="True"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="interval_number">10</field>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,23 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="lunch_order_line_action_order" model="ir.actions.server">
|
||||
<field name="name">Lunch: Order meals</field>
|
||||
<field name="model_id" ref="model_lunch_order_line"/>
|
||||
<field name="binding_model_id" ref="model_lunch_order_line"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
if records:
|
||||
action = records.order()
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="lunch_order_line_action_confirm" model="ir.actions.server">
|
||||
<field name="name">Lunch: Receive meals</field>
|
||||
<field name="model_id" ref="model_lunch_order_line"/>
|
||||
<field name="binding_model_id" ref="model_lunch_order_line"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">records.confirm()</field>
|
||||
<field name="code">records.action_confirm()</field>
|
||||
</record>
|
||||
|
||||
<record id="lunch_order_line_action_cancel" model="ir.actions.server">
|
||||
@@ -25,7 +14,7 @@ if records:
|
||||
<field name="model_id" ref="model_lunch_order_line"/>
|
||||
<field name="binding_model_id" ref="model_lunch_order_line"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">records.cancel()</field>
|
||||
<field name="code">records.action_cancel()</field>
|
||||
</record>
|
||||
</data>
|
||||
<data noupdate="0">
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<field name="name">Chirashi</field>
|
||||
</record>
|
||||
|
||||
<record model="res.partner" id="partner_coin_gourmand">
|
||||
<record id="partner_coin_gourmand" model="res.partner">
|
||||
<field name="name">Coin gourmand</field>
|
||||
<field name="city">Tirana</field>
|
||||
<field name="country_id" ref="base.al"/>
|
||||
@@ -37,7 +37,13 @@
|
||||
<field name="email">coin.gourmand@yourcompany.example.com</field>
|
||||
</record>
|
||||
|
||||
<record model="res.partner" id="partner_pizza_inn">
|
||||
<record model="lunch.supplier" id="supplier_coin_gourmand">
|
||||
<field name="partner_id" ref="partner_coin_gourmand"/>
|
||||
<field name="recurrency_from">8</field>
|
||||
<field name="recurrency_to">23</field>
|
||||
</record>
|
||||
|
||||
<record id="partner_pizza_inn" model="res.partner">
|
||||
<field name="name">Pizza Inn</field>
|
||||
<field name="city">New Delhi TN</field>
|
||||
<field name="country_id" ref="base.us"/>
|
||||
@@ -56,6 +62,12 @@
|
||||
<field name="email">info@corner.com</field>
|
||||
</record>
|
||||
|
||||
<record model="lunch.supplier" id="supplier_corner">
|
||||
<field name="partner_id" ref="partner_corner"/>
|
||||
<field name="recurrency_from">8</field>
|
||||
<field name="recurrency_to">23</field>
|
||||
</record>
|
||||
|
||||
<record model="res.partner" id="partner_sushi_shop">
|
||||
<field name="name">Sushi Shop</field>
|
||||
<field name="city">Paris</field>
|
||||
@@ -65,178 +77,228 @@
|
||||
<field name="email">order@sushi.com</field>
|
||||
</record>
|
||||
|
||||
<record model="lunch.supplier" id="supplier_sushi_shop">
|
||||
<field name="partner_id" ref="partner_sushi_shop"/>
|
||||
<field name="recurrency_from">8</field>
|
||||
<field name="recurrency_to">23</field>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_bacon">
|
||||
<field name="name">Bacon</field>
|
||||
<field name="category_id" eval="str(ref('categ_burger'))"/>
|
||||
<field name="price">7.5</field>
|
||||
<field name="supplier" eval="str(ref('partner_corner'))"/>
|
||||
<field name="supplier_id" ref="supplier_corner"/>
|
||||
<field name="description">Beef, Bacon, Salad, Cheddar, Fried Onion, BBQ Sauce</field>
|
||||
<field name="image" type="base64" file="lunch/static/img/bacon_burger.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_cheeseburger">
|
||||
<field name="name">Cheese Burger</field>
|
||||
<field name="category_id" eval="str(ref('categ_burger'))"/>
|
||||
<field name="price">7.0</field>
|
||||
<field name="supplier" eval="str(ref('partner_corner'))"/>
|
||||
<field name="supplier_id" ref="supplier_corner"/>
|
||||
<field name="description">Beef, Cheddar, Salad, Fried Onions, BBQ Sauce</field>
|
||||
<field name="image" type="base64" file="lunch/static/img/cheeseburger.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_chicken_curry">
|
||||
<field name="name">Chicken Curry</field>
|
||||
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
|
||||
<field name="price">3.0</field>
|
||||
<field name="supplier" eval="str(ref('partner_corner'))"/>
|
||||
<field name="supplier_id" ref="supplier_corner"/>
|
||||
<field name="image" type="base64" file="lunch/static/img/chicken_curry.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_spicy_tuna">
|
||||
<field name="name">Spicy Tuna</field>
|
||||
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
|
||||
<field name="price">3.0</field>
|
||||
<field name="supplier" eval="str(ref('partner_corner'))"/>
|
||||
<field name="supplier_id" ref="supplier_corner"/>
|
||||
<field name="image" type="base64" file="lunch/static/img/chicken_curry.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_mozzarella">
|
||||
<field name="name">Mozzarella</field>
|
||||
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
|
||||
<field name="price">3.9</field>
|
||||
<field name="supplier" eval="str(ref('partner_corner'))"/>
|
||||
<field name="supplier_id" ref="supplier_corner"/>
|
||||
<field name="description">Mozzarella, Pesto, Tomatoes</field>
|
||||
<field name="image" type="base64" file="lunch/static/img/mozza.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_club">
|
||||
<field name="name">Club</field>
|
||||
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
|
||||
<field name="price">3.4</field>
|
||||
<field name="supplier" eval="str(ref('partner_corner'))"/>
|
||||
<field name="supplier_id" ref="supplier_corner"/>
|
||||
<field name="description">Ham, Cheese, Vegetables</field>
|
||||
<field name="image" type="base64" file="lunch/static/img/club.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_maki">
|
||||
<field name="name">Lunch Maki 18pc</field>
|
||||
<field name="category_id" eval="str(ref('categ_sushi'))"/>
|
||||
<field name="price">12.0</field>
|
||||
<field name="supplier" eval="str(ref('partner_sushi_shop'))"/>
|
||||
<field name="supplier_id" ref="supplier_sushi_shop"/>
|
||||
<field name="description">6 Maki Salmon - 6 Maki Tuna - 6 Maki Shrimp/Avocado</field>
|
||||
<field name="image" type="base64" file="lunch/static/img/maki.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_salmon">
|
||||
<field name="name">Lunch Salmon 20pc</field>
|
||||
<field name="category_id" eval="str(ref('categ_sushi'))"/>
|
||||
<field name="price">13.80</field>
|
||||
<field name="supplier" eval="str(ref('partner_sushi_shop'))"/>
|
||||
<field name="supplier_id" ref="supplier_sushi_shop"/>
|
||||
<field name="description">4 Sushi Salmon - 6 Maki Salmon - 4 Sashimi Salmon </field>
|
||||
<field name="image" type="base64" file="lunch/static/img/salmon_sushi.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_temaki">
|
||||
<field name="name">Lunch Temaki mix 3pc</field>
|
||||
<field name="category_id" eval="str(ref('categ_temaki'))"/>
|
||||
<field name="price">14.0</field>
|
||||
<field name="supplier" eval="str(ref('partner_sushi_shop'))"/>
|
||||
<field name="description">1 Avocado/Cooked Tuna - 1 Salmon/Eggs - 1 Salmon/Avocado - 1 Tuna/Mango</field>
|
||||
<field name="supplier_id" ref="supplier_sushi_shop"/>
|
||||
<field name="description">1 Avocado - 1 Salmon - 1 Eggs - 1 Tuna</field>
|
||||
<field name="image" type="base64" file="lunch/static/img/temaki.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_chirashi">
|
||||
<field name="name">Salmon and Avocado</field>
|
||||
<field name="category_id" eval="str(ref('categ_chirashi'))"/>
|
||||
<field name="price">9.25</field>
|
||||
<field name="supplier" eval="str(ref('partner_sushi_shop'))"/>
|
||||
<field name="description">Shrimp Tempura, Yam Tempura, Chinese Cabbage, Pickled Onions, Sesame Sauce</field>
|
||||
<field name="supplier_id" ref="supplier_sushi_shop"/>
|
||||
<field name="description">2 Tempuras, Cabbages, Onions, Sesame Sauce</field>
|
||||
<field name="image" type="base64" file="lunch/static/img/chirashi.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.supplier" id="supplier_pizza_inn">
|
||||
<field name="partner_id" ref="partner_pizza_inn"/>
|
||||
<field name="send_by">mail</field>
|
||||
<field name="automatic_email_send" eval="True"/>
|
||||
<field name="automatic_email_time">11</field>
|
||||
<field name="recurrency_from">8</field>
|
||||
<field name="recurrency_to">23</field>
|
||||
<field name="recurrency">reccurent</field>
|
||||
<field name="recurrency_monday" eval="True"/>
|
||||
<field name="recurrency_tuesday" eval="True"/>
|
||||
<field name="recurrency_wednesday" eval="True"/>
|
||||
<field name="recurrency_thursday" eval="True"/>
|
||||
<field name="recurrency_friday" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_cheese_ham">
|
||||
<field name="name">Cheese And Ham</field>
|
||||
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
|
||||
<field name="price">3.30</field>
|
||||
<field name="supplier" eval="str(ref('partner_coin_gourmand'))"/>
|
||||
<field name="supplier_id" eval="str(ref('supplier_coin_gourmand'))"/>
|
||||
<field name="description">Cheese, Ham, Salad, Tomatoes, cucumbers, eggs</field>
|
||||
<field name="image" type="base64" file="lunch/static/img/club.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_country">
|
||||
<field name="name">The Country</field>
|
||||
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
|
||||
<field name="price">3.30</field>
|
||||
<field name="supplier" eval="str(ref('partner_coin_gourmand'))"/>
|
||||
<field name="supplier_id" eval="str(ref('supplier_coin_gourmand'))"/>
|
||||
<field name="description">Brie, Honey, Walnut Kernels</field>
|
||||
<field name="image" type="base64" file="lunch/static/img/brie.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_tuna">
|
||||
<field name="name">Tuna</field>
|
||||
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
|
||||
<field name="price">2.50</field>
|
||||
<field name="supplier" eval="str(ref('partner_coin_gourmand'))"/>
|
||||
<field name="supplier_id" eval="str(ref('supplier_coin_gourmand'))"/>
|
||||
<field name="description">Tuna, Mayonnaise</field>
|
||||
<field name="image" type="base64" file="lunch/static/img/tuna_sandwich.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_gouda">
|
||||
<field name="name">Gouda Cheese</field>
|
||||
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
|
||||
<field name="price">2.50</field>
|
||||
<field name="supplier" eval="str(ref('partner_coin_gourmand'))"/>
|
||||
<field name="supplier_id" eval="str(ref('supplier_coin_gourmand'))"/>
|
||||
<field name="description"></field>
|
||||
<field name="image" type="base64" file="lunch/static/img/gouda.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_chicken_curry">
|
||||
<field name="name">Chicken Curry</field>
|
||||
<field name="category_id" eval="str(ref('categ_sandwich'))"/>
|
||||
<field name="price">2.60</field>
|
||||
<field name="supplier" eval="str(ref('partner_coin_gourmand'))"/>
|
||||
<field name="supplier_id" eval="str(ref('supplier_coin_gourmand'))"/>
|
||||
<field name="description"></field>
|
||||
<field name="image" type="base64" file="lunch/static/img/chicken_curry.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_margherita">
|
||||
<field name="name">Pizza Margherita</field>
|
||||
<field name="category_id" eval="str(ref('categ_pizza'))"/>
|
||||
<field name="price">6.90</field>
|
||||
<field name="supplier" eval="str(ref('partner_pizza_inn'))"/>
|
||||
<field name="supplier_id" eval="str(ref('supplier_pizza_inn'))"/>
|
||||
<field name="description">Tomatoes, Mozzarella</field>
|
||||
<field name="image" type="base64" file="lunch/static/img/pizza_margherita.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_funghi">
|
||||
<field name="name">Pizza Funghi</field>
|
||||
<field name="category_id" eval="str(ref('categ_pizza'))"/>
|
||||
<field name="price">7.00</field>
|
||||
<field name="supplier" eval="str(ref('partner_pizza_inn'))"/>
|
||||
<field name="supplier_id" ref="supplier_pizza_inn"/>
|
||||
<field name="description">Tomatoes, Mushrooms, Mozzarella</field>
|
||||
<field name="image" type="base64" file="lunch/static/img/pizza_funghi.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_vege">
|
||||
<field name="name">Pizza Vegetarian</field>
|
||||
<field name="category_id" eval="str(ref('categ_pizza'))"/>
|
||||
<field name="price">7.00</field>
|
||||
<field name="supplier" eval="str(ref('partner_pizza_inn'))"/>
|
||||
<field name="supplier_id" ref="supplier_pizza_inn"/>
|
||||
<field name="description">Tomatoes, Mozzarella, Mushrooms, Peppers, Olives</field>
|
||||
<field name="image" type="base64" file="lunch/static/img/pizza_veggie.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_italiana">
|
||||
<field name="name">Pizza Italiana</field>
|
||||
<field name="category_id" eval="str(ref('categ_pizza'))"/>
|
||||
<field name="price">7.40</field>
|
||||
<field name="supplier" eval="str(ref('partner_pizza_inn'))"/>
|
||||
<field name="supplier_id" eval="str(ref('supplier_pizza_inn'))"/>
|
||||
<field name="description">Fresh Tomatoes, Basil, Mozzarella</field>
|
||||
<field name="image" type="base64" file="lunch/static/img/italiana.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_Bolognese">
|
||||
<field name="name">Bolognese Pasta</field>
|
||||
<field name="category_id" eval="str(ref('categ_pasta'))"/>
|
||||
<field name="price">7.70</field>
|
||||
<field name="supplier" eval="str(ref('partner_pizza_inn'))"/>
|
||||
<field name="supplier_id" eval="str(ref('supplier_pizza_inn'))"/>
|
||||
<field name="description"></field>
|
||||
<field name="image" type="base64" file="lunch/static/img/pasta_bolognese.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_Napoli">
|
||||
<field name="name">Napoli Pasta</field>
|
||||
<field name="category_id" eval="str(ref('categ_pasta'))"/>
|
||||
<field name="price">7.70</field>
|
||||
<field name="supplier" eval="str(ref('partner_pizza_inn'))"/>
|
||||
<field name="supplier_id" eval="str(ref('supplier_pizza_inn'))"/>
|
||||
<field name="description">Tomatoes, Basil</field>
|
||||
<field name="image" type="base64" file="lunch/static/img/napoli.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_olives">
|
||||
<field name="name">Olives</field>
|
||||
<field name="price">0.30</field>
|
||||
<field name="supplier_id" eval="str(ref('supplier_pizza_inn'))"/>
|
||||
<field name="is_topping" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.product" id="product_4formaggi">
|
||||
<field name="name">4 Formaggi</field>
|
||||
<field name="category_id" eval="str(ref('categ_pasta'))"/>
|
||||
<field name="price">5.50</field>
|
||||
<field name="supplier" eval="str(ref('partner_pizza_inn'))"/>
|
||||
<field name="supplier_id" ref="supplier_pizza_inn"/>
|
||||
<field name="description">Tomato sauce, Olive oil, Fresh Tomatoes, Onions, Vegetables, Parmesan</field>
|
||||
<field name="image" type="base64" file="lunch/static/img/4formaggio.png"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.order" id="order_1">
|
||||
@@ -244,6 +306,7 @@
|
||||
<field name="date" eval="(DateTime.now() + timedelta(days=2)).strftime('%Y-%m-%d')"/>
|
||||
<field name="order_line_ids" eval="[]"/>
|
||||
<field name="state">new</field>
|
||||
<field name="mail_sent" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.order" id="order_2">
|
||||
@@ -251,6 +314,7 @@
|
||||
<field name="date" eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="order_line_ids" eval="[]"/>
|
||||
<field name="state">confirmed</field>
|
||||
<field name="mail_sent" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.order" id="order_3">
|
||||
@@ -258,6 +322,7 @@
|
||||
<field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
|
||||
<field name="order_line_ids" eval="[]"/>
|
||||
<field name="state">confirmed</field>
|
||||
<field name="mail_sent" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record model="lunch.order" id="order_4">
|
||||
@@ -273,9 +338,9 @@
|
||||
<field name="price">7.70</field>
|
||||
<field name="date" eval="(DateTime.now() + timedelta(days=2)).strftime('%Y-%m-%d')"/>
|
||||
<field name="state">new</field>
|
||||
<field name="supplier" ref="partner_pizza_inn"/>
|
||||
<field name="note">+Emmental</field>
|
||||
<field name="supplier_id" ref="supplier_pizza_inn"/>
|
||||
<field name="order_id" ref="order_1"/>
|
||||
<field name="quantity">1</field>
|
||||
</record>
|
||||
|
||||
<record model="lunch.order.line" id="order_line_2">
|
||||
@@ -284,9 +349,9 @@
|
||||
<field name="price">7.40</field>
|
||||
<field name="date" eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="state">confirmed</field>
|
||||
<field name="supplier" ref="partner_pizza_inn"/>
|
||||
<field name="note">+Mushrooms</field>
|
||||
<field name="supplier_id" ref="supplier_pizza_inn"/>
|
||||
<field name="order_id" ref="order_2"/>
|
||||
<field name="quantity">1</field>
|
||||
</record>
|
||||
|
||||
<record model="lunch.order.line" id="order_line_3">
|
||||
@@ -295,9 +360,9 @@
|
||||
<field name="price">2.50</field>
|
||||
<field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
|
||||
<field name="state">cancelled</field>
|
||||
<field name="supplier" ref="partner_coin_gourmand"/>
|
||||
<field name="note">+Salad +Tomatoes +Cucumbers</field>
|
||||
<field name="supplier_id" ref="supplier_coin_gourmand"/>
|
||||
<field name="order_id" ref="order_3"/>
|
||||
<field name="quantity">1</field>
|
||||
</record>
|
||||
|
||||
<record model="lunch.order.line" id="order_line_4">
|
||||
@@ -306,9 +371,9 @@
|
||||
<field name="price">2.60</field>
|
||||
<field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
|
||||
<field name="state">confirmed</field>
|
||||
<field name="supplier" ref="partner_coin_gourmand"/>
|
||||
<field name="note">+Salad +Tomatoes +Eggs</field>
|
||||
<field name="supplier_id" ref="supplier_coin_gourmand"/>
|
||||
<field name="order_id" ref="order_3"/>
|
||||
<field name="quantity">1</field>
|
||||
</record>
|
||||
|
||||
<record model="lunch.order.line" id="order_line_5">
|
||||
@@ -317,7 +382,7 @@
|
||||
<field name="price">5.50</field>
|
||||
<field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
|
||||
<field name="state">confirmed</field>
|
||||
<field name="supplier" ref="partner_pizza_inn"/>
|
||||
<field name="supplier_id" ref="supplier_pizza_inn"/>
|
||||
<field name="order_id" ref="order_4"/>
|
||||
</record>
|
||||
|
||||
@@ -326,7 +391,7 @@
|
||||
<field name="date" eval="(DateTime.now() + timedelta(days=2)).strftime('%Y-%m-%d')"/>
|
||||
<field name="description">Pizza Italiana</field>
|
||||
<field name="amount">-7.40</field>
|
||||
<field name="order_id" ref="order_2"/>
|
||||
<field name="order_line_id" ref="order_line_1"/>
|
||||
<field name="state">order</field>
|
||||
</record>
|
||||
|
||||
@@ -335,7 +400,7 @@
|
||||
<field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
|
||||
<field name="description">Chicken curry</field>
|
||||
<field name="amount">-2.60</field>
|
||||
<field name="order_id" ref="order_3"/>
|
||||
<field name="order_line_id" ref="order_line_2"/>
|
||||
<field name="state">order</field>
|
||||
</record>
|
||||
|
||||
@@ -360,22 +425,8 @@
|
||||
<field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
|
||||
<field name="description">Pasta 4 Formaggi</field>
|
||||
<field name="amount">-5.50</field>
|
||||
<field name="order_id" ref="order_4"/>
|
||||
<field name="order_line_id" ref="order_line_3"/>
|
||||
<field name="state">order</field>
|
||||
</record>
|
||||
|
||||
<record model="lunch.alert" id="alert_1">
|
||||
<field name="alert_type">week</field>
|
||||
<field name="monday" eval="True"/>
|
||||
<field name="tuesday" eval="True"/>
|
||||
<field name="wednesday" eval="True"/>
|
||||
<field name="thursday" eval="True"/>
|
||||
<field name="friday" eval="True"/>
|
||||
<field name="saturday" eval="False"/>
|
||||
<field name="sunday" eval="False"/>
|
||||
<field name="start_hour">7</field>
|
||||
<field name="end_hour">23</field>
|
||||
<field name="message">Lunch must be ordered before 10:30 AM</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import lunch
|
||||
from . import lunch_cashmove
|
||||
from . import lunch_order
|
||||
from . import lunch_product
|
||||
from . import lunch_supplier
|
||||
|
||||
@@ -1,471 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import json
|
||||
import datetime
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import AccessError, ValidationError
|
||||
from odoo.addons import decimal_precision as dp
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
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())
|
||||
|
||||
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,
|
||||
digits=dp.get_precision('Account'))
|
||||
state = fields.Selection([('new', 'New'),
|
||||
('confirmed', 'Received'),
|
||||
('ordered', 'Ordered'),
|
||||
('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)
|
||||
|
||||
template = self.env.ref('lunch.lunch_order_mail_supplier', raise_if_not_found=False)
|
||||
ctx = dict(
|
||||
default_composition_mode='mass_mail',
|
||||
default_use_template=bool(template),
|
||||
default_template_id=template.id,
|
||||
default_lang=order['supplier'].lang or self.env.user.lang,
|
||||
order=order,
|
||||
lines=lines,
|
||||
)
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'mail.compose.message',
|
||||
'target': 'new',
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
else:
|
||||
raise AccessError(_("Only your lunch manager processes the orders."))
|
||||
|
||||
@api.one
|
||||
def confirm(self):
|
||||
"""
|
||||
confirm one or more order line, update order status and create new cashmove
|
||||
"""
|
||||
if self.user_has_groups("lunch.group_lunch_manager"):
|
||||
if self.state != 'confirmed':
|
||||
values = {
|
||||
'user_id': self.user_id.id,
|
||||
'amount': -self.price,
|
||||
'description': self.product_id.name,
|
||||
'order_id': self.id,
|
||||
'state': 'order',
|
||||
'date': self.date,
|
||||
}
|
||||
self.env['lunch.cashmove'].create(values)
|
||||
self.state = 'confirmed'
|
||||
else:
|
||||
raise AccessError(_("Only your lunch manager sets the orders as received."))
|
||||
|
||||
@api.one
|
||||
def cancel(self):
|
||||
"""
|
||||
cancel one or more order.line, update order status and unlink existing cashmoves
|
||||
"""
|
||||
if self.user_has_groups("lunch.group_lunch_manager"):
|
||||
self.state = 'cancelled'
|
||||
self.cashmove.unlink()
|
||||
else:
|
||||
raise AccessError(_("Only your lunch manager cancels the orders."))
|
||||
|
||||
|
||||
class LunchProduct(models.Model):
|
||||
""" Products available to order. A product is linked to a specific vendor. """
|
||||
_name = 'lunch.product'
|
||||
_description = 'Lunch Product'
|
||||
|
||||
name = fields.Char('Product', required=True)
|
||||
category_id = fields.Many2one('lunch.product.category', 'Product Category', required=True)
|
||||
description = fields.Text('Description')
|
||||
price = fields.Float('Price', digits=dp.get_precision('Account'))
|
||||
supplier = fields.Many2one('res.partner', 'Vendor')
|
||||
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)]
|
||||
|
||||
|
||||
class LunchProductCategory(models.Model):
|
||||
""" Category of the product such as pizza, sandwich, pasta, chinese, burger... """
|
||||
_name = 'lunch.product.category'
|
||||
_description = 'Lunch Product Category'
|
||||
|
||||
name = fields.Char('Product Category', required=True)
|
||||
|
||||
|
||||
class LunchCashMove(models.Model):
|
||||
""" Two types of cashmoves: payment (credit) or order (debit) """
|
||||
_name = 'lunch.cashmove'
|
||||
_description = 'Lunch Cashmove'
|
||||
|
||||
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')
|
||||
|
||||
@api.multi
|
||||
def name_get(self):
|
||||
return [(cashmove.id, '%s %s' % (_('Lunch Cashmove'), '#%d' % cashmove.id)) for cashmove in self]
|
||||
|
||||
|
||||
class LunchAlert(models.Model):
|
||||
""" Alerts to display during a lunch order. An alert can be specific to a
|
||||
given day, weekly or daily. The alert is displayed from start to end hour. """
|
||||
_name = 'lunch.alert'
|
||||
_description = 'Lunch Alert'
|
||||
_rec_name = 'message'
|
||||
|
||||
display = fields.Boolean(compute='_compute_display_get')
|
||||
message = fields.Text('Message', required=True)
|
||||
alert_type = fields.Selection([('specific', 'Specific Day'),
|
||||
('week', 'Every Week'),
|
||||
('days', 'Every Day')],
|
||||
string='Recurrence', required=True, index=True, default='specific')
|
||||
partner_id = fields.Many2one('res.partner', string="Vendor",
|
||||
help="If specified, the selected vendor can be ordered only on selected days")
|
||||
specific_day = fields.Date('Day', default=fields.Date.context_today)
|
||||
monday = fields.Boolean('Monday')
|
||||
tuesday = fields.Boolean('Tuesday')
|
||||
wednesday = fields.Boolean('Wednesday')
|
||||
thursday = fields.Boolean('Thursday')
|
||||
friday = fields.Boolean('Friday')
|
||||
saturday = fields.Boolean('Saturday')
|
||||
sunday = fields.Boolean('Sunday')
|
||||
start_hour = fields.Float('Between', oldname='active_from', required=True, default=7)
|
||||
end_hour = fields.Float('And', oldname='active_to', required=True, default=23)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
@api.multi
|
||||
def name_get(self):
|
||||
return [(alert.id, '%s %s' % (_('Alert'), '#%d' % alert.id)) for alert in self]
|
||||
|
||||
@api.depends('alert_type', 'specific_day', 'monday', 'tuesday', 'thursday',
|
||||
'friday', 'saturday', 'sunday', 'start_hour', 'end_hour')
|
||||
def _compute_display_get(self):
|
||||
"""
|
||||
This method check if the alert can be displayed today
|
||||
if alert type is specific : compare specific_day(date) with today's date
|
||||
if alert type is week : check today is set as alert (checkbox true) eg. self['monday']
|
||||
if alert type is day : True
|
||||
return : Message if can_display_alert is True else False
|
||||
"""
|
||||
days_codes = {'0': 'sunday',
|
||||
'1': 'monday',
|
||||
'2': 'tuesday',
|
||||
'3': 'wednesday',
|
||||
'4': 'thursday',
|
||||
'5': 'friday',
|
||||
'6': 'saturday'}
|
||||
fullday = False
|
||||
now = datetime.datetime.now()
|
||||
if self.env.context.get('lunch_date'):
|
||||
# lunch_date is a fields.Date -> 00:00:00
|
||||
lunch_date = fields.Datetime.from_string(self.env.context['lunch_date'])
|
||||
# if lunch_date is in the future, planned lunch, ignore hours
|
||||
fullday = lunch_date > now
|
||||
now = max(lunch_date, now)
|
||||
mynow = fields.Datetime.context_timestamp(self, now)
|
||||
|
||||
for alert in self:
|
||||
can_display_alert = {
|
||||
'specific': (str(alert.specific_day) == fields.Date.to_string(mynow)),
|
||||
'week': alert[days_codes[mynow.strftime('%w')]],
|
||||
'days': True
|
||||
}
|
||||
|
||||
if can_display_alert[alert.alert_type]:
|
||||
hour_to = int(alert.end_hour)
|
||||
min_to = int((alert.end_hour - hour_to) * 60)
|
||||
to_alert = datetime.time(hour_to, min_to)
|
||||
hour_from = int(alert.start_hour)
|
||||
min_from = int((alert.start_hour - hour_from) * 60)
|
||||
from_alert = datetime.time(hour_from, min_from)
|
||||
|
||||
if fullday or (from_alert <= mynow.time() <= to_alert):
|
||||
alert.display = True
|
||||
else:
|
||||
alert.display = False
|
||||
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 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'
|
||||
|
||||
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_line_id = fields.Many2one('lunch.order.line', 'Order', ondelete='cascade')
|
||||
state = fields.Selection([('order', 'Order'), ('payment', 'Payment')],
|
||||
'Is an order or a payment', default='payment')
|
||||
|
||||
@api.multi
|
||||
def name_get(self):
|
||||
return [(cashmove.id, '%s %s' % (_('Lunch Cashmove'), '#%d' % cashmove.id)) for cashmove in self]
|
||||
|
||||
@api.model
|
||||
def get_wallet_balance(self, user):
|
||||
return float_round(sum(move['amount'] for move in self.search_read([('user_id', '=', user.id)], ['amount'])), precision_digits=2)
|
||||
@@ -0,0 +1,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, 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.
|
||||
"""
|
||||
_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())
|
||||
|
||||
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'),
|
||||
('ordered', 'Ordered'),
|
||||
('confirmed', 'Received'),
|
||||
('cancelled', 'Cancelled')],
|
||||
'Status', readonly=True, index=True, copy=False, default='new')
|
||||
company_id = fields.Many2one('res.company', related='user_id.company_id', store=True, readonly=False)
|
||||
currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True, store=True)
|
||||
cash_move_balance = fields.Monetary(compute='_compute_cash_move_balance', multi='cash_move_balance')
|
||||
balance_visible = fields.Boolean(compute='_compute_cash_move_balance', multi='cash_move_balance')
|
||||
|
||||
mail_sent = fields.Boolean('Mail Sent', readonly=True)
|
||||
supplier_ids = fields.Many2many('lunch.supplier', compute='_compute_supplier_ids', store=True)
|
||||
|
||||
@api.depends('order_line_ids', 'order_line_ids.supplier_id', 'order_line_ids.product_id')
|
||||
def _compute_supplier_ids(self):
|
||||
for order in self:
|
||||
order.supplier_ids = order.mapped('order_line_ids.supplier_id')
|
||||
|
||||
@api.depends('order_line_ids', 'order_line_ids.quantity', 'order_line_ids.price')
|
||||
def _compute_total(self):
|
||||
"""
|
||||
get and sum the order lines' price
|
||||
"""
|
||||
self.ensure_one()
|
||||
self.total = sum(
|
||||
orderline.price for orderline in self.order_line_ids)
|
||||
|
||||
@api.multi
|
||||
def name_get(self):
|
||||
return [(order.id, '%s %s' % (_('Lunch Order'), '#%d' % order.id)) for order in self]
|
||||
|
||||
@api.depends('user_id')
|
||||
def _compute_cash_move_balance(self):
|
||||
self.ensure_one()
|
||||
domain = [('user_id', '=', self.user_id.id)]
|
||||
lunch_cash = self.env['lunch.cashmove'].read_group(domain, ['amount', 'user_id'], ['user_id'])
|
||||
if lunch_cash:
|
||||
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.constrains('date')
|
||||
def _check_date(self):
|
||||
"""
|
||||
Prevents the user to create an order in the past
|
||||
"""
|
||||
self.ensure_one()
|
||||
date_order = self.date
|
||||
date_today = fields.Date.context_today(self)
|
||||
if date_order < date_today:
|
||||
raise ValidationError(_('The date of your order is in the past.'))
|
||||
|
||||
def action_order(self):
|
||||
for order in self:
|
||||
for line in order.order_line_ids:
|
||||
self.env['lunch.cashmove'].sudo().create({
|
||||
'user_id': line.user_id.id,
|
||||
'amount': -line.price,
|
||||
'description': line.product_id.name,
|
||||
'order_line_id': line.id,
|
||||
'state': 'order',
|
||||
'date': line.date,
|
||||
})
|
||||
|
||||
self.mapped('order_line_ids').write({'state': 'ordered'})
|
||||
self.write({'state': 'ordered'})
|
||||
|
||||
def action_confirm(self, supplier=None):
|
||||
for order in self:
|
||||
if supplier:
|
||||
order.order_line_ids.filtered(lambda line: line.state == 'ordered' and line.supplier_id == supplier).write({'state': 'confirmed'})
|
||||
else:
|
||||
order.order_line_ids.filtered(lambda line: line.state == 'ordered').write({'state': 'confirmed'})
|
||||
order.write({'state': 'confirmed',
|
||||
'mail_sent': True})
|
||||
|
||||
def action_cancel(self):
|
||||
for order in self:
|
||||
# RLI FIXME maybe we don't need to filter
|
||||
order.order_line_ids.filtered(lambda line: line.state != 'confirmed').action_cancel()
|
||||
order.write({'state': 'cancelled'})
|
||||
|
||||
|
||||
class LunchOrderLine(models.Model):
|
||||
_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)
|
||||
topping_ids = fields.Many2many('lunch.product', string='Toppings')
|
||||
product_id = fields.Many2one('lunch.product', string="Product", domain=[('is_topping', '=', False)], required=True)
|
||||
category_id = fields.Many2one('lunch.product.category', string='Product Category',
|
||||
related='product_id.category_id', readonly=True, store=True)
|
||||
date = fields.Date(string='Date', related='order_id.date', readonly=True, store=True)
|
||||
supplier_id = fields.Many2one('lunch.supplier', string='Vendor', related='product_id.supplier_id',
|
||||
readonly=True, store=True, index=True)
|
||||
user_id = fields.Many2one('res.users', string='User', related='order_id.user_id',
|
||||
readonly=True, store=True)
|
||||
note = fields.Text('Note')
|
||||
price = fields.Float('Total Price', compute='_compute_total_price', readonly=True, store=True,
|
||||
digits=dp.get_precision('Account'))
|
||||
state = fields.Selection([('new', 'New'),
|
||||
('ordered', 'Ordered'),
|
||||
('confirmed', 'Received'),
|
||||
('cancelled', 'Cancelled')],
|
||||
'Status', readonly=True, index=True, default='new')
|
||||
cashmove = fields.One2many('lunch.cashmove', 'order_line_id', 'Cash Move')
|
||||
currency_id = fields.Many2one('res.currency', related='order_id.currency_id', readonly=False)
|
||||
quantity = fields.Float('Quantity', required=True, default=1)
|
||||
|
||||
@api.depends('topping_ids', 'product_id', 'quantity')
|
||||
def _compute_total_price(self):
|
||||
for line in self:
|
||||
line.price = line.quantity * sum(product.price for product in line.product_id | line.topping_ids)
|
||||
|
||||
def update_quantity(self, increment):
|
||||
for line in self:
|
||||
old_cost = line.price
|
||||
line.quantity += increment
|
||||
new_cost = line.price
|
||||
if new_cost - old_cost > self.env['lunch.cashmove'].get_wallet_balance(line.user_id):
|
||||
raise UserError(_('Your wallet does not contain enough money to order that.'
|
||||
'To add some money to your wallet, please contact your lunch manager.'))
|
||||
cashmove = self.env['lunch.cashmove'].search([('order_line_id', '=', line.id)], limit=1)
|
||||
if cashmove:
|
||||
cashmove.amount = -new_cost
|
||||
if line.quantity <= 0:
|
||||
order_id = line.order_id
|
||||
line.sudo().unlink()
|
||||
if not order_id.order_line_ids:
|
||||
order_id.sudo().unlink()
|
||||
|
||||
def action_confirm(self):
|
||||
self.write({'state': 'confirmed'})
|
||||
|
||||
for order in self.mapped('order_id'):
|
||||
if all(line.state == 'confirmed' for line in order.order_line_ids):
|
||||
order.write({'state': 'confirmed', 'mail_sent': True})
|
||||
|
||||
def action_cancel(self):
|
||||
for line in self:
|
||||
line.cashmove.sudo().unlink()
|
||||
|
||||
self.write({'state': 'cancelled'})
|
||||
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
|
||||
from odoo.addons import decimal_precision as dp
|
||||
|
||||
|
||||
class LunchProductCategory(models.Model):
|
||||
""" Category of the product such as pizza, sandwich, pasta, chinese, burger... """
|
||||
_name = 'lunch.product.category'
|
||||
_description = 'Lunch Product Category'
|
||||
|
||||
name = fields.Char('Product Category', required=True)
|
||||
|
||||
|
||||
class LunchProduct(models.Model):
|
||||
""" Products available to order. A product is linked to a specific vendor. """
|
||||
_name = 'lunch.product'
|
||||
_description = 'Lunch Product'
|
||||
|
||||
name = fields.Char('Name', required=True)
|
||||
category_id = fields.Many2one('lunch.product.category', 'Product Category')
|
||||
description = fields.Text('Description')
|
||||
price = fields.Float('Price', digits=dp.get_precision('Account'), required=True)
|
||||
supplier_id = fields.Many2one('lunch.supplier', 'Vendor', required=True)
|
||||
active = fields.Boolean(default=True)
|
||||
is_topping = fields.Boolean("This product is an extra garniture")
|
||||
|
||||
# image: all image fields are base64 encoded and PIL-supported
|
||||
image = fields.Binary(
|
||||
"Image", attachment=True,
|
||||
help="This field holds the image used as image for the product, limited to 1024x1024px.")
|
||||
image_medium = fields.Binary(
|
||||
"Medium-sized image", attachment=True,
|
||||
help="Medium-sized image of the product. It is automatically "
|
||||
"resized as a 128x128px image, with aspect ratio preserved, "
|
||||
"only when the image exceeds one of those sizes. Use this field in form views or some kanban views.")
|
||||
image_small = fields.Binary(
|
||||
"Small-sized image", attachment=True,
|
||||
help="Small-sized image of the product. It is automatically "
|
||||
"resized as a 64x64px image, with aspect ratio preserved. "
|
||||
"Use this field anywhere a small image is required.")
|
||||
|
||||
already_ordered = fields.Boolean('Has Already Been Ordered', compute='_compute_already_ordered')
|
||||
|
||||
@api.model
|
||||
def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None):
|
||||
res = super(LunchProduct, self).search_read(domain, fields, offset, limit, order)
|
||||
|
||||
if not order and 'already_ordered' in fields:
|
||||
res = sorted(res, key=lambda x: x['already_ordered'], reverse=True)
|
||||
|
||||
return res
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for values in vals_list:
|
||||
tools.image_resize_images(values)
|
||||
return super(LunchProduct, self).create(vals_list)
|
||||
|
||||
def write(self, values):
|
||||
tools.image_resize_images(values)
|
||||
return super(LunchProduct, self).write(values)
|
||||
|
||||
def _compute_already_ordered(self):
|
||||
last_ordered = fields.Date.today() - relativedelta(weeks=2)
|
||||
for product in self:
|
||||
product.already_ordered = bool(self.env['lunch.order.line'].search_count(
|
||||
[('product_id', '=', product.id), ('date', '>=', last_ordered), ('user_id', '=', self.env.user.id)]))
|
||||
@@ -0,0 +1,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import math
|
||||
import pytz
|
||||
|
||||
from datetime import datetime, time
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import float_round
|
||||
|
||||
from odoo.addons.base.models.res_partner import _tz_get
|
||||
|
||||
|
||||
WEEKDAY_TO_NAME = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
|
||||
|
||||
def float_to_time(hours, tz=None):
|
||||
""" Convert a number of hours into a time object. """
|
||||
if hours == 24.0:
|
||||
return time.max
|
||||
fractional, integral = math.modf(hours)
|
||||
res = time(int(integral), int(float_round(60 * fractional, precision_digits=0)), 0)
|
||||
if tz:
|
||||
res = res.replace(tzinfo=pytz.timezone(tz))
|
||||
return res
|
||||
|
||||
def time_to_float(t):
|
||||
return float_round(t.hour + t.minute/60 + t.second/3600, precision_digits=2)
|
||||
|
||||
class LunchSupplier(models.Model):
|
||||
_name = 'lunch.supplier'
|
||||
_description = 'Lunch Supplier'
|
||||
|
||||
partner_id = fields.Many2one('res.partner', string='Linked partner', required=True)
|
||||
|
||||
name = fields.Char('Name', related='partner_id.name')
|
||||
description = fields.Text('Description')
|
||||
|
||||
email = fields.Char(related='partner_id.email')
|
||||
email_formatted = fields.Char(related='partner_id.email_formatted', readonly=True)
|
||||
phone = fields.Char(related='partner_id.phone')
|
||||
street = fields.Char(related='partner_id.street')
|
||||
street2 = fields.Char(related='partner_id.street2')
|
||||
zip_code = fields.Char(related='partner_id.zip')
|
||||
city = fields.Char(related='partner_id.city')
|
||||
state_id = fields.Many2one("res.country.state", related='partner_id.state_id')
|
||||
country_id = fields.Many2one('res.country', related='partner_id.country_id')
|
||||
|
||||
vat = fields.Char(related='partner_id.vat')
|
||||
|
||||
image = fields.Binary(related='partner_id.image', readonly=False)
|
||||
image_medium = fields.Binary(related='partner_id.image_medium', readonly=False)
|
||||
image_small = fields.Binary(related='partner_id.image_small', readonly=False)
|
||||
|
||||
send_by = fields.Selection([
|
||||
('phone', 'Phone'),
|
||||
('mail', 'Email'),
|
||||
], 'Send Order By', default='phone')
|
||||
automatic_email_send = fields.Boolean('Automatic Email Sending')
|
||||
automatic_email_time = fields.Float('Hour')
|
||||
|
||||
recurrency = fields.Selection([('once', 'Specific Day'), ('reccurent', 'Reccurent')], 'Recurrency', default='once')
|
||||
recurrency_from = fields.Float('From')
|
||||
recurrency_to = fields.Float('To')
|
||||
recurrency_date = fields.Date('Day', default=fields.Date.today())
|
||||
recurrency_date_from = fields.Datetime('from', compute='_compute_recurrency_date_from', store=True)
|
||||
recurrency_date_to = fields.Datetime('to', compute='_compute_recurrency_date_to', store=True)
|
||||
recurrency_monday = fields.Boolean('Monday')
|
||||
recurrency_tuesday = fields.Boolean('Tuesday')
|
||||
recurrency_wednesday = fields.Boolean('Wednesday')
|
||||
recurrency_thursday = fields.Boolean('Thursday')
|
||||
recurrency_friday = fields.Boolean('Friday')
|
||||
recurrency_saturday = fields.Boolean('Saturday')
|
||||
recurrency_sunday = fields.Boolean('Sunday')
|
||||
|
||||
available_today = fields.Boolean('This is True when if the supplier is available today',
|
||||
compute='_compute_available_today', search='_search_available_today')
|
||||
|
||||
tz = fields.Selection(_tz_get, string='Timezone', required=True, default='UTC')
|
||||
|
||||
_sql_constraints = [
|
||||
('automatic_email_time_range', 'CHECK(automatic_email_time >= 0 AND automatic_email_time <= 24)', 'Automatic Email Sending Time should be between 0 and 24'),
|
||||
('recurrency_from', 'CHECK(recurrency_from >= 0 AND recurrency_from <= 24)', 'Recurrency From should be between 0 and 24'),
|
||||
('recurrency_to', 'CHECK(recurrency_to >= 0 AND recurrency_to <= 24)', 'Recurrency To should be between 0 and 24')
|
||||
]
|
||||
|
||||
@api.model
|
||||
def _auto_email_send(self):
|
||||
"""
|
||||
This method is called every 10 minutes via a cron.
|
||||
Its job is simply to get all the orders made for each supplier and send an email
|
||||
automatically to the supplier if the supplier is configured for it and we are ready
|
||||
to send it (usually at 11am or so)
|
||||
"""
|
||||
records = self.search([('automatic_email_send', '=', True)])
|
||||
|
||||
for supplier in records:
|
||||
send_at = datetime.combine(fields.Date.today(), float_to_time(supplier.automatic_email_time, supplier.tz)).astimezone(pytz.UTC).replace(tzinfo=None)
|
||||
if supplier.available_today and fields.Datetime.now() > send_at:
|
||||
orders = self.env['lunch.order'].search([('mail_sent', '=', False), ('supplier_ids', 'in', [supplier.id]), ('state', '=', 'ordered')])
|
||||
|
||||
if orders:
|
||||
order = {
|
||||
'company_name': orders[0].company_id.name,
|
||||
'currency_id': orders[0].currency_id.id,
|
||||
'supplier_id': supplier.partner_id.id,
|
||||
'supplier_name': supplier.name,
|
||||
'supplier_email': supplier.email_formatted
|
||||
}
|
||||
|
||||
lines = orders.mapped('order_line_ids').filtered(lambda line: line.supplier_id == supplier)
|
||||
lines = [{'product': line.product_id.name, 'note': line.note, 'quantity': line.quantity, 'price': line.price} for line in lines]
|
||||
|
||||
order['amount_total'] = sum(line['price'] for line in lines)
|
||||
|
||||
self.env.ref('lunch.lunch_order_mail_supplier').with_context(order=order, lines=lines).send_mail(supplier.id)
|
||||
|
||||
orders.action_confirm(supplier)
|
||||
|
||||
@api.depends('recurrency_date', 'recurrency_from')
|
||||
def _compute_recurrency_date_from(self):
|
||||
for supplier in self:
|
||||
if supplier.recurrency_date and supplier.recurrency_from:
|
||||
supplier.recurrency_date_from = datetime.combine(supplier.recurrency_date, float_to_time(supplier.recurrency_from))
|
||||
|
||||
@api.depends('recurrency_date', 'recurrency_to')
|
||||
def _compute_recurrency_date_to(self):
|
||||
for supplier in self:
|
||||
if supplier.recurrency_date and supplier.recurrency_to:
|
||||
supplier.recurrency_date_to = datetime.combine(supplier.recurrency_date, float_to_time(supplier.recurrency_to))
|
||||
|
||||
@api.depends('recurrency', 'recurrency_date', 'recurrency_from', 'recurrency_to', 'recurrency_monday',
|
||||
'recurrency_tuesday', 'recurrency_wednesday', 'recurrency_thursday',
|
||||
'recurrency_friday', 'recurrency_saturday', 'recurrency_sunday')
|
||||
def _compute_available_today(self):
|
||||
now = fields.Datetime.now().replace(tzinfo=pytz.UTC)
|
||||
|
||||
for supplier in self:
|
||||
now = now.astimezone(pytz.timezone(supplier.tz))
|
||||
time_from = float_to_time(supplier.recurrency_from)
|
||||
time_to = float_to_time(supplier.recurrency_to)
|
||||
|
||||
if supplier.recurrency == 'once':
|
||||
supplier.available_today = (supplier.reccurrency_date_from <= now <= supplier.reccurrency_date_to)
|
||||
else:
|
||||
fieldname = 'recurrency_%s' % (WEEKDAY_TO_NAME[now.weekday()])
|
||||
supplier.available_today = supplier[fieldname] and (time_from <= now.time() <= time_to)
|
||||
|
||||
def _search_available_today(self, operator, value):
|
||||
if (not operator in ['=', '!=']) or (not value in [True, False]):
|
||||
return []
|
||||
|
||||
searching_for_true = (operator == '=' and value) or (operator == '!=' and not value)
|
||||
now = fields.Datetime.now()
|
||||
float_now = time_to_float(now.time())
|
||||
fieldname = 'recurrency_%s' % (WEEKDAY_TO_NAME[now.weekday()])
|
||||
|
||||
if searching_for_true:
|
||||
specific = expression.AND([
|
||||
[('recurrency', '=', 'once')],
|
||||
[('recurrency_date_from', '<=', now)],
|
||||
[('recurrency_date_to', '>=', now)]
|
||||
])
|
||||
else:
|
||||
specific = expression.AND([
|
||||
[('recurrency', '=', 'once')],
|
||||
expression.OR([
|
||||
[('recurrency_date_from', '>=', now)],
|
||||
[('recurrency_date_to', '<=', now)]
|
||||
])
|
||||
])
|
||||
|
||||
recurrence = expression.AND([
|
||||
[(fieldname, operator, value)],
|
||||
[('recurrency_from', '<=' if searching_for_true else '>=', float_now)],
|
||||
[('recurrency_to', '>=' if searching_for_true else '<=', float_now)]
|
||||
])
|
||||
|
||||
return expression.OR([specific, recurrence])
|
||||
@@ -4,10 +4,10 @@ order_line_manager,"Order Line user",model_lunch_order_line,group_lunch_manager,
|
||||
cashmove_manager,"Cashmove user",model_lunch_cashmove,group_lunch_manager,1,1,1,1
|
||||
product_manager,"Product user",model_lunch_product,group_lunch_manager,1,1,1,1
|
||||
product_category_manager,"Product category user",model_lunch_product_category,group_lunch_manager,1,1,1,1
|
||||
alert_manager,"Alert user",model_lunch_alert,group_lunch_manager,1,1,1,1
|
||||
order_user,"Order user",model_lunch_order,group_lunch_user,1,1,1,0
|
||||
order_line_user,"Order Line user",model_lunch_order_line,group_lunch_user,1,1,1,1
|
||||
cashmove_user,"Cashmove user",model_lunch_cashmove,group_lunch_user,1,0,0,0
|
||||
product_user,"Product user",model_lunch_product,group_lunch_user,1,0,0,0
|
||||
product_category_user,"Product category user",model_lunch_product_category,group_lunch_user,1,0,0,0
|
||||
alert_user,"Alert user",model_lunch_alert,group_lunch_user,1,0,0,0
|
||||
lunch_supplier_user,"Lunch Supplier User Rights",model_lunch_supplier,group_lunch_user,1,0,0,0
|
||||
lunch_supplier_manager,"Lunch Supplier Manager Rights",model_lunch_supplier,group_lunch_manager,1,1,1,1
|
||||
|
||||
|
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
@@ -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);
|
||||
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
odoo.define('lunch.LunchKanbanController', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* This file defines the Controller for the Lunch Kanban view, which is an
|
||||
* override of the KanbanController.
|
||||
*/
|
||||
|
||||
var core = require('web.core');
|
||||
var KanbanController = require('web.KanbanController');
|
||||
var LunchKanbanWidget = require('lunch.LunchKanbanWidget');
|
||||
var LunchPaymentDialog = require('lunch.LunchPaymentDialog');
|
||||
var session = require('web.session');
|
||||
|
||||
var qweb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
var LunchKanbanController = KanbanController.extend({
|
||||
custom_events: _.extend({}, KanbanController.prototype.custom_events, {
|
||||
add_money: '_onAddMoney',
|
||||
add_product: '_onAddProduct',
|
||||
change_user: '_onUserChanged',
|
||||
edit_order: '_onEditOrder',
|
||||
open_wizard: '_onOpenWizard',
|
||||
order_now: '_onOrderNow',
|
||||
remove_product: '_onRemoveProduct',
|
||||
save_order: '_onSaveOrder',
|
||||
unlink_order: '_onUnlinkOrder',
|
||||
}),
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this.userId = null;
|
||||
this.editMode = false;
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
start: function () {
|
||||
this.$el.addClass('o_lunch_kanban');
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
_fetchPaymentInfo: function (){
|
||||
return this._rpc({
|
||||
route: '/lunch/payment_message',
|
||||
});
|
||||
},
|
||||
|
||||
_onAddMoney: function (ev) {
|
||||
var self = this;
|
||||
|
||||
ev.stopPropagation();
|
||||
|
||||
this._showPaymentDialog(_t('Your wallet is empty!'));
|
||||
},
|
||||
_onAddProduct: function (ev) {
|
||||
var self = this;
|
||||
ev.stopPropagation();
|
||||
|
||||
this._rpc({
|
||||
model: 'lunch.order.line',
|
||||
method: 'update_quantity',
|
||||
args: [[ev.data.lineId], 1],
|
||||
}).then(function () {
|
||||
self.reload();
|
||||
});
|
||||
},
|
||||
_onEditOrder: function (ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
this.editMode = true;
|
||||
this.reload();
|
||||
},
|
||||
_onOpenWizard: function (ev) {
|
||||
var self = this;
|
||||
ev.stopPropagation();
|
||||
|
||||
var ctx = this.userId ? {default_user_id: this.userId}: {};
|
||||
|
||||
var options = {
|
||||
on_close: function () {
|
||||
self.reload();
|
||||
},
|
||||
};
|
||||
|
||||
this.do_action({
|
||||
res_model: 'lunch.order.line.temp',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[false, 'form']],
|
||||
target: 'new',
|
||||
context: _.extend(ctx, {default_product_id: ev.data.res_id}),
|
||||
}, options);
|
||||
},
|
||||
_onOrderNow: function (ev) {
|
||||
var self = this;
|
||||
ev.stopPropagation();
|
||||
|
||||
this._rpc({
|
||||
route: '/lunch/pay',
|
||||
params: {
|
||||
user_id: this.userId,
|
||||
},
|
||||
}).then(function (isPaid) {
|
||||
if (isPaid) {
|
||||
// TODO: feedback?
|
||||
self.reload();
|
||||
} else {
|
||||
self._showPaymentDialog();
|
||||
self.reload();
|
||||
}
|
||||
});
|
||||
},
|
||||
_onRemoveProduct: function (ev) {
|
||||
var self = this;
|
||||
ev.stopPropagation();
|
||||
|
||||
this._rpc({
|
||||
model: 'lunch.order.line',
|
||||
method: 'update_quantity',
|
||||
args: [[ev.data.lineId], -1],
|
||||
}).then(function () {
|
||||
self.reload();
|
||||
});
|
||||
},
|
||||
_onSaveOrder: function (ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
this.editMode = false;
|
||||
this.reload();
|
||||
},
|
||||
_onUserChanged: function (ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
this.userId = ev.data.userId;
|
||||
this.reload();
|
||||
},
|
||||
_onUnlinkOrder: function (ev) {
|
||||
var self = this;
|
||||
ev.stopPropagation();
|
||||
|
||||
this._rpc({
|
||||
route: '/lunch/trash',
|
||||
params: {
|
||||
user_id: this.userId,
|
||||
},
|
||||
}).then(function () {
|
||||
self.reload();
|
||||
});
|
||||
},
|
||||
_orderPaid: function () {
|
||||
Dialog.alert(this, _t('Your order has been paid have a nice day'), {'title': 'Order Paid'});
|
||||
},
|
||||
_orderNotPaid: function () {
|
||||
this._showPaymentDialog();
|
||||
},
|
||||
_showPaymentDialog: function (title) {
|
||||
var self = this;
|
||||
|
||||
title = title || '';
|
||||
|
||||
this._fetchPaymentInfo().then(function (data) {
|
||||
var paymentDialog = new LunchPaymentDialog(self, _.extend(data, {title: title}));
|
||||
paymentDialog.open();
|
||||
});
|
||||
},
|
||||
_update: function () {
|
||||
var self = this;
|
||||
|
||||
this._rpc({
|
||||
route: '/lunch/infos',
|
||||
params: {
|
||||
user_id: this.userId,
|
||||
},
|
||||
}).then(function (data) {
|
||||
if (self.widget) {
|
||||
self.widget.destroy();
|
||||
}
|
||||
data.wallet = parseFloat(data.wallet).toFixed(2);
|
||||
self.widget = new LunchKanbanWidget(self, _.extend(data, {edit: self.editMode}));
|
||||
self.widget.insertBefore(self.$('.o_kanban_view'));
|
||||
});
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
return LunchKanbanController;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
odoo.define('lunch.LunchKanbanModel', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* This file defines the Model for the Lunch Kanban view, which is an
|
||||
* override of the KanbanModel.
|
||||
*/
|
||||
|
||||
var KanbanModel = require('web.KanbanModel');
|
||||
var core = require('web.core');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
var LunchKanbanModel = KanbanModel.extend({
|
||||
});
|
||||
|
||||
return LunchKanbanModel;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
odoo.define('lunch.LunchKanbanRecord', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* This file defines the KanbanRecord for the Lunch Kanban view.
|
||||
*/
|
||||
|
||||
var KanbanRecord = require('web.KanbanRecord');
|
||||
|
||||
var LunchKanbanRecord = KanbanRecord.extend({
|
||||
_onGlobalClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
// ignore clicks on oe_kanban_action elements
|
||||
if (!$(ev.target).hasClass('oe_kanban_action')) {
|
||||
this.trigger_up('open_wizard', {res_id: this.id});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return LunchKanbanRecord;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
odoo.define('lunch.LunchKanbanView', function (require) {
|
||||
"use strict";
|
||||
|
||||
var LunchKanbanController = require('lunch.LunchKanbanController');
|
||||
var LunchKanbanModel = require('lunch.LunchKanbanModel');
|
||||
var LunchKanbanRenderer = require('lunch.LunchKanbanRenderer');
|
||||
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var KanbanView = require('web.KanbanView');
|
||||
var view_registry = require('web.view_registry');
|
||||
|
||||
var _lt = core._lt;
|
||||
|
||||
if (config.device.isMobile) {
|
||||
// use the classical KanbanView in mobile
|
||||
view_registry.add('lunch_kanban', KanbanView);
|
||||
return;
|
||||
}
|
||||
|
||||
var LunchKanbanView = KanbanView.extend({
|
||||
config: _.extend({}, KanbanView.prototype.config, {
|
||||
Controller: LunchKanbanController,
|
||||
Model: LunchKanbanModel,
|
||||
Renderer: LunchKanbanRenderer,
|
||||
}),
|
||||
display_name: _lt('Lunch Kanban'),
|
||||
groupable: false,
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
view_registry.add('lunch_kanban', LunchKanbanView);
|
||||
|
||||
return LunchKanbanView;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
odoo.define('lunch.LunchKanbanWidget', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var qweb = core.qweb;
|
||||
|
||||
var LunchKanbanWidget = Widget.extend({
|
||||
template: 'LunchKanbanWidget',
|
||||
custom_events: {
|
||||
},
|
||||
events: {
|
||||
'click .o_add_money': '_onAddMoney',
|
||||
'click .o_add_product': '_onAddProduct',
|
||||
'click .o_lunch_widget_edit': '_onEditOrder',
|
||||
'click .o_lunch_widget_order_button': '_onOrderNow',
|
||||
'click .o_remove_product': '_onRemoveProduct',
|
||||
'click .o_lunch_widget_save': '_onSaveOrder',
|
||||
'click .o_lunch_widget_unlink': '_onUnlinkOrder',
|
||||
'change select': '_onUserChanged',
|
||||
},
|
||||
|
||||
init: function (parent, params) {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
this.data = params;
|
||||
this.currency = session.get_currency(session.company_currency_id);
|
||||
},
|
||||
renderElement: function () {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
this.$('select').val(this.data.username);
|
||||
},
|
||||
|
||||
_onAddMoney: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.trigger_up('add_money', {});
|
||||
},
|
||||
_onAddProduct: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.trigger_up('add_product', {lineId: $(ev.currentTarget).data('id')});
|
||||
},
|
||||
_onEditOrder: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.trigger_up('edit_order', {});
|
||||
},
|
||||
_onOrderNow: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.trigger_up('order_now', {});
|
||||
},
|
||||
_onRemoveProduct: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.trigger_up('remove_product', {lineId: $(ev.currentTarget).data('id')});
|
||||
},
|
||||
_onSaveOrder: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.trigger_up('save_order', {});
|
||||
},
|
||||
_onUnlinkOrder: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.trigger_up('unlink_order', {});
|
||||
},
|
||||
_onUserChanged: function (ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
this.trigger_up('change_user', {userId: $(ev.currentTarget).find(':selected').data('user-id')});
|
||||
}
|
||||
});
|
||||
|
||||
return LunchKanbanWidget;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
.o_lunch_kanban {
|
||||
.o_pink {
|
||||
color: nth($o-colors, 10);
|
||||
}
|
||||
|
||||
.o_lunch_widget {
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
border-bottom: 1px solid #CED4DA;
|
||||
background-color: white;
|
||||
min-height: 90px;
|
||||
max-height: 90px;
|
||||
|
||||
.o_lunch_widget_info {
|
||||
padding: 5px 12px;
|
||||
margin: 4px 8px;
|
||||
width: 300px;
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
|
||||
.o_lunch_ordered {
|
||||
color: white;
|
||||
background-color: #F0D970;
|
||||
padding: 1px 3px;
|
||||
border-radius: .25em;
|
||||
}
|
||||
|
||||
.o_lunch_confirmed {
|
||||
color: white;
|
||||
background-color: #00BA4E;
|
||||
padding: 1px 3px;
|
||||
border-radius: .25em;
|
||||
}
|
||||
|
||||
.o_lunch_widget_title {
|
||||
font-weight: bold;
|
||||
font-size: initial;
|
||||
}
|
||||
|
||||
.o_lunch_widget_order_button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 4px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<div class="o_lunch_widget" t-name="LunchKanbanWidget">
|
||||
<div class="o_lunch_widget_info d-flex flex-row">
|
||||
<div>
|
||||
<img class="rounded-circle" t-attf-src="{{ widget.data.userimage }}"/>
|
||||
</div>
|
||||
<div class="w-50 ml-2 py-3">
|
||||
<div>
|
||||
<t t-if="widget.data.is_manager">
|
||||
<select class="o_input">
|
||||
<t t-foreach="widget.data.users" t-as="user">
|
||||
<option t-attf-data-user-id="{{ user.id }}" t-attf-value="{{ user.name }}"><t t-esc="user.name"/></option>
|
||||
</t>
|
||||
</select>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="widget.data.username"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="d-flex flex-row">
|
||||
<span class="flex-grow-1">
|
||||
<i class="fa fa-money"/>
|
||||
<t t-call="currency_field">
|
||||
<t t-set="value" t-value="widget.data.wallet"/>
|
||||
<t t-set="currency" t-value="widget.currency"/>
|
||||
</t>
|
||||
</span>
|
||||
<span>
|
||||
<a class="o_add_money" href="#">Add money</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_lunch_widget_info">
|
||||
<t t-if="widget.data.order">
|
||||
<t t-if="widget.data.state == 'ordered'">
|
||||
<t t-set="state_class" t-value="'o_lunch_ordered'"/>
|
||||
</t>
|
||||
<t t-else="widget.data.state == 'confirmed'">
|
||||
<t t-set="state_class" t-value="'o_lunch_confirmed'"/>
|
||||
</t>
|
||||
<div class="d-flex flex-row">
|
||||
<div class="flex-grow-1">
|
||||
<span class="o_lunch_widget_title">Your order</span>
|
||||
<a href="#"><i t-if="widget.data.state != 'confirmed'" class="fa fa-trash o_lunch_widget_unlink"/></a>
|
||||
<a t-if="widget.data.state == 'ordered' && !widget.data.edit" href="#" class="o_lunch_widget_edit">Edit</a>
|
||||
<a t-if="widget.data.state == 'ordered' && widget.data.edit" href="#" class="o_lunch_widget_save">Save</a>
|
||||
</div>
|
||||
<div t-if="widget.data.state != 'new'" t-esc="widget.data.state" t-attf-class="flex-grow-0 {{ state_class }}"/>
|
||||
</div>
|
||||
<div t-foreach="widget.data.lines" t-as="line">
|
||||
<div class="d-flex flex-row">
|
||||
<div class="flex-grow-0">
|
||||
<a class="o_remove_product" t-if="widget.data.state == 'new' || widget.data.edit" t-attf-data-id="{{ line.id }}" href="#"><i class="fa fa-minus-circle"/></a>
|
||||
<span t-esc="line.quantity"/>
|
||||
<a class="o_add_product" t-if="widget.data.state == 'new' || widget.data.edit" t-attf-data-id="{{ line.id }}" href="#"><i class="fa fa-plus-circle"/></a>
|
||||
</div>
|
||||
<div class="flex-grow-1 pl-2">
|
||||
<span t-esc="line.product[0]"/>
|
||||
</div>
|
||||
<div class="flex-grow-0">
|
||||
<t t-call="currency_field">
|
||||
<t t-set="value" t-value="line.product[1]"/>
|
||||
<t t-set="currency" t-value="widget.currency"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div t-foreach="line.toppings" t-as="topping" class="d-flex flex-row">
|
||||
<div class="flex-grow-1 pl-5 ml-5">
|
||||
<span>+ <t t-esc="topping[0]"/></span>
|
||||
</div>
|
||||
<div class="flex-grow-0">
|
||||
<t t-call="currency_field">
|
||||
<t t-set="value" t-value="topping[1]"/>
|
||||
<t t-set="currency" t-value="widget.currency"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_lunch_widget_info">
|
||||
<t t-if="widget.data.order && widget.data.state == 'new'">
|
||||
<div class="d-flex flex-row">
|
||||
<div class="o_lunch_widget_title flex-grow-1">
|
||||
Total
|
||||
</div>
|
||||
<div class="flex-grow-0 pr-1">
|
||||
<t t-call="currency_field">
|
||||
<t t-set="value" t-value="widget.data.total"/>
|
||||
<t t-set="currency" t-value="widget.currency"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary o_lunch_widget_order_button" t-attf-data-order-id="{{ widget.data.order }}">ORDER NOW</button>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t t-name="currency_field">
|
||||
<t t-if="currency">
|
||||
<t t-if="currency.position == 'after'">
|
||||
<t t-esc="value"/><t t-esc="currency.symbol"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="currency.symbol"/><t t-esc="value"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="value"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<div t-name="lunch.LunchPaymentDialog">
|
||||
<span t-esc="widget.message"/>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
odoo.define('lunch.lunchKanbanTests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var LunchKanbanView = require('lunch.LunchKanbanView');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
var createView = testUtils.createView;
|
||||
|
||||
QUnit.module('Views', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
product: {
|
||||
fields: {
|
||||
id: {string: 'ID', type: 'integer'},
|
||||
},
|
||||
records: [
|
||||
{id: 1, name: 'Tuna sandwich', price: 3.0},
|
||||
]
|
||||
},
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
QUnit.module('LunchKanbanView');
|
||||
|
||||
QUnit.test('Simple rendering of LunchKanbanView', function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
var lunchKanban = createView({
|
||||
View: LunchKanbanView,
|
||||
model: 'product',
|
||||
data: this.data,
|
||||
arch: '<kanban><templates><t t-name="kanban-box">' +
|
||||
'<div>' +
|
||||
'</div>' +
|
||||
'</t></templates></kanban>',
|
||||
mockRPC: function(route, args) {
|
||||
if (route === '/lunch/infos') {
|
||||
return $.when({
|
||||
order: 1,
|
||||
wallet: 20,
|
||||
username: 'Marc Demo',
|
||||
userimage: '',
|
||||
is_manager: false,
|
||||
users: [
|
||||
{id: 1, name: 'Mitchell Admin'},
|
||||
{id: 2, name: 'Marc Demo'},
|
||||
],
|
||||
total: 7.4,
|
||||
state: 'new',
|
||||
lines: [
|
||||
{id: 1, product: ['Pizza Italiana', 7.4], toppings: [], quantity: 1.0, price: 7.4}
|
||||
]
|
||||
});
|
||||
} else if (route.startsWith('data:image/png;base64,')) {
|
||||
return $.when();
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
var $section = $(lunchKanban.$('.o_lunch_widget_info')[0]);
|
||||
// username
|
||||
assert.strictEqual($section.find('div:eq(1) div:eq(0)').text().trim(), 'Marc Demo', 'Username should have been Marc Demo');
|
||||
// wallet_balance
|
||||
assert.strictEqual($section.find('div:eq(1) div:eq(1) span:eq(0)').text().trim(), '20.00', 'Wallet balance should have been 20');
|
||||
// your order section
|
||||
$section = $(lunchKanban.$('.o_lunch_widget_info')[1]);
|
||||
// only one line
|
||||
assert.strictEqual($section.find('div:eq(2) div.d-flex').length, 1, 'There should be only one line');
|
||||
// quantity = 1
|
||||
assert.strictEqual($section.find('div:eq(2) div:eq(0) span:eq(0)').text().trim(), '1', 'The line should contain only one product');
|
||||
// buttons to remove and to add product
|
||||
assert.strictEqual($section.find('.o_remove_product').length, 1, 'There should be a remove product button');
|
||||
assert.strictEqual($section.find('.o_add_product').length, 1, 'There should be a add product button');
|
||||
|
||||
$section = $(lunchKanban.$('.o_lunch_widget_info')[2]);
|
||||
// total
|
||||
assert.strictEqual($section.find('div:eq(0) div:eq(1)').text().trim(), '7.4', 'total should be of 7.4');
|
||||
// order now button
|
||||
assert.strictEqual($section.find('button').length, 1, 'order now button should be available');
|
||||
|
||||
lunchKanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('User interactions', function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
var state = 'new';
|
||||
|
||||
var lunchKanban = createView({
|
||||
View: LunchKanbanView,
|
||||
model: 'product',
|
||||
data: this.data,
|
||||
arch: '<kanban><templates><t t-name="kanban-box">' +
|
||||
'<div>' +
|
||||
'</div>' +
|
||||
'</t></templates></kanban>',
|
||||
mockRPC: function(route, args) {
|
||||
if (route === '/lunch/infos') {
|
||||
return $.when({
|
||||
order: 1,
|
||||
wallet: 20,
|
||||
username: 'Marc Demo',
|
||||
userimage: '',
|
||||
is_manager: false,
|
||||
users: [
|
||||
{id: 1, name: 'Mitchell Admin'},
|
||||
{id: 2, name: 'Marc Demo'},
|
||||
],
|
||||
total: 7.4,
|
||||
state: state,
|
||||
lines: [
|
||||
{id: 1, product: ['Pizza Italiana', 7.4], toppings: [], quantity: 1.0, price: 7.4}
|
||||
]
|
||||
});
|
||||
} else if (route === '/lunch/payment_message') {
|
||||
return $.when({message: 'Hello'});
|
||||
} else if (route === '/lunch/pay') {
|
||||
return $.when(true);
|
||||
} else if (args.method === 'update_quantity') {
|
||||
assert.step(args.args);
|
||||
return $.when();
|
||||
} else if (route.startsWith('data:image/png;base64,')) {
|
||||
return $.when();
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
lunchKanban.$('.o_add_money').click();
|
||||
var modal = $('.modal-dialog');
|
||||
assert.strictEqual(modal.find('.modal-body').text().trim(), 'Hello', 'Message should have been Hello');
|
||||
modal.find('.btn.btn-primary').click();
|
||||
|
||||
lunchKanban.$('.o_add_product').click();
|
||||
lunchKanban.$('.o_remove_product').click();
|
||||
|
||||
state = 'ordered';
|
||||
lunchKanban.$('.o_lunch_widget_order_button').click();
|
||||
// state is shown
|
||||
assert.strictEqual(lunchKanban.$('.o_lunch_ordered').length, 1, 'state should be shown as ordered');
|
||||
// no more buttons
|
||||
assert.strictEqual(lunchKanban.$('.o_remove_product').length, 0, 'button to remove product should not be shown anymore');
|
||||
assert.strictEqual(lunchKanban.$('.o_add_product').length, 0, 'button to add product should not be shown anymore');
|
||||
state = 'confirmed';
|
||||
lunchKanban.reload();
|
||||
// state is updated
|
||||
assert.strictEqual(lunchKanban.$('.o_lunch_confirmed').length, 1, 'state should be shown as confirmed');
|
||||
|
||||
assert.verifySteps([
|
||||
[[1], 1],
|
||||
[[1], -1],
|
||||
]);
|
||||
|
||||
lunchKanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Manager interactions', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var lunchKanban = createView({
|
||||
View: LunchKanbanView,
|
||||
model: 'product',
|
||||
data: this.data,
|
||||
arch: '<kanban><templates><t t-name="kanban-box">' +
|
||||
'<div>' +
|
||||
'</div>' +
|
||||
'</t></templates></kanban>',
|
||||
mockRPC: function(route, args) {
|
||||
if (route === '/lunch/infos') {
|
||||
return $.when({
|
||||
order: 1,
|
||||
wallet: 20,
|
||||
username: 'Marc Demo',
|
||||
userimage: '',
|
||||
is_manager: true,
|
||||
users: [
|
||||
{id: 1, name: 'Mitchell Admin'},
|
||||
{id: 2, name: 'Marc Demo'},
|
||||
],
|
||||
total: 7.4,
|
||||
state: 'new',
|
||||
lines: [
|
||||
{id: 1, product: ['Pizza Italiana', 7.4], toppings: [], quantity: 1.0, price: 7.4}
|
||||
]
|
||||
});
|
||||
} else if (route.startsWith('data:image/png;base64,')) {
|
||||
return $.when();
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
var select = lunchKanban.$('select.o_input');
|
||||
var options = select.find('option');
|
||||
|
||||
assert.strictEqual(select.length, 1, 'There should be a selection field');
|
||||
assert.strictEqual(options.length, 2, 'Select should have two options');
|
||||
assert.strictEqual(options.text().trim(), 'Mitchell AdminMarc Demo', 'Options should be the name of the users');
|
||||
assert.strictEqual($(options[0]).data('user-id'), 1, 'user id should be on option');
|
||||
assert.strictEqual($(options[1]).data('user-id'), 2, 'user id should be on option');
|
||||
|
||||
lunchKanban.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_lunch
|
||||
from . import common
|
||||
from . import test_supplier
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import common
|
||||
|
||||
|
||||
class TestsCommon(common.TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestsCommon, self).setUp()
|
||||
|
||||
self.partner_pizza_inn = self.env['res.partner'].create({
|
||||
'name': 'Pizza Inn',
|
||||
})
|
||||
|
||||
self.supplier_pizza_inn = self.env['lunch.supplier'].create({
|
||||
'partner_id': self.partner_pizza_inn.id,
|
||||
'send_by': 'mail',
|
||||
'automatic_email_send': True,
|
||||
'automatic_email_time': 11,
|
||||
'recurrency': 'reccurent',
|
||||
'recurrency_from': 8,
|
||||
'recurrency_to': 23,
|
||||
'recurrency_monday': True,
|
||||
'recurrency_tuesday': True,
|
||||
'recurrency_wednesday': True,
|
||||
'recurrency_thursday': True,
|
||||
'recurrency_friday': True,
|
||||
})
|
||||
|
||||
self.partner_coin_gourmand = self.env['res.partner'].create({
|
||||
'name': 'Coin Gourmand',
|
||||
})
|
||||
|
||||
self.supplier_coin_gourmand = self.env['lunch.supplier'].create({
|
||||
'partner_id': self.partner_coin_gourmand.id,
|
||||
'send_by': 'phone',
|
||||
'recurrency': 'reccurent',
|
||||
'recurrency_from': 8,
|
||||
'recurrency_to': 23,
|
||||
'recurrency_monday': True,
|
||||
'recurrency_tuesday': True,
|
||||
'recurrency_wednesday': True,
|
||||
'recurrency_thursday': True,
|
||||
'recurrency_friday': True,
|
||||
})
|
||||
|
||||
self.category_pizza = self.env['lunch.product.category'].create({
|
||||
'name': 'Pizza',
|
||||
})
|
||||
|
||||
self.category_sandwich = self.env['lunch.product.category'].create({
|
||||
'name': 'Sandwich',
|
||||
})
|
||||
|
||||
self.product_pizza = self.env['lunch.product'].create({
|
||||
'name': 'Pizza',
|
||||
'category_id': self.category_pizza.id,
|
||||
'price': 9,
|
||||
'supplier_id': self.supplier_pizza_inn.id,
|
||||
})
|
||||
|
||||
self.product_sandwich_tuna = self.env['lunch.product'].create({
|
||||
'name': 'Tuna Sandwich',
|
||||
'category_id': self.category_sandwich.id,
|
||||
'price': 3,
|
||||
'supplier_id': self.supplier_coin_gourmand.id,
|
||||
})
|
||||
|
||||
self.topping_olives = self.env['lunch.product'].create({
|
||||
'name': 'Olives',
|
||||
'price': 0.3,
|
||||
'supplier_id': self.supplier_pizza_inn.id,
|
||||
'is_topping': True,
|
||||
})
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo import fields
|
||||
|
||||
from odoo.addons.lunch.tests.common import TestsCommon
|
||||
|
||||
|
||||
class TestSupplier(TestsCommon):
|
||||
def setUp(self):
|
||||
super(TestSupplier, self).setUp()
|
||||
|
||||
self.monday_3am = datetime(2018, 10, 29, 3, 0, 0)
|
||||
self.monday_10am = datetime(2018, 10, 29, 10, 0, 0)
|
||||
self.monday_1pm = datetime(2018, 10, 29, 13, 0, 0)
|
||||
self.monday_8pm = datetime(2018, 10, 29, 20, 0, 0)
|
||||
|
||||
self.saturday_3am = datetime(2018, 11, 3, 3, 0, 0)
|
||||
self.saturday_10am = datetime(2018, 11, 3, 10, 0, 0)
|
||||
self.saturday_1pm = datetime(2018, 11, 3, 13, 0, 0)
|
||||
self.saturday_8pm = datetime(2018, 11, 3, 20, 0, 0)
|
||||
|
||||
def test_compute_available_today(self):
|
||||
tests = [(self.monday_3am, False), (self.monday_10am, True),
|
||||
(self.monday_1pm, True), (self.monday_8pm, True),
|
||||
(self.saturday_3am, False), (self.saturday_10am, False),
|
||||
(self.saturday_1pm, False), (self.saturday_8pm, False)]
|
||||
|
||||
for value, result in tests:
|
||||
with patch.object(fields.Datetime, 'now', return_value=value) as _:
|
||||
assert self.supplier_pizza_inn.available_today == result,\
|
||||
'supplier pizza inn should %s considered available on %s' % ('be' if result else 'not be', value)
|
||||
|
||||
self.env['lunch.supplier'].invalidate_cache(['available_today'], [self.supplier_pizza_inn.id])
|
||||
|
||||
def test_search_available_today(self):
|
||||
Supplier = self.env['lunch.supplier']
|
||||
|
||||
tests = [(self.monday_3am, 3.0, 'monday'), (self.monday_10am, 10.0, 'monday'),
|
||||
(self.monday_1pm, 13.0, 'monday'), (self.monday_8pm, 20.0, 'monday'),
|
||||
(self.saturday_3am, 3.0, 'saturday'), (self.saturday_10am, 10.0, 'saturday'),
|
||||
(self.saturday_1pm, 13.0, 'saturday'), (self.saturday_8pm, 20.0, 'saturday')]
|
||||
|
||||
assert Supplier._search_available_today('>', 7) == []
|
||||
assert Supplier._search_available_today('>', True) == []
|
||||
|
||||
for value, rvalue, dayname in tests:
|
||||
with patch.object(fields.Datetime, 'now', return_value=value) as _:
|
||||
assert Supplier._search_available_today('=', True) == ['|', '&', '&', ('recurrency', '=', 'once'), ('recurrency_date_from', '<=', value),
|
||||
('recurrency_date_to', '>=', value), '&', '&', ('recurrency_%s' % (dayname), '=', True),
|
||||
('recurrency_from', '<=', rvalue), ('recurrency_to', '>=', rvalue)], 'Wrong domain generated for values (%s, %s)' % (value, rvalue)
|
||||
|
||||
assert self.supplier_pizza_inn in Supplier.search([('available_today', '=', True)])
|
||||
|
||||
def test_auto_email_send(self):
|
||||
order = self.env['lunch.order'].create({})
|
||||
line = self.env['lunch.order.line'].create({
|
||||
'order_id': order.id,
|
||||
'product_id': self.product_pizza.id,
|
||||
})
|
||||
|
||||
assert len(order.supplier_ids) == 1 and self.supplier_pizza_inn in order.supplier_ids
|
||||
|
||||
order.action_order()
|
||||
assert order.state == 'ordered'
|
||||
assert line.state == 'ordered'
|
||||
|
||||
with patch.object(fields.Datetime, 'now', return_value=self.monday_1pm):
|
||||
with patch.object(fields.Date, 'today', return_value=self.monday_1pm.date()):
|
||||
self.supplier_pizza_inn._auto_email_send()
|
||||
|
||||
assert order.state == 'confirmed'
|
||||
assert line.state == 'confirmed'
|
||||
assert order.mail_sent
|
||||
|
||||
# Do the same but this time order contains multiple lines with products from at least two suppliers
|
||||
# Check if the lines for pizza_inn have been sent, and put in state='confirmed' only those lines should be
|
||||
# moreover the order should not yet be in state='confirmed' as there still are some lines to confirm
|
||||
order = self.env['lunch.order'].create({})
|
||||
line = self.env['lunch.order.line'].create({
|
||||
'order_id': order.id,
|
||||
'product_id': self.product_pizza.id,
|
||||
'topping_ids': [(6, 0, [self.topping_olives.id])],
|
||||
})
|
||||
line2 = self.env['lunch.order.line'].create({
|
||||
'order_id': order.id,
|
||||
'product_id': self.product_sandwich_tuna.id,
|
||||
})
|
||||
|
||||
assert len(order.supplier_ids) == 2 and self.supplier_coin_gourmand in order.supplier_ids and self.supplier_pizza_inn in order.supplier_ids
|
||||
|
||||
order.action_order()
|
||||
assert order.state == 'ordered'
|
||||
assert line.state == 'ordered'
|
||||
assert line2.state == 'ordered'
|
||||
|
||||
with patch.object(fields.Datetime, 'now', return_value=self.monday_1pm):
|
||||
with patch.object(fields.Date, 'today', return_value=self.monday_1pm.date()):
|
||||
self.supplier_pizza_inn._auto_email_send()
|
||||
|
||||
assert order.state == 'confirmed'
|
||||
assert line.state == 'confirmed'
|
||||
assert line2.state == 'ordered'
|
||||
|
||||
assert order.mail_sent
|
||||
|
||||
# Check with multiple orders
|
||||
order_1 = self.env['lunch.order'].create({})
|
||||
|
||||
line_1_1 = self.env['lunch.order.line'].create({
|
||||
'order_id': order_1.id,
|
||||
'product_id': self.product_pizza.id,
|
||||
})
|
||||
|
||||
line_1_2 = line_1_1 = self.env['lunch.order.line'].create({
|
||||
'order_id': order_1.id,
|
||||
'product_id': self.product_pizza.id,
|
||||
'topping_ids': [(6, 0, [self.topping_olives.id])]
|
||||
})
|
||||
|
||||
order_2 = self.env['lunch.order'].create({})
|
||||
|
||||
line_2_1 = self.env['lunch.order.line'].create({
|
||||
'order_id': order_2.id,
|
||||
'product_id': self.product_sandwich_tuna.id
|
||||
})
|
||||
|
||||
line_2_2 = self.env['lunch.order.line'].create({
|
||||
'order_id': order_2.id,
|
||||
'product_id': self.product_pizza.id,
|
||||
})
|
||||
|
||||
order_3 = self.env['lunch.order'].create({})
|
||||
|
||||
line_3_1 = self.env['lunch.order.line'].create({
|
||||
'order_id': order_3.id,
|
||||
'product_id': self.product_sandwich_tuna.id
|
||||
})
|
||||
|
||||
(order_1 | order_2 | order_3).action_order()
|
||||
|
||||
assert all(order.state == 'ordered' for order in [order_1, order_2, order_3])
|
||||
assert all(line.state == 'ordered' for line in [line_1_1, line_1_2, line_2_1, line_2_2, line_3_1])
|
||||
|
||||
with patch.object(fields.Datetime, 'now', return_value=self.monday_1pm):
|
||||
with patch.object(fields.Date, 'today', return_value=self.monday_1pm.date()):
|
||||
self.supplier_pizza_inn._auto_email_send()
|
||||
|
||||
assert all(order.state == 'confirmed' for order in [order_1, order_2])
|
||||
assert order_3.state == 'ordered'
|
||||
assert all(line.state == 'confirmed' for line in [line_1_1, line_1_2, line_2_2])
|
||||
assert all(line.state == 'ordered' for line in [line_2_1, line_3_1])
|
||||
|
||||
assert all(order.mail_sent for order in [order_1, order_2])
|
||||
assert not order_3.mail_sent
|
||||
@@ -0,0 +1,165 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="lunch_cashmove_view_search" 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 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="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"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="lunch_cashmove_view_form" model="ir.ui.view">
|
||||
<field name="name">lunch.cashmove.form</field>
|
||||
<field name="model">lunch.cashmove</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="cashmove form">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="user_id" required="1"/>
|
||||
<field name="date"/>
|
||||
<field name="amount"/>
|
||||
</group>
|
||||
<label for='description'/>
|
||||
<field name="description"/>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_lunch_cashmove_kanban" model="ir.ui.view">
|
||||
<field name="name">lunch.cashmove.kanban</field>
|
||||
<field name="model">lunch.cashmove</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="date"/>
|
||||
<field name="user_id"/>
|
||||
<field name="description"/>
|
||||
<field name="amount"/>
|
||||
<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"/></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_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">
|
||||
Register a payment
|
||||
</p><p>
|
||||
Payments are used to register liquidity movements. You can process those payments by your own means or by using installed facilities.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,226 @@
|
||||
<?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>
|
||||
<field name="model">lunch.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string='Orders Form' class="o_lunch">
|
||||
<header>
|
||||
<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>
|
||||
</group>
|
||||
|
||||
<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_id' 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">
|
||||
<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')]"/>
|
||||
<separator/>
|
||||
<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_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>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Order lines Tree" create="false" edit="false">
|
||||
<field name='date'/>
|
||||
<field name='supplier_id' invisible='1'/>
|
||||
<field name='product_id'/>
|
||||
<field name='note'/>
|
||||
<field name='state'/>
|
||||
<field name='user_id'/>
|
||||
<field name='price' sum="Total" string="Price"/>
|
||||
<button name="action_confirm" string="Confirm" type="object" icon="fa-phone text-success" attrs="{'invisible': [('state', '!=', 'ordered')]}" groups="lunch.group_lunch_manager"/>
|
||||
<button name="action_cancel" string="Cancel" type="object" icon="fa-times-circle text-danger" attrs="{'invisible': [('state', 'in', ['cancelled', 'confirmed'])]}" groups="lunch.group_lunch_manager"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<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>
|
||||
<field name="arch" type="xml">
|
||||
<kanban>
|
||||
<field name="product_id"/>
|
||||
<field name="note"/>
|
||||
<field name="state"/>
|
||||
<field name="user_id"/>
|
||||
<field name="date"/>
|
||||
<field name="price"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<div class="o_kanban_record_top">
|
||||
<div class="o_kanban_record_headings">
|
||||
<strong class="o_kanban_record_title"><field name="product_id"/></strong>
|
||||
</div>
|
||||
<field name="state" widget="kanban_label_selection" options="{'classes': {'new': 'default', 'confirmed': 'success', 'cancelled':'danger'}}"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="note"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<i class="fa fa-money" role="img" aria-label="Money" title="Money"/> <field name="price"/>
|
||||
</div>
|
||||
<div class="col-6 text-right">
|
||||
<i class="fa fa-clock-o" role="img" aria-label="Date" title="Date"/> <field name="date"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt4">
|
||||
<div class="col-6">
|
||||
<a class="btn btn-sm btn-success" role="button" name="order" string="Order" type="object" attrs="{'invisible': ['|',('state','=','confirmed'),('state','=','ordered')]}" groups="lunch.group_lunch_manager">
|
||||
<i class="fa fa-phone" role="img" aria-label="Order button" title="Order button"/>
|
||||
</a>
|
||||
<a class="btn btn-sm btn-info" role="button" name="confirm" string="Receive" type="object" attrs="{'invisible': [('state','!=','ordered')]}" groups="lunch.group_lunch_manager">
|
||||
<i class="fa fa-check" role="img" aria-label="Receive button" title="Receive button"/>
|
||||
</a>
|
||||
<a class="btn btn-sm btn-danger" role="button" name="cancel" string="Cancel" type="object" attrs="{'invisible': [('state','=','cancelled')]}" groups="lunch.group_lunch_manager">
|
||||
<i class="fa fa-times" role="img" aria-label="Cancel button" title="Cancel button"/>
|
||||
</a>
|
||||
</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_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>
|
||||
|
||||
<record id="lunch_order_line_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="context">{"search_default_is_mine":1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
No previous order found
|
||||
</p><p>
|
||||
There is no previous order recorded. Click on "My Lunch" and then create a new lunch order.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="lunch_order_line_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="view_mode">tree,kanban</field>
|
||||
<field name="search_view_id" ref="lunch_order_line_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">
|
||||
Nothing to order today
|
||||
</p><p>
|
||||
Here you can see today's orders grouped by vendors.
|
||||
</p><p>
|
||||
Click on the <span class="fa fa-phone text-success" role="img" aria-label="Order button" title="Order button"/> to announce that the order is ordered.<br/>
|
||||
Click on the <span class="fa fa-check text-success" role="img" aria-label="Receive button" title="Receive button"/> to announce that the order is received.<br/>
|
||||
Click on the <span class="fa fa-times-circle text-danger" role="img" aria-label="Cancel button" title="Cancel button"/> to announce that the order isn't available.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="lunch_order_line_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="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
No lunch order yet
|
||||
</p><p>
|
||||
Summary of all lunch orders, grouped by vendor and by date.
|
||||
</p><p>
|
||||
Click on the <span class="fa fa-phone text-success" role="img" aria-label="Order button" title="Order button"/> to announce that the order is ordered.<br/>
|
||||
Click on the <span class="fa fa-check text-success" role="img" aria-label="Receive button" title="Receive button"/> to announce that the order is received.<br/>
|
||||
Click on the <span class="fa fa-times-circle text-danger" role="img" aria-label="Cancel button" title="Cancel button"/> red X to announce that the order isn't available.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="lunch_product_view_search" model="ir.ui.view">
|
||||
<field name="name">lunch.product.search</field>
|
||||
<field name="model">lunch.product</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Product Search">
|
||||
<field name="name"/>
|
||||
<field name="category_id"/>
|
||||
<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_id'}"/>
|
||||
<filter name="category" string="Product Category" context="{'group_by': 'category_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="lunch_product_view_tree" model="ir.ui.view">
|
||||
<field name="name">lunch.product.tree</field>
|
||||
<field name="model">lunch.product</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Products Tree">
|
||||
<field name="name"/>
|
||||
<field name="category_id"/>
|
||||
<field name="supplier_id"/>
|
||||
<field name="description"/>
|
||||
<field name="price"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="lunch_product_view_form" model="ir.ui.view">
|
||||
<field name="name">lunch.product.form</field>
|
||||
<field name="model">lunch.product</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Products 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>
|
||||
<field name="image" widget="image" class="oe_avatar" options="{'preview_image': 'image_medium'}"/>
|
||||
<label for="name"/>
|
||||
<field name='name'/>
|
||||
<div>
|
||||
<field name="is_topping"/>
|
||||
<label for="is_topping"/>
|
||||
</div>
|
||||
<group>
|
||||
<field name='category_id' attrs="{'invisible': [('is_topping', '=', True)], 'required': [('is_topping', '=', False)]}"/>
|
||||
<field name='supplier_id'/>
|
||||
<field name='price'/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Description" name="description">
|
||||
<label for='description'/>
|
||||
<field name='description'/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</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="arch" type="xml">
|
||||
<kanban js_class="lunch_kanban" create="0" class="o_kanban_mobile">
|
||||
<field name="id"/>
|
||||
<field name="name"/>
|
||||
<field name="category_id"/>
|
||||
<field name="supplier_id"/>
|
||||
<field name="description"/>
|
||||
<field name="price"/>
|
||||
<field name="image_small"/>
|
||||
<field name="already_ordered"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click o_kanban_record_has_image_fill">
|
||||
<div class="o_kanban_image_fill_left d-none d-md-block flex-grow-0"
|
||||
t-attf-style="background-image:url('#{kanban_image('lunch.product', 'image_medium', record.id.raw_value)}')"/>
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<div class="o_kanban_record_top d-flex flex-row">
|
||||
<div class="o_kanban_record_headings flex-grow-1">
|
||||
<i t-if="record.already_ordered.raw_value" class="fa fa-heart mr-1 o_pink"/><strong class="o_kanban_record_title"><t t-esc="record.name.value"/></strong>
|
||||
</div>
|
||||
<span class="badge badge-pill">
|
||||
Price: <t t-esc="record.price.value"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom d-flex flex-column">
|
||||
<div>
|
||||
<t t-esc="record.supplier_id.value"/>
|
||||
</div>
|
||||
<div>
|
||||
<t t-esc="record.description.value"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="lunch_product_category_view_tree" model="ir.ui.view">
|
||||
<field name="name">Product category Tree</field>
|
||||
<field name="model">lunch.product.category</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Products List" editable="bottom">
|
||||
<field name='name' string="Product Category: "/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="lunch_product_category_view_form" model="ir.ui.view">
|
||||
<field name="name">Product category Form</field>
|
||||
<field name="model">lunch.product.category</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Products Form">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name='name' string="Product Category: "/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="lunch_product_action" model="ir.actions.act_window">
|
||||
<field name="name">Products</field>
|
||||
<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">
|
||||
Create a new product for lunch
|
||||
</p><p>
|
||||
A product is defined by its name, category, price and vendor.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="lunch_product_action_order" model="ir.actions.act_window">
|
||||
<field name="name">Order</field>
|
||||
<field name="res_model">lunch.product</field>
|
||||
<field name="view_mode">kanban</field>
|
||||
<field name="domain">[('is_topping', '=', False), ('supplier_id.available_today', '=', True)]</field>
|
||||
</record>
|
||||
|
||||
<record id="lunch_product_category_action" model="ir.actions.act_window">
|
||||
<field name="name">Product Categories</field>
|
||||
<field name="res_model">lunch.product.category</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new lunch category
|
||||
</p><p>
|
||||
Here you can access all categories for the lunch products.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,113 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="lunch_supplier_view_tree" model="ir.ui.view">
|
||||
<field name="name">lunch.supplier.view.tree</field>
|
||||
<field name="model">lunch.supplier</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="phone"/>
|
||||
<field name="email"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="lunch_supplier_view_form" model="ir.ui.view">
|
||||
<field name="name">lunch.supplier.view.form</field>
|
||||
<field name="model">lunch.supplier</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="image" widget="image" class="oe_avatar" options="{'preview_image': 'image_medium'}"/>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1><field name="name" required="1"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<field name="partner_id"/>
|
||||
<label for="street" string="Address"/>
|
||||
<div class="o_address_format">
|
||||
<field name="street" placeholder="Street..." class="o_address_street"/>
|
||||
<field name="street2" placeholder="Street 2..." class="o_address_street"/>
|
||||
<field name="city" placeholder="City" class="o_address_city"/>
|
||||
<field name="state_id" class="o_address_state" placeholder="State" options="{'no_open': True}" context="{'country_id': country_id, 'zip': zip_code}"/>
|
||||
<field name="zip_code" placeholder="ZIP" class="o_address_zip"/>
|
||||
<field name="country_id" placeholder="Country" class="o_address_country" options="{'no_open': True, 'no_create': True}"/>
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
<field name="vat"/>
|
||||
<field name="email"/>
|
||||
<field name="phone"/>
|
||||
</group>
|
||||
<notebook colspan="4">
|
||||
<page string="Availability">
|
||||
<group>
|
||||
<field name="tz"/>
|
||||
<field name="recurrency"/>
|
||||
<field name="recurrency_date" attrs="{'invisible': [('recurrency', '!=', 'once')]}"/>
|
||||
<field name="recurrency_monday" attrs="{'invisible': [('recurrency', '!=', 'reccurent')]}"/>
|
||||
<field name="recurrency_tuesday" attrs="{'invisible': [('recurrency', '!=', 'reccurent')]}"/>
|
||||
<field name="recurrency_wednesday" attrs="{'invisible': [('recurrency', '!=', 'reccurent')]}"/>
|
||||
<field name="recurrency_thursday" attrs="{'invisible': [('recurrency', '!=', 'reccurent')]}"/>
|
||||
<field name="recurrency_friday" attrs="{'invisible': [('recurrency', '!=', 'reccurent')]}"/>
|
||||
<field name="recurrency_saturday" attrs="{'invisible': [('recurrency', '!=', 'reccurent')]}"/>
|
||||
<field name="recurrency_sunday" attrs="{'invisible': [('recurrency', '!=', 'reccurent')]}"/>
|
||||
<field name="recurrency_from" widget="float_time"/>
|
||||
<field name="recurrency_to" widget="float_time"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Description">
|
||||
<label for="description" string="Type your description here" class="oe_edit_only"/>
|
||||
<field name="description"/>
|
||||
</page>
|
||||
<page string="Order">
|
||||
<label for="send_by"/>
|
||||
<field name="send_by" widget="radio"/>
|
||||
<div>
|
||||
<label for="automatic_email_send" string="Send Orders automatically at" attrs="{'invisible': [('send_by', '!=', 'mail')]}"/>
|
||||
<field name="automatic_email_time" widget="float_time" attrs="{'invisible': [('send_by', '!=', 'mail')]}"/>
|
||||
</div>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="lunch_supplier_view_kanban" model="ir.ui.view">
|
||||
<field name="name">lunch.supplier.view.kanban</field>
|
||||
<field name="model">lunch.supplier</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban>
|
||||
<field name="partner_id"/>
|
||||
<field name="image_small"/>
|
||||
<field name="city"/>
|
||||
<field name="country_id"/>
|
||||
<field name="email"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click o_kanban_record_has_image_fill o_res_partner_kanban">
|
||||
<div class="o_kanban_image_fill_left d-none d-md-block" t-attf-style="background-image:url('#{kanban_image('res.partner', 'image_medium', record.partner_id.raw_value)}')"/>
|
||||
<div class="oe_kanban_details">
|
||||
<strong class="o_kanban_record_title oe_partner_heading"><field name="display_name"/></strong>
|
||||
<ul>
|
||||
<li t-if="record.city.raw_value and !record.country_id.raw_value"><field name="city"/></li>
|
||||
<li t-if="!record.city.raw_value and record.country_id.raw_value"><field name="country_id"/></li>
|
||||
<li t-if="record.city.raw_value and record.country_id.raw_value"><field name="city"/>, <field name="country_id"/></li>
|
||||
<li t-if="record.email.raw_value" class="o_text_overflow"><field name="email"/></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="lunch_vendors_action" model="ir.actions.act_window">
|
||||
<field name="name">Vendors</field>
|
||||
<field name="res_model">lunch.supplier</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -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>
|
||||
|
||||
@@ -6,549 +6,19 @@
|
||||
<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"/>
|
||||
|
||||
<!--View Search to group/filter by Supplier and time-->
|
||||
<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">
|
||||
<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')]"/>
|
||||
<separator/>
|
||||
<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_date" string="Order Date" context="{'group_by':'date'}" help="Vendor Orders by Date"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!--View Search to group by employee and input/output (cashmoves)-->
|
||||
<record id="lunch_cashmove_view_search" 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 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_alert_view_search" model="ir.ui.view">
|
||||
<field name="name">lunch.alert.search</field>
|
||||
<field name="model">lunch.alert</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search">
|
||||
<field name="message"/>
|
||||
<filter name="inactive" string="Archived" domain="[('active','=',False)]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!--view for cashmove-->
|
||||
<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="date"/>
|
||||
<field name="user_id"/>
|
||||
<field name="description"/>
|
||||
<field name="amount" sum="Total"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_lunch_cashmove_kanban" model="ir.ui.view">
|
||||
<field name="name">lunch.cashmove.kanban</field>
|
||||
<field name="model">lunch.cashmove</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="date"/>
|
||||
<field name="user_id"/>
|
||||
<field name="description"/>
|
||||
<field name="amount"/>
|
||||
<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"/></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_view_form" model="ir.ui.view">
|
||||
<field name="name">lunch.cashmove.form</field>
|
||||
<field name="model">lunch.cashmove</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="cashmove form">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="user_id" required="1"/>
|
||||
<field name="date"/>
|
||||
<field name="amount"/>
|
||||
</group>
|
||||
<label for='description'/>
|
||||
<field name="description"/>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<!--Action for Your Orders-->
|
||||
<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>
|
||||
<menuitem name="New Order" parent="menu_lunch_title" id="lunch_order_menu_form" action="lunch_order_action_form" sequence="1"/>
|
||||
|
||||
<record id="lunch_order_line_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="context">{"search_default_is_mine":1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
No previous order found
|
||||
</p><p>
|
||||
There is no previous order recorded. Click on "My Lunch" and then create a new lunch order.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<menuitem name="New Order" parent="menu_lunch_title" id="lunch_order_menu_form" action="lunch.lunch_product_action_order" sequence="1"/>
|
||||
<menuitem name="Previous Orders" parent="menu_lunch_title" id="lunch_order_menu_tree" action="lunch_order_line_action" sequence="2"/>
|
||||
|
||||
<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"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!--Action for Lunch cashmoves-->
|
||||
<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>
|
||||
|
||||
<menuitem name="Your Lunch Account" parent="menu_lunch_title" id="lunch_cashmove_menu_form" action="lunch_cashmove_action_account" sequence="3"/>
|
||||
|
||||
<!--Action for Administrate Orders group by supplier-->
|
||||
<record id="lunch_order_line_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="view_mode">tree,kanban</field>
|
||||
<field name="search_view_id" ref="lunch_order_line_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">
|
||||
Nothing to order today
|
||||
</p><p>
|
||||
Here you can see today's orders grouped by vendors.
|
||||
</p><p>
|
||||
Click on the <span class="fa fa-phone text-success" role="img" aria-label="Order button" title="Order button"/> to announce that the order is ordered.<br/>
|
||||
Click on the <span class="fa fa-check text-success" role="img" aria-label="Receive button" title="Receive button"/> to announce that the order is received.<br/>
|
||||
Click on the <span class="fa fa-times-circle text-danger" role="img" aria-label="Cancel button" title="Cancel button"/> to announce that the order isn't available.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<menuitem name="Today's Orders" parent="menu_lunch_admin" id="lunch_order_line_menu_by_supplier" action="lunch_order_line_action_by_supplier" />
|
||||
|
||||
<!--Action for control Vendor -->
|
||||
<record id="lunch_order_line_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="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
No lunch order yet
|
||||
</p><p>
|
||||
Summary of all lunch orders, grouped by vendor and by date.
|
||||
</p><p>
|
||||
Click on the <span class="fa fa-phone text-success" role="img" aria-label="Order button" title="Order button"/> to announce that the order is ordered.<br/>
|
||||
Click on the <span class="fa fa-check text-success" role="img" aria-label="Receive button" title="Receive button"/> to announce that the order is received.<br/>
|
||||
Click on the <span class="fa fa-times-circle text-danger" role="img" aria-label="Cancel button" title="Cancel button"/> red X to announce that the order isn't available.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<menuitem name="Orders by Vendor" parent="menu_lunch_admin" id="lunch_order_line_menu_control_suppliers" action="lunch_order_line_action_control_suppliers" />
|
||||
|
||||
<!--Action for Control Accounts-->
|
||||
<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>
|
||||
<menuitem name="Control Accounts" parent="menu_lunch_admin" id="lunch_cashmove_menu_control_accounts" action="lunch_cashmove_action_control_accounts"/>
|
||||
|
||||
<!--Action for Payment cashmove-->
|
||||
<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">
|
||||
Register a payment
|
||||
</p><p>
|
||||
Payments are used to register liquidity movements. You can process those payments by your own means or by using installed facilities.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<menuitem name="Employee Payments" parent="menu_lunch_admin" id="lunch_cashmove_menu_payment" action="lunch_cashmove_action_payment"/>
|
||||
|
||||
<!--Action for Products-->
|
||||
<record id="lunch_product_action" model="ir.actions.act_window">
|
||||
<field name="name">Products</field>
|
||||
<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">
|
||||
Create a new product for lunch
|
||||
</p><p>
|
||||
A product is defined by its name, category, price and vendor.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<menuitem name="Products" parent="menu_lunch_config" id="lunch_product_menu" action="lunch_product_action"/>
|
||||
<menuitem name="Vendors" parent="menu_lunch_config" id="lunch_vendors_menu" action="lunch_vendors_action" sequence="1"/>
|
||||
<menuitem name="Products" parent="menu_lunch_config" id="lunch_product_menu" action="lunch_product_action" sequence="2"/>
|
||||
<menuitem name="Product Categories" parent="menu_lunch_config" id="lunch_product_category_menu" action="lunch_product_category_action" sequence="3"/>
|
||||
|
||||
<!--Action for Product categories-->
|
||||
<record id="lunch_product_category_action" model="ir.actions.act_window">
|
||||
<field name="name">Product Categories</field>
|
||||
<field name="res_model">lunch.product.category</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new lunch category
|
||||
</p><p>
|
||||
Here you can access all categories for the lunch products.
|
||||
</p>
|
||||
</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">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name='name' string="Product Category: "/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem name="Product Categories" parent="menu_lunch_config" id="lunch_product_category_menu" action="lunch_product_category_action" />
|
||||
|
||||
<!--Action for Alert-->
|
||||
<record id="lunch_alert_action" model="ir.actions.act_window">
|
||||
<field name="name">Alerts</field>
|
||||
<field name="res_model">lunch.alert</field>
|
||||
<field name="view_mode">tree,kanban,form</field>
|
||||
<field name="search_view_id" ref="lunch_alert_view_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new lunch alert
|
||||
</p><p>
|
||||
Alerts are used to warn employee from possible issues concerning the lunch orders.
|
||||
To create a lunch alert you have to define its recurrence, the time interval during which the alert should be executed and the message to display.
|
||||
</p><p>
|
||||
Example:<br/>
|
||||
Recurency: Everyday,<br/>
|
||||
Time interval: from 00h00 am to 11h59 pm,<br/>
|
||||
Message: "You must order before 10h30 am".
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<menuitem name="Alerts" parent="menu_lunch_config" id="lunch_alert_menu" action="lunch_alert_action" />
|
||||
|
||||
<!--View for Order lines-->
|
||||
<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>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Order lines Tree" create="false" edit="false">
|
||||
<field name='date'/>
|
||||
<field name='supplier' invisible='1'/>
|
||||
<field name='product_id'/>
|
||||
<field name='note'/>
|
||||
<field name='state'/>
|
||||
<field name='user_id'/>
|
||||
<field name='price' sum="Total" string="Price"/>
|
||||
<button name="order" string="Order" type="object" icon="fa-phone text-success" attrs="{'invisible': ['|',('state','=','confirmed'),('state','=','ordered')]}" groups="lunch.group_lunch_manager"/>
|
||||
<button name="confirm" string="Receive" type="object" icon="fa-check text-success" attrs="{'invisible': [('state','!=','ordered')]}" groups="lunch.group_lunch_manager"/>
|
||||
<button name="cancel" string="Cancel" type="object" icon="fa-times-circle text-danger" attrs="{'invisible': [('state','=','cancelled')]}" groups="lunch.group_lunch_manager"/>
|
||||
</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>
|
||||
<field name="arch" type="xml">
|
||||
<kanban>
|
||||
<field name='product_id'/>
|
||||
<field name='note'/>
|
||||
<field name='state'/>
|
||||
<field name='user_id'/>
|
||||
<field name='date'/>
|
||||
<field name="price"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<div class="o_kanban_record_top">
|
||||
<div class="o_kanban_record_headings">
|
||||
<strong class="o_kanban_record_title"><field name="product_id"/></strong>
|
||||
</div>
|
||||
<field name="state" widget="kanban_label_selection" options="{'classes': {'new': 'default', 'confirmed': 'success', 'cancelled':'danger'}}"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="note"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<i class="fa fa-money" role="img" aria-label="Money" title="Money"/> <field name="price"/>
|
||||
</div>
|
||||
<div class="col-6 text-right">
|
||||
<i class="fa fa-clock-o" role="img" aria-label="Date" title="Date"/> <field name="date"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt4">
|
||||
<div class="col-6">
|
||||
<a class="btn btn-sm btn-success" role="button" name="order" string="Order" type="object" attrs="{'invisible': ['|',('state','=','confirmed'),('state','=','ordered')]}" groups="lunch.group_lunch_manager">
|
||||
<i class="fa fa-phone" role="img" aria-label="Order button" title="Order button"/>
|
||||
</a>
|
||||
<a class="btn btn-sm btn-info" role="button" name="confirm" string="Receive" type="object" attrs="{'invisible': [('state','!=','ordered')]}" groups="lunch.group_lunch_manager">
|
||||
<i class="fa fa-check" role="img" aria-label="Receive button" title="Receive button"/>
|
||||
</a>
|
||||
<a class="btn btn-sm btn-danger" role="button" name="cancel" string="Cancel" type="object" attrs="{'invisible': [('state','=','cancelled')]}" groups="lunch.group_lunch_manager">
|
||||
<i class="fa fa-times" role="img" aria-label="Cancel button" title="Cancel button"/>
|
||||
</a>
|
||||
</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>
|
||||
|
||||
<!--View for Your orders-->
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<!--View for Products-->
|
||||
<record id="lunch_product_view_tree" model="ir.ui.view">
|
||||
<field name="name">lunch.product.tree</field>
|
||||
<field name="model">lunch.product</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Products Tree">
|
||||
<field name="name"/>
|
||||
<field name="category_id"/>
|
||||
<field name="supplier"/>
|
||||
<field name="description"/>
|
||||
<field name="price"/>
|
||||
</tree>
|
||||
</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="arch" type="xml">
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="name"/>
|
||||
<field name="category_id"/>
|
||||
<field name="supplier"/>
|
||||
<field name="description"/>
|
||||
<field name="price"/>
|
||||
<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>
|
||||
<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>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_server_lunch_archive_product" model="ir.actions.server">
|
||||
<field name="name">Lunch: Archive/Restore products</field>
|
||||
@@ -558,157 +28,4 @@
|
||||
<field name="state">code</field>
|
||||
<field name="code">records.toggle_active()</field>
|
||||
</record>
|
||||
|
||||
<!-- Search for product -->
|
||||
<record id="lunch_product_view_search" model="ir.ui.view">
|
||||
<field name="name">lunch.product.search</field>
|
||||
<field name="model">lunch.product</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Product Search">
|
||||
<field name="name"/>
|
||||
<field name="category_id"/>
|
||||
<field name="supplier"/>
|
||||
<field name="description"/>
|
||||
<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)]"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="lunch_product_view_form" model="ir.ui.view">
|
||||
<field name="name">lunch.product.form</field>
|
||||
<field name="model">lunch.product</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Products Form">
|
||||
<header>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="toggle_active" type="object"
|
||||
class="oe_stat_button" icon="fa-archive">
|
||||
<field name="active" widget="boolean_button"
|
||||
options='{"terminology": "archive"}'/>
|
||||
</button>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name='name'/>
|
||||
<field name='category_id'/>
|
||||
</group>
|
||||
<group>
|
||||
<field name='supplier'/>
|
||||
<field name='price'/>
|
||||
</group>
|
||||
</group>
|
||||
<label for='description'/>
|
||||
<field name='description'/>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!--view for alerts-->
|
||||
<record id="lunch_alert_view_tree" model="ir.ui.view">
|
||||
<field name="name">lunch.alert.tree</field>
|
||||
<field name="model">lunch.alert</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="message"/>
|
||||
<field name="alert_type"/>
|
||||
<field name='start_hour' widget='float_time'/>
|
||||
<field name='end_hour' widget='float_time'/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="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_view_form" model="ir.ui.view">
|
||||
<field name="name">lunch.alert.form</field>
|
||||
<field name="model">lunch.alert</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="toggle_active" type="object"
|
||||
class="oe_stat_button" icon="fa-archive">
|
||||
<field name="active" widget="boolean_button"
|
||||
options='{"terminology": "archive"}'/>
|
||||
</button>
|
||||
</div>
|
||||
<group>
|
||||
<group name="schedule" string="Schedule">
|
||||
<group colspan="4">
|
||||
<field name="alert_type"/>
|
||||
<field name="specific_day"
|
||||
attrs="{'invisible': [('alert_type','!=','specific')], 'required':[('alert_type','=','specific')]}"/>
|
||||
<field name='start_hour' widget='float_time' />
|
||||
<field name='end_hour' widget='float_time' />
|
||||
</group>
|
||||
<group name="weekdays" col="4" attrs="{'invisible': [('alert_type','!=','week')]}">
|
||||
<field name="monday"/>
|
||||
<field name="tuesday"/>
|
||||
<field name="wednesday"/>
|
||||
<field name="thursday"/>
|
||||
<field name="friday"/>
|
||||
<field name="saturday"/>
|
||||
<field name="sunday"/>
|
||||
</group>
|
||||
</group>
|
||||
<group name="vendor" string="Vendor">
|
||||
<group name="partner" colspan="4">
|
||||
<field name="partner_id"/>
|
||||
<field name="display" invisible="True"/>
|
||||
</group>
|
||||
<div class="oe_grey" colspan="4"
|
||||
attrs="{'invisible': ['|', ('partner_id', '=', False), ('display', '=', True)]}">
|
||||
Products from this vendor can <strong>not</strong> be ordered at that moment.
|
||||
</div>
|
||||
<div class="oe_grey" colspan="4"
|
||||
attrs="{'invisible': ['|', ('partner_id', '=', False), ('display', '=', False)]}">
|
||||
Products from this vendor can be ordered at that moment.
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<group string='Message'>
|
||||
<field name='message' nolabel='1' placeholder="Write the message you want to display during the defined period..."/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.'))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,77 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class LunchOrderLineWizard(models.TransientModel):
|
||||
_name = 'lunch.order.line.temp'
|
||||
_description = 'Lunch Order Line Temp'
|
||||
|
||||
product_id = fields.Many2one('lunch.product', string='Product ID')
|
||||
product_description = fields.Text('Description', related='product_id.description')
|
||||
product_name = fields.Char('Product', related='product_id.name')
|
||||
product_supplier = fields.Many2one('lunch.supplier', related='product_id.supplier_id')
|
||||
topping_ids = fields.Many2many('lunch.product', string="Extra Garniture", domain="[('is_topping', '=', True), ('supplier_id', '=', product_supplier)]")
|
||||
|
||||
quantity = fields.Float('Quantity', default=1)
|
||||
price_total = fields.Float('Total Price', compute='_compute_price_total')
|
||||
note = fields.Text('Special Instructions')
|
||||
|
||||
user_id = fields.Many2one('res.users', default=lambda self: self.env.user.id)
|
||||
|
||||
@api.depends('product_id', 'topping_ids', 'quantity')
|
||||
def _compute_price_total(self):
|
||||
for wizard in self:
|
||||
wizard.price_total = wizard.quantity * sum(record.price for record in (wizard.product_id | wizard.topping_ids))
|
||||
|
||||
def find_current_order(self):
|
||||
order_id = self.env['lunch.order'].search([('state', '!=', 'cancel'),
|
||||
('user_id', '=', self.user_id.id),
|
||||
('date', '=', fields.Date.today())], limit=1)
|
||||
if not order_id:
|
||||
order_id = self.env['lunch.order'].create({})
|
||||
return order_id
|
||||
|
||||
def add_to_cart(self):
|
||||
self.ensure_one()
|
||||
|
||||
order_id = self.find_current_order()
|
||||
|
||||
# Do not add a line to confirmed orders as they are uneditable
|
||||
if order_id.state == 'confirmed':
|
||||
return
|
||||
|
||||
lines = order_id.order_line_ids.filtered(lambda line: line.product_id == self.product_id and line.topping_ids == self.topping_ids)
|
||||
|
||||
if lines:
|
||||
line = lines[0]
|
||||
line.quantity += 1
|
||||
else:
|
||||
line = self.env['lunch.order.line'].create({
|
||||
'order_id': order_id.id,
|
||||
'product_id': self.product_id.id,
|
||||
'topping_ids': [(6, 0, self.topping_ids.ids)],
|
||||
'quantity': self.quantity,
|
||||
})
|
||||
wallet_balance = self.env['lunch.cashmove'].get_wallet_balance(self.user_id)
|
||||
if wallet_balance < line.price:
|
||||
raise UserError(_('Your wallet does not contain enough money to order that.'
|
||||
'To add some money to your wallet, please contact your lunch manager.'))
|
||||
|
||||
if order_id.state == 'ordered':
|
||||
# Need to update cashmove
|
||||
cashmove = self.env['lunch.cashmove'].search([('order_line_id', '=', line.id)], limit=1)
|
||||
if cashmove:
|
||||
cashmove.amount = -line.price
|
||||
else:
|
||||
self.env['lunch.cashmove'].create({
|
||||
'order_line_id': line.id,
|
||||
'user_id': line.user_id.id,
|
||||
'description': line.product_id.name,
|
||||
'state': 'order',
|
||||
'date': line.date,
|
||||
'amount': -line.price,
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="view_lunch_order_line_temp" model="ir.ui.view">
|
||||
<field name="name">view.lunch.order.line.temp</field>
|
||||
<field name="model">lunch.order.line.temp</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="product_id" invisible="1"/>
|
||||
<field name="product_supplier" invisible="1"/>
|
||||
<field name="product_name"/>
|
||||
<field name="product_description"/>
|
||||
<field name="topping_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
|
||||
<group>
|
||||
<field name="note"/>
|
||||
</group>
|
||||
|
||||
<group>
|
||||
<field name="quantity"/>
|
||||
<field name="price_total" widget="monetary"/>
|
||||
</group>
|
||||
|
||||
<footer>
|
||||
<button name="add_to_cart" string="Add To Cart" type="object" class="oe_highlight"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
{
|
||||
'name': 'Lunch HR',
|
||||
'sequence': 120,
|
||||
'version': '1.0',
|
||||
'depends': ['lunch', 'hr'],
|
||||
'category': 'Human Resources',
|
||||
'summary': 'Handle lunch orders of your employees',
|
||||
'description': """""",
|
||||
'data': [
|
||||
'views/lunch_product_views.xml',
|
||||
'views/lunch_supplier_views.xml',
|
||||
],
|
||||
'demo': ['data/lunch_hr_demo.xml',],
|
||||
'auto_install': True,
|
||||
'installable': True,
|
||||
'application': False,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="partner_farm_1" model="res.partner">
|
||||
<field name="name">Farm 1</field>
|
||||
</record>
|
||||
|
||||
<record id="partner_farm_2" model="res.partner">
|
||||
<field name="name">Farm 2</field>
|
||||
</record>
|
||||
|
||||
<record id="partner_brussels" model="res.partner">
|
||||
<field name="name">Brussels</field>
|
||||
</record>
|
||||
|
||||
<record id="hr.employee_admin" model="hr.employee">
|
||||
<field name="address_id" ref="partner_farm_1"/>
|
||||
</record>
|
||||
|
||||
<record id="lunch.supplier_pizza_inn" model="lunch.supplier">
|
||||
<field name="available_location_ids" eval="[
|
||||
(6, 0, [ref('lunch_hr.partner_farm_1'), ref('lunch_hr.partner_farm_2')]),
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import lunch_product
|
||||
from . import lunch_supplier
|
||||
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import pycompat
|
||||
|
||||
|
||||
class LunchProduct(models.Model):
|
||||
_inherit = 'lunch.product'
|
||||
|
||||
# This field is used only for searching
|
||||
is_available_at = fields.Many2one('res.partner', 'Product Availability', compute='_compute_is_available_at', search='_search_is_available_at')
|
||||
|
||||
def _compute_is_available_at(self):
|
||||
for product in self:
|
||||
product.is_available_at = False
|
||||
|
||||
def _search_is_available_at(self, operator, value):
|
||||
if operator in ['=', '!=', 'ilike', 'not ilike'] and isinstance(value, pycompat.string_types):
|
||||
location_ids = self.env['res.partner'].search([('name', operator, value)]).ids
|
||||
|
||||
if operator in ['='] and not value:
|
||||
if self.env.user.employee_ids and self.env.user.employee_ids[0].address_id:
|
||||
location_ids = self.env.user.employee_ids[0].address_id.ids
|
||||
|
||||
if location_ids:
|
||||
suppliers = self.env['lunch.supplier'].search([('available_location_ids', 'in', location_ids)])
|
||||
product_ids = self.env['lunch.product'].search([('supplier', 'in', suppliers.ids)])
|
||||
return [('id', 'in', product_ids.ids)]
|
||||
|
||||
return expression.TRUE_DOMAIN
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class LunchSupplier(models.Model):
|
||||
_inherit = 'lunch.supplier'
|
||||
|
||||
available_location_ids = fields.Many2many('res.partner', string='This Supplier is available at')
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="lunch_product_view_search_inherit" model="ir.ui.view">
|
||||
<field name="name">lunch.product.search.inherit</field>
|
||||
<field name="model">lunch.product</field>
|
||||
<field name="inherit_id" ref="lunch.lunch_product_view_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//filter[@name='inactive']" position="after">
|
||||
<filter name="available" string="Available at current location" domain="[('is_available_at', '=', False)]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="lunch_supplier_view_form_inherit" model="ir.ui.view">
|
||||
<field name="name">lunch.supplier.form.view.inherit</field>
|
||||
<field name="model">lunch.supplier</field>
|
||||
<field name="inherit_id" ref="lunch.lunch_supplier_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='recurrency_to']" position="after">
|
||||
<field name="available_location_ids" widget="many2many_tags"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||