squashed commit

This commit is contained in:
hoangvv
2026-09-18 13:55:25 +07:00
commit 039c98d4d0
45882 changed files with 24235585 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
from . import report
+57
View File
@@ -0,0 +1,57 @@
{
'name': 'Lunch',
'sequence': 300,
'version': '1.0',
'depends': ['mail'],
'category': 'Human Resources/Lunch',
'summary': 'Handle lunch orders of your employees',
'description': """
The base module to manage lunch.
================================
Many companies order sandwiches, pizzas and other, from usual vendors, for their employees to offer them more facilities.
However lunches management within the company requires proper administration especially when the number of employees or vendors is important.
The “Lunch Order” module has been developed to make this management easier but also to offer employees more tools and usability.
In addition to a full meal and vendor management, this module offers the possibility to display warning and provides quick order selection based on employees preferences.
If you want to save your employees' time and avoid them to always have coins in their pockets, this module is essential.
""",
'data': [
'security/lunch_security.xml',
'security/ir.model.access.csv',
'report/lunch_cashmove_report_views.xml',
'views/lunch_templates.xml',
'views/lunch_alert_views.xml',
'views/lunch_cashmove_views.xml',
'views/lunch_location_views.xml',
'views/lunch_orders_views.xml',
'views/lunch_product_views.xml',
'views/lunch_supplier_views.xml',
'views/res_config_settings.xml',
'views/lunch_views.xml',
'data/mail_template_data.xml',
'data/lunch_data.xml',
],
'demo': ['data/lunch_demo.xml'],
'installable': True,
'application': True,
'assets': {
'web.assets_backend': [
'lunch/static/src/components/*',
'lunch/static/src/mixins/*.js',
'lunch/static/src/views/*',
'lunch/static/src/scss/lunch_view.scss',
'lunch/static/src/scss/lunch_kanban.scss',
],
'web.assets_tests': [
'lunch/static/tests/tours/*.js',
],
'web.assets_unit_tests': [
'lunch/static/tests/**/*.test.js',
],
},
'license': 'LGPL-3',
}
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
+163
View File
@@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, http, fields
from odoo.exceptions import AccessError
from odoo.http import request
from odoo.osv import expression
from odoo.tools import float_round, float_repr
class LunchController(http.Controller):
@http.route('/lunch/infos', type='json', auth='user')
def infos(self, user_id=None, context=None):
if context:
request.update_context(**context)
self._check_user_impersonification(user_id)
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
infos = self._make_infos(user, order=False)
lines = self._get_current_lines(user)
if lines:
translated_states = dict(request.env['lunch.order']._fields['state']._description_selection(request.env))
lines = [{
'id': line.id,
'product': (line.product_id.id, line.product_id.name, float_repr(
float_round(line.product_id.price, 2) * line.quantity, 2),
float_round(line.product_id.price, 2)),
'toppings': [(topping.name, float_repr(float_round(topping.price, 2) * line.quantity, 2),
float_round(topping.price, 2))
for topping in line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3],
'quantity': line.quantity,
'price': line.price,
'raw_state': line.state,
'state': translated_states[line.state],
'date': line.date,
'location': line.lunch_location_id.name,
'note': line.note
} for line in lines.sorted('date')]
total = float_round(sum(line['price'] for line in lines), 2)
paid_subtotal = float_round(sum(line['price'] for line in lines if line['raw_state'] != 'new'), 2)
unpaid_subtotal = total - paid_subtotal
infos.update({
'total': float_repr(total, 2),
'paid_subtotal': float_repr(paid_subtotal, 2),
'unpaid_subtotal': float_repr(unpaid_subtotal, 2),
'raw_state': self._get_state(lines),
'lines': lines,
})
return infos
@http.route('/lunch/trash', type='json', auth='user')
def trash(self, user_id=None, context=None):
if context:
request.update_context(**context)
self._check_user_impersonification(user_id)
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
lines = self._get_current_lines(user)
lines = lines.filtered_domain([('state', 'not in', ['sent', 'confirmed'])])
lines.action_cancel()
lines.unlink()
@http.route('/lunch/pay', type='json', auth='user')
def pay(self, user_id=None, context=None):
if context:
request.update_context(**context)
self._check_user_impersonification(user_id)
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
lines = self._get_current_lines(user)
if lines:
lines = lines.filtered(lambda line: line.state == 'new')
lines.action_order()
return True
return False
@http.route('/lunch/payment_message', type='json', auth='user')
def payment_message(self):
return {'message': request.env['ir.qweb']._render('lunch.lunch_payment_dialog', {})}
@http.route('/lunch/user_location_set', type='json', auth='user')
def set_user_location(self, location_id=None, user_id=None, context=None):
if context:
request.update_context(**context)
self._check_user_impersonification(user_id)
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
user.sudo().last_lunch_location_id = request.env['lunch.location'].browse(location_id)
return True
@http.route('/lunch/user_location_get', type='json', auth='user')
def get_user_location(self, user_id=None, context=None):
if context:
request.update_context(**context)
self._check_user_impersonification(user_id)
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
company_ids = request.env.context.get('allowed_company_ids', request.env.company.ids)
user_location = user.last_lunch_location_id
has_multi_company_access = not user_location.company_id or user_location.company_id.id in company_ids
if not user_location or not has_multi_company_access:
return request.env['lunch.location'].search([('company_id', 'in', [False] + company_ids)], limit=1).id
return user_location.id
def _make_infos(self, user, **kwargs):
res = dict(kwargs)
is_manager = request.env.user.has_group('lunch.group_lunch_manager')
currency = user.company_id.currency_id
res.update({
'username': user.sudo().name,
'userimage': '/web/image?model=res.users&id=%s&field=avatar_128' % user.id,
'wallet': request.env['lunch.cashmove'].get_wallet_balance(user, False),
'wallet_with_config': request.env['lunch.cashmove'].get_wallet_balance(user),
'is_manager': is_manager,
'group_portal_id': request.env.ref('base.group_portal').id,
'locations': request.env['lunch.location'].search_read([], ['name']),
'currency': {'symbol': currency.symbol, 'position': currency.position},
})
user_location = user.last_lunch_location_id
has_multi_company_access = not user_location.company_id or user_location.company_id.id in request.env.context.get('allowed_company_ids', request.env.company.ids)
if not user_location or not has_multi_company_access:
user.last_lunch_location_id = user_location = request.env['lunch.location'].search([], limit=1) or user_location
alert_domain = expression.AND([
[('available_today', '=', True)],
[('location_ids', 'in', user_location.id)],
[('mode', '=', 'alert')],
])
res.update({
'user_location': (user_location.id, user_location.name),
'alerts': request.env['lunch.alert'].search_read(alert_domain, ['message']),
})
return res
def _check_user_impersonification(self, user_id=None):
if (user_id and request.env.uid != user_id and not request.env.user.has_group('lunch.group_lunch_manager')):
raise AccessError(_('You are trying to impersonate another user, but this can only be done by a lunch manager'))
def _get_current_lines(self, user):
return request.env['lunch.order'].search(
[('user_id', '=', user.id), ('date', '>=', fields.Date.context_today(user)), ('state', '!=', 'cancelled')]
)
def _get_state(self, lines):
"""
This method returns the lowest state of the list of lines
eg: [confirmed, confirmed, new] will return ('new', 'To Order')
"""
states_to_int = {'new': 0, 'ordered': 1, 'sent': 2, 'confirmed': 3, 'cancelled': 4}
int_to_states = ['new', 'ordered', 'sent', 'confirmed', 'cancelled']
return int_to_states[min(states_to_int[line['raw_state']] for line in lines)]
+67
View File
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="lunch_location_main" model="lunch.location" forcecreate="0">
<field name="name">HQ Office</field>
</record>
<record model="lunch.product.category" id="categ_sandwich" forcecreate="0">
<field name="name">Sandwich</field>
</record>
<record id="categ_pizza" model="lunch.product.category" forcecreate="0">
<field name="name">Pizza</field>
<field name="image_1920" type="base64" file="lunch/static/img/pizza.png"/>
</record>
<record id="categ_burger" model="lunch.product.category" forcecreate="0">
<field name="name">Burger</field>
<field name="image_1920" type="base64" file="lunch/static/img/burger.png"/>
</record>
<record id="categ_drinks" model="lunch.product.category" forcecreate="0">
<field name="name">Drinks</field>
<field name="image_1920" type="base64" file="lunch/static/img/drink.png"/>
</record>
<record id="partner_hungry_dog" model="res.partner" forcecreate="0">
<field name="name">Lunch Supplier</field>
</record>
<record id="supplier_hungry_dog" model="lunch.supplier" forcecreate="0">
<field name="partner_id" ref="partner_hungry_dog"/>
<field name="available_location_ids" eval="[
(6, 0, [ref('lunch_location_main')]),
]"/>
</record>
</data>
<data>
<record id="lunch_order_action_confirm" model="ir.actions.server">
<field name="name">Lunch: Receive meals</field>
<field name="model_id" ref="model_lunch_order"/>
<field name="binding_model_id" ref="model_lunch_order"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">records.action_confirm()</field>
</record>
<record id="lunch_order_action_cancel" model="ir.actions.server">
<field name="name">Lunch: Cancel meals</field>
<field name="model_id" ref="model_lunch_order"/>
<field name="binding_model_id" ref="model_lunch_order"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">records.action_cancel()</field>
</record>
<record id="lunch_order_action_notify" model="ir.actions.server">
<field name="name">Lunch: Send notifications</field>
<field name="model_id" ref="model_lunch_order"/>
<field name="binding_model_id" ref="model_lunch_order"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">records.action_notify()</field>
</record>
</data>
</odoo>
+486
View File
@@ -0,0 +1,486 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="lunch_location_main" model="lunch.location">
<field name="address">87323 Francis Corner Oscarhaven, OK 12782</field>
</record>
<record id="partner_hungry_dog" model="res.partner">
<field name="name">Hungry Dog</field>
<field name="city">Russeltown</field>
<field name="country_id" ref="base.us"/>
<field name="street">975 Bullock Orchard</field>
<field name="zip">02155</field>
<field name="email">hungry_dog@yourcompany.example.com</field>
<field name="company_id" ref="base.main_company"/>
</record>
<record id="product_bacon_0" model="lunch.product">
<field name="name">Bacon</field>
<field name="category_id" ref="categ_burger"/>
<field name="price">7.2</field>
<field name="supplier_id" ref="supplier_hungry_dog"/>
<field name="description">Beef, Bacon, Salad, Cheddar, Fried Onion, BBQ Sauce</field>
<field name="image_1920" type="base64" file="lunch/static/img/bacon_burger.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record id="product_cheese_burger_0" model="lunch.product">
<field name="name">Cheese Burger</field>
<field name="category_id" ref="categ_burger"/>
<field name="price">6.8</field>
<field name="supplier_id" ref="supplier_hungry_dog"/>
<field name="description">Beef, Cheddar, Salad, Fried Onions, BBQ Sauce</field>
<field name="image_1920" type="base64" file="lunch/static/img/cheeseburger.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record id="product_club_0" model="lunch.product">
<field name="name">Club</field>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">3.4</field>
<field name="supplier_id" ref="supplier_hungry_dog"/>
<field name="description">Ham, Cheese, Vegetables</field>
<field name="image_1920" type="base64" file="lunch/static/img/club.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record id="product_coke_0" model="lunch.product">
<field name="name">Coca Cola</field>
<field name="category_id" ref="categ_drinks"/>
<field name="price">2.9</field>
<field name="description"></field>
<field name="supplier_id" ref="supplier_hungry_dog"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record id="product_pizza_0" model="lunch.product">
<field name="name">Pizza Margherita</field>
<field name="category_id" ref="categ_pizza"/>
<field name="price">6.90</field>
<field name="supplier_id" ref="supplier_hungry_dog"/>
<field name="description">Tomatoes, Mozzarella</field>
<field name="company_id" ref="base.main_company"/>
</record>
<record id="location_office_1" model="lunch.location">
<field name="name">Office 1</field>
</record>
<record id="location_office_2" model="lunch.location">
<field name="name">Office 2</field>
</record>
<record id="location_office_3" model="lunch.location">
<field name="name">Office 3</field>
</record>
<record id="alert_office_3" model="lunch.alert">
<field name="name">Alert for Office 3</field>
<field name="message">Please order</field>
<field name="location_ids" eval="[(4, ref('location_office_3'))]" />
<field name="mode">chat</field>
</record>
<record id="base.user_admin" model="res.users">
<field name="last_lunch_location_id" ref="location_office_2"/>
</record>
<record id="base.user_demo" model="res.users">
<field name="last_lunch_location_id" ref="location_office_3"/>
<field name="groups_id" eval="[(3, ref('lunch.group_lunch_manager'))]"/>
</record>
<record model="lunch.product.category" id="categ_pasta">
<field name="name">Pasta</field>
</record>
<record model="lunch.product.category" id="categ_sushi">
<field name="name">Sushi</field>
</record>
<record model="lunch.product.category" id="categ_temaki">
<field name="name">Temaki</field>
</record>
<record model="lunch.product.category" id="categ_chirashi">
<field name="name">Chirashi</field>
</record>
<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"/>
<field name="street">Rr. e Durrësit, Pall. M.C. Inerte</field>
<field name="street2">Kati.1, Laprakë, Tirana, Shqipëri</field>
<field name="email">coin.gourmand@yourcompany.example.com</field>
<field name="phone">+32485562388</field>
<field name="company_type">company</field>
</record>
<record id="partner_pizza_inn" model="res.partner">
<field name="name">Pizza Inn</field>
<field name="city">Gandhi Nagar</field>
<field name="country_id" ref="base.in"/>
<field name="company_type">company</field>
<field name="image_1920" type="base64" file="lunch/static/img/pizza.jpeg"/>
<field name="street">#8, 1 st Floor,iscore complex</field>
<field name="street2">Gandhi Gramam</field>
<field name="zip">607308</field>
<field name="email">pizza.inn@yourcompany.example.com</field>
<field name="phone">+32456325289</field>
</record>
<record model="res.partner" id="partner_corner">
<field name="name">The Corner</field>
<field name="city">Atlanta</field>
<field name="country_id" ref="base.us"/>
<field name="street">Genessee Ave SW</field>
<field name="zip">607409</field>
<field name="email">info@corner.com</field>
<field name="phone">+32654321515</field>
<field name="company_type">company</field>
</record>
<record model="res.partner" id="partner_sushi_shop">
<field name="name">Sushi Shop</field>
<field name="city">Paris</field>
<field name="country_id" ref="base.fr"/>
<field name="street">Boulevard Saint-Germain</field>
<field name="zip">486624</field>
<field name="email">order@sushi.com</field>
<field name="phone">+32498859912</field>
<field name="company_type">company</field>
</record>
<record model="lunch.supplier" id="supplier_coin_gourmand">
<field name="partner_id" ref="partner_coin_gourmand"/>
<field name="available_location_ids" eval="[
(6, 0, [ref('location_office_1'), ref('location_office_2')]),
]"/>
</record>
<record model="lunch.supplier" id="supplier_pizza_inn">
<field name="partner_id" ref="partner_pizza_inn"/>
<field name="send_by">mail</field>
<field name="automatic_email_time">11</field>
<field name="available_location_ids" eval="[
(6, 0, [ref('location_office_1'), ref('location_office_2')]),
]"/>
</record>
<record model="lunch.supplier" id="supplier_corner">
<field name="partner_id" ref="partner_corner"/>
<field name="available_location_ids" eval="[
(6, 0, [ref('location_office_3')]),
]"/>
</record>
<record model="lunch.supplier" id="supplier_sushi_shop">
<field name="partner_id" ref="partner_sushi_shop"/>
<field name="available_location_ids" eval="[
(6, 0, [ref('location_office_1'), ref('location_office_2')]),
]"/>
</record>
<record model="lunch.product" id="product_bacon">
<field name="name">Bacon</field>
<field name="category_id" ref="categ_burger"/>
<field name="price">7.5</field>
<field name="supplier_id" ref="supplier_corner"/>
<field name="description">Beef, Bacon, Salad, Cheddar, Fried Onion, BBQ Sauce</field>
<field name="image_1920" type="base64" file="lunch/static/img/bacon_burger.png"/>
<field name="new_until" eval="datetime.today() + relativedelta(weeks=1)"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_cheeseburger">
<field name="name">Cheese Burger</field>
<field name="category_id" ref="categ_burger"/>
<field name="price">7.0</field>
<field name="supplier_id" ref="supplier_corner"/>
<field name="description">Beef, Cheddar, Salad, Fried Onions, BBQ Sauce</field>
<field name="image_1920" type="base64" file="lunch/static/img/cheeseburger.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_chicken_curry">
<field name="name">Chicken Curry</field>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">3.0</field>
<field name="supplier_id" ref="supplier_corner"/>
<field name="image_1920" type="base64" file="lunch/static/img/chicken_curry.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_spicy_tuna">
<field name="name">Spicy Tuna</field>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">3.0</field>
<field name="supplier_id" ref="supplier_corner"/>
<field name="description"></field>
<field name="image_1920" type="base64" file="lunch/static/img/chicken_curry.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_mozzarella">
<field name="name">Mozzarella</field>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">3.9</field>
<field name="supplier_id" ref="supplier_corner"/>
<field name="description">Mozzarella, Pesto, Tomatoes</field>
<field name="image_1920" type="base64" file="lunch/static/img/mozza.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_club">
<field name="name">Club</field>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">3.4</field>
<field name="supplier_id" ref="supplier_corner"/>
<field name="description">Ham, Cheese, Vegetables</field>
<field name="image_1920" type="base64" file="lunch/static/img/club.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_maki">
<field name="name">Lunch Maki 18pc</field>
<field name="category_id" ref="categ_sushi"/>
<field name="price">12.0</field>
<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_1920" type="base64" file="lunch/static/img/maki.png"/>
<field name="new_until" eval="datetime.today() + relativedelta(weeks=1)"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_salmon">
<field name="name">Lunch Salmon 20pc</field>
<field name="category_id" ref="categ_sushi"/>
<field name="price">13.80</field>
<field name="supplier_id" ref="supplier_sushi_shop"/>
<field name="description">4 Sushi Salmon - 6 Maki Salmon - 4 Sashimi Salmon </field>
<field name="image_1920" type="base64" file="lunch/static/img/salmon_sushi.png"/>
<field name="new_until" eval="datetime.today() + relativedelta(weeks=1)"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_temaki">
<field name="name">Lunch Temaki mix 3pc</field>
<field name="category_id" ref="categ_temaki"/>
<field name="price">14.0</field>
<field name="supplier_id" ref="supplier_sushi_shop"/>
<field name="description">1 Avocado - 1 Salmon - 1 Eggs - 1 Tuna</field>
<field name="image_1920" type="base64" file="lunch/static/img/temaki.png"/>
<field name="new_until" eval="datetime.today() + relativedelta(weeks=1)"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_chirashi">
<field name="name">Salmon and Avocado</field>
<field name="category_id" ref="categ_chirashi"/>
<field name="price">9.25</field>
<field name="supplier_id" ref="supplier_sushi_shop"/>
<field name="description">2 Tempuras, Cabbages, Onions, Sesame Sauce</field>
<field name="image_1920" type="base64" file="lunch/static/img/chirashi.png"/>
<field name="new_until" eval="datetime.today() + relativedelta(weeks=1)"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_cheese_ham">
<field name="name">Cheese And Ham</field>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">3.30</field>
<field name="supplier_id" ref="supplier_coin_gourmand"/>
<field name="description">Cheese, Ham, Salad, Tomatoes, cucumbers, eggs</field>
<field name="image_1920" type="base64" file="lunch/static/img/club.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_country">
<field name="name">The Country</field>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">3.30</field>
<field name="supplier_id" ref="supplier_coin_gourmand"/>
<field name="description">Brie, Honey, Walnut Kernels</field>
<field name="image_1920" type="base64" file="lunch/static/img/brie.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_tuna">
<field name="name">Tuna</field>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">2.50</field>
<field name="supplier_id" ref="supplier_coin_gourmand"/>
<field name="description">Tuna, Mayonnaise</field>
<field name="image_1920" type="base64" file="lunch/static/img/tuna_sandwich.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_gouda">
<field name="name">Gouda Cheese</field>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">2.50</field>
<field name="supplier_id" ref="supplier_coin_gourmand"/>
<field name="description"></field>
<field name="image_1920" type="base64" file="lunch/static/img/gouda.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_chicken_curry">
<field name="name">Chicken Curry</field>
<field name="category_id" ref="categ_sandwich"/>
<field name="price">2.60</field>
<field name="supplier_id" ref="supplier_coin_gourmand"/>
<field name="description"></field>
<field name="image_1920" type="base64" file="lunch/static/img/chicken_curry.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_margherita">
<field name="name">Pizza Margherita</field>
<field name="category_id" ref="categ_pizza"/>
<field name="price">6.90</field>
<field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="description">Tomatoes, Mozzarella</field>
<field name="image_1920" type="base64" file="lunch/static/img/pizza_margherita.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_funghi">
<field name="name">Pizza Funghi</field>
<field name="category_id" ref="categ_pizza"/>
<field name="price">7.00</field>
<field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="description">Tomatoes, Mushrooms, Mozzarella</field>
<field name="image_1920" type="base64" file="lunch/static/img/pizza_funghi.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_vege">
<field name="name">Pizza Vegetarian</field>
<field name="category_id" ref="categ_pizza"/>
<field name="price">7.00</field>
<field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="description">Tomatoes, Mozzarella, Mushrooms, Peppers, Olives</field>
<field name="image_1920" type="base64" file="lunch/static/img/pizza_veggie.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_italiana">
<field name="name">Pizza Italiana</field>
<field name="category_id" ref="categ_pizza"/>
<field name="price">7.40</field>
<field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="description">Fresh Tomatoes, Basil, Mozzarella</field>
<field name="image_1920" type="base64" file="lunch/static/img/italiana.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_Bolognese">
<field name="name">Bolognese Pasta</field>
<field name="category_id" ref="categ_pasta"/>
<field name="price">7.70</field>
<field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="description"></field>
<field name="image_1920" type="base64" file="lunch/static/img/pasta_bolognese.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.product" id="product_Napoli">
<field name="name">Napoli Pasta</field>
<field name="category_id" ref="categ_pasta"/>
<field name="price">7.70</field>
<field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="description">Tomatoes, Basil</field>
<field name="image_1920" type="base64" file="lunch/static/img/napoli.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.topping" id="product_olives">
<field name="name">Olives</field>
<field name="price">0.30</field>
<field name="supplier_id" ref="supplier_pizza_inn"/>
</record>
<record model="lunch.product" id="product_4formaggi">
<field name="name">4 Formaggi</field>
<field name="category_id" ref="categ_pasta"/>
<field name="price">5.50</field>
<field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="description">Tomato sauce, Olive oil, Fresh Tomatoes, Onions, Vegetables, Parmesan</field>
<field name="image_1920" type="base64" file="lunch/static/img/4formaggio.png"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record model="lunch.cashmove" id="cashmove_1">
<field name="user_id" ref="base.user_demo"/>
<field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
<field name="description">Payment: 5 lunch tickets (6€)</field>
<field name="amount">30</field>
</record>
<record model="lunch.cashmove" id="cashmove_2">
<field name="user_id" ref="base.user_admin"/>
<field name="date" eval="(DateTime.now() - timedelta(days=3)).strftime('%Y-%m-%d')"/>
<field name="description">Payment: 7 lunch tickets (6€)</field>
<field name="amount">42</field>
</record>
<record model="lunch.order" id="order_line_1">
<field name="user_id" ref="base.user_demo"/>
<field name="product_id" ref="product_Bolognese"/>
<field name="price">7.70</field>
<field name="date" eval="(DateTime.now() + timedelta(days=2)).strftime('%Y-%m-%d')"/>
<field name="state">new</field>
<field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="quantity">1</field>
<field name="lunch_location_id" ref="location_office_3"/>
</record>
<record model="lunch.order" id="order_line_2">
<field name="user_id" ref="base.user_demo"/>
<field name="product_id" ref="product_italiana"/>
<field name="price">7.40</field>
<field name="date" eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d')"/>
<field name="state">confirmed</field>
<field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="quantity">1</field>
<field name="lunch_location_id" ref="location_office_3"/>
</record>
<record model="lunch.order" id="order_line_3">
<field name="user_id" ref="base.user_demo"/>
<field name="product_id" ref="product_gouda"/>
<field name="price">2.50</field>
<field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
<field name="state">cancelled</field>
<field name="supplier_id" ref="supplier_coin_gourmand"/>
<field name="quantity">1</field>
<field name="lunch_location_id" ref="location_office_3"/>
</record>
<record model="lunch.order" id="order_line_4">
<field name="user_id" ref="base.user_demo"/>
<field name="product_id" ref="product_chicken_curry"/>
<field name="price">2.60</field>
<field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
<field name="state">confirmed</field>
<field name="supplier_id" ref="supplier_coin_gourmand"/>
<field name="quantity">1</field>
<field name="lunch_location_id" ref="location_office_3"/>
</record>
<record model="lunch.order" id="order_line_5">
<field name="user_id" ref="base.user_admin"/>
<field name="product_id" ref="product_4formaggi"/>
<field name="price">5.50</field>
<field name="date" eval="(DateTime.now() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
<field name="state">confirmed</field>
<field name="supplier_id" ref="supplier_pizza_inn"/>
<field name="lunch_location_id" ref="location_office_2"/>
</record>
</data>
</odoo>
+142
View File
@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<record id="lunch_order_mail_supplier" model="mail.template">
<field name="name">Lunch: Supplier Order</field>
<field name="model_id" ref="lunch.model_lunch_supplier"/>
<field name="email_from">{{ ctx['order']['email_from'] }}</field>
<field name="partner_to">{{ ctx['order']['supplier_id'] }}</field>
<field name="subject">Orders for {{ ctx['order']['company_name'] }}</field>
<field name="lang">{{ ctx.get('default_lang') }}</field>
<field name="description">Sent to vendor with the order of the day</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
<tbody>
<!-- HEADER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle">
<span style="font-size: 10px;">Lunch Order</span><br/>
</td><td valign="middle" align="right" t-if="not user.company_id.uses_default_logo">
<img t-attf-src="/logo.png?company={{ user.company_id.id }}" style="padding: 0px; margin: 0px; height: auto; width: 80px;" t-att-alt="user.company_id.name"/>
</td></tr>
<tr><td colspan="2" style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin:16px 0px 16px 0px;"/>
</td></tr>
</table>
</td>
</tr>
<!-- CONTENT -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr>
<td valign="top" style="font-size: 13px;">
<div>
<t t-set="lines" t-value="ctx.get('lines', [])"/>
<t t-set="order" t-value="ctx.get('order')"/>
<t t-set="currency" t-value="user.env['res.currency'].browse(order.get('currency_id'))"/>
<p>
Dear <t t-out="order.get('supplier_name', '')">Laurie Poiret</t>,
</p><p>
Here is, today orders for <t t-out="order.get('company_name', '')">LunchCompany</t>:
</p>
<t t-if="sites">
<br/>
<p>Location</p>
<t t-foreach="site" t-as="site">
<p><t t-out="site['name'] or ''"></t> : <t t-out="site['address'] or ''"></t></p>
</t>
<br/>
</t>
<table>
<thead>
<tr style="background-color:rgb(233,232,233);">
<th style="width: 100%; min-width: 96px; font-size: 13px;"><strong>Product</strong></th>
<th style="width: 100%; min-width: 96px; font-size: 13px;"><strong>Comments</strong></th>
<th style="width: 100%; min-width: 96px; font-size: 13px;"><strong>Person</strong></th>
<th style="width: 100%; min-width: 96px; font-size: 13px;"><strong>Site</strong></th>
<th style="width: 100%; min-width: 96px; font-size: 13px;" align="center"><strong>Qty</strong></th>
<th style="width: 100%; min-width: 96px; font-size: 13px;" align="center"><strong>Price</strong></th>
</tr>
</thead>
<tbody>
<tr t-foreach="lines" t-as="line">
<td style="width: 100%; font-size: 13px;" valign="top" t-out="line['product'] or ''">Sushi salmon</td>
<td style="width: 100%; font-size: 13px;" valign="top">
<t t-if="line['toppings']">
<t t-out="line['toppings'] or ''">Soy sauce</t>
</t>
<t t-if="line['note']">
<div style="color: rgb(173,181,189);" t-out="line['note'] or ''">With wasabi.</div>
</t>
</td>
<td style="width: 100%; font-size: 13px;" valign="top" t-out="line['username'] or ''">lap</td>
<td style="width: 100%; font-size: 13px;" valign="top" t-out="line['site'] or ''">Office 1</td>
<td style="width: 100%; font-size: 13px;" valign="top" align="right" t-out="line['quantity'] or ''">10</td>
<td style="width: 100%; font-size: 13px;" valign="top" align="right" t-out="format_amount(line['price'], currency) or ''">$ 1.00</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td style="width: 100%; font-size: 13px; border-top: 1px solid black;"><strong>Total</strong></td>
<td style="width: 100%; font-size: 13px; border-top: 1px solid black;" align="right"><strong t-out="format_amount(order['amount_total'], currency) or ''">$ 10.00</strong></td>
</tr>
</tbody>
</table>
<p>Do not hesitate to contact us if you have any questions.</p>
</div>
</td>
</tr>
<tr>
<td style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td>
</tr>
</table>
</td>
</tr>
<!-- FOOTER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle" align="left">
<t t-out="user.company_id.name or ''">YourCompany</t>
</td></tr>
<tr><td valign="middle" align="left" style="opacity: 0.7;">
<t t-out="user.company_id.phone or ''">+1 650-123-4567</t>
<t t-if="user.company_id.phone and (user.company_id.email or user.company_id.website)">|</t>
<t t-if="user.company_id.email">
<a t-attf-href="'mailto:%s' % {{ user.company_id.email }}" style="text-decoration:none; color: #454748;" t-out="user.company_id.email or ''">info@yourcompany.com</a>
</t>
<t t-if="user.company_id.email and user.company_id.website">|</t>
<t t-if="user.company_id.website">
<a t-attf-href="'%s' % {{ user.company_id.website }}" style="text-decoration:none; color: #454748;" t-out="user.company_id.website or ''">http://www.example.com</a>
</t>
</td></tr>
</table>
</td>
</tr>
</tbody>
</table>
</td></tr>
<!-- POWERED BY -->
<tr><td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: #F1F1F1; color: #454748; padding: 8px; border-collapse:separate;">
<tr><td style="text-align: center; font-size: 13px;">
Powered by <a target="_blank" href="https://www.odoo.com" style="color: #875A7B;">Odoo</a>
</td></tr>
</table>
</td></tr>
</table>
</field>
</record>
</data></odoo>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import lunch_alert
from . import lunch_cashmove
from . import lunch_location
from . import lunch_order
from . import lunch_product
from . import lunch_product_category
from . import lunch_topping
from . import lunch_supplier
from . import res_company
from . import res_config_settings
from . import res_users
+201
View File
@@ -0,0 +1,201 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
import logging
from odoo import api, fields, models, _
from odoo.osv import expression
from .lunch_supplier import float_to_time
from datetime import datetime, timedelta
from textwrap import dedent
from odoo.addons.base.models.res_partner import _tz_get
_logger = logging.getLogger(__name__)
WEEKDAY_TO_NAME = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
CRON_DEPENDS = {'name', 'active', 'mode', 'until', 'notification_time', 'notification_moment', 'tz'}
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'
_order = 'write_date desc, id'
name = fields.Char('Alert Name', required=True, translate=True)
message = fields.Html('Message', required=True, translate=True)
mode = fields.Selection([
('alert', 'Alert in app'),
('chat', 'Chat notification')], string='Display', default='alert')
recipients = fields.Selection([
('everyone', 'Everyone'),
('last_week', 'Employee who ordered last week'),
('last_month', 'Employee who ordered last month'),
('last_year', 'Employee who ordered last year')], string='Recipients', default='everyone')
notification_time = fields.Float(default=10.0, string='Notification Time')
notification_moment = fields.Selection([
('am', 'AM'),
('pm', 'PM')], default='am', required=True)
tz = fields.Selection(_tz_get, string='Timezone', required=True, default=lambda self: self.env.user.tz or 'UTC')
cron_id = fields.Many2one('ir.cron', ondelete='cascade', required=True, readonly=True)
until = fields.Date('Show Until')
mon = fields.Boolean(default=True)
tue = fields.Boolean(default=True)
wed = fields.Boolean(default=True)
thu = fields.Boolean(default=True)
fri = fields.Boolean(default=True)
sat = fields.Boolean(default=True)
sun = fields.Boolean(default=True)
available_today = fields.Boolean('Is Displayed Today',
compute='_compute_available_today', search='_search_available_today')
active = fields.Boolean('Active', default=True)
location_ids = fields.Many2many('lunch.location', string='Location')
_sql_constraints = [
('notification_time_range',
'CHECK(notification_time >= 0 and notification_time <= 12)',
'Notification time must be between 0 and 12')
]
@api.depends('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun')
def _compute_available_today(self):
today = fields.Date.context_today(self)
fieldname = WEEKDAY_TO_NAME[today.weekday()]
for alert in self:
alert.available_today = alert.until > today if alert.until else True and alert[fieldname]
def _search_available_today(self, operator, value):
if (not operator in ['=', '!=']) or (not value in [True, False]):
return []
searching_for_true = (operator == '=' and value) or (operator == '!=' and not value)
today = fields.Date.context_today(self)
fieldname = WEEKDAY_TO_NAME[today.weekday()]
return expression.AND([
[(fieldname, operator, value)],
expression.OR([
[('until', '=', False)],
[('until', '>' if searching_for_true else '<', today)],
])
])
def _sync_cron(self):
""" Synchronise the related cron fields to reflect this alert """
for alert in self:
alert = alert.with_context(tz=alert.tz)
cron_required = (
alert.active
and alert.mode == 'chat'
and (not alert.until or fields.Date.context_today(alert) <= alert.until)
)
sendat_tz = pytz.timezone(alert.tz).localize(datetime.combine(
fields.Date.context_today(alert, fields.Datetime.now()),
float_to_time(alert.notification_time, alert.notification_moment)))
cron = alert.cron_id.sudo()
lc = cron.lastcall
if ((
lc and sendat_tz.date() <= fields.Datetime.context_timestamp(alert, lc).date()
) or (
not lc and sendat_tz <= fields.Datetime.context_timestamp(alert, fields.Datetime.now())
)):
sendat_tz += timedelta(days=1)
sendat_utc = sendat_tz.astimezone(pytz.UTC).replace(tzinfo=None)
cron.name = f"Lunch: alert chat notification ({alert.name})"
cron.active = cron_required
cron.nextcall = sendat_utc
cron.code = dedent(f"""\
# This cron is dynamically controlled by {self._description}.
# Do NOT modify this cron, modify the related record instead.
env['{self._name}'].browse([{alert.id}])._notify_chat()""")
@api.model_create_multi
def create(self, vals_list):
crons = self.env['ir.cron'].sudo().create([
{
'user_id': self.env.ref('base.user_root').id,
'active': False,
'interval_type': 'days',
'interval_number': 1,
'name': "Lunch: alert chat notification",
'model_id': self.env['ir.model']._get_id(self._name),
'state': 'code',
'code': "",
}
for _ in range(len(vals_list))
])
self.env['ir.model.data'].sudo().create([{
'name': f'lunch_alert_cron_sa_{cron.ir_actions_server_id.id}',
'module': 'lunch',
'res_id': cron.ir_actions_server_id.id,
'model': 'ir.actions.server',
# noupdate is set to true to avoid to delete record at module update
'noupdate': True,
} for cron in crons])
for vals, cron in zip(vals_list, crons):
vals['cron_id'] = cron.id
alerts = super().create(vals_list)
alerts._sync_cron()
return alerts
def write(self, values):
res = super().write(values)
if not CRON_DEPENDS.isdisjoint(values):
self._sync_cron()
return res
def unlink(self):
crons = self.cron_id.sudo()
server_actions = crons.ir_actions_server_id
res = super().unlink()
crons.unlink()
server_actions.unlink()
return res
def _notify_chat(self):
# Called daily by cron
self.ensure_one()
if not self.available_today:
_logger.warning("cancelled, not available today")
if self.cron_id and self.until and fields.Date.context_today(self) > self.until:
self.cron_id.unlink()
self.cron_id = False
return
if not self.active or self.mode != 'chat':
raise ValueError("Cannot send a chat notification in the current state")
order_domain = [('state', '!=', 'cancelled')]
if self.location_ids.ids:
order_domain = expression.AND([order_domain, [('user_id.last_lunch_location_id', 'in', self.location_ids.ids)]])
if self.recipients != 'everyone':
weeksago = fields.Date.today() - timedelta(weeks=(
1 if self.recipients == 'last_week' else
4 if self.recipients == 'last_month' else
52 # if self.recipients == 'last_year'
))
order_domain = expression.AND([order_domain, [('date', '>=', weeksago)]])
partners = self.env['lunch.order'].search(order_domain).user_id.partner_id
if partners:
self.env['mail.thread'].message_notify(
model=self._name,
res_id=self.id,
body=self.message,
partner_ids=partners.ids,
subject=_('Your Lunch Order'),
)
+31
View File
@@ -0,0 +1,31 @@
# -*- 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'
_order = 'date desc'
currency_id = fields.Many2one('res.currency', default=lambda self: self.env.company.currency_id, required=True)
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)
description = fields.Text('Description')
def _compute_display_name(self):
for cashmove in self:
cashmove.display_name = '{} {}'.format(_('Lunch Cashmove'), '#%d' % cashmove.id)
@api.model
def get_wallet_balance(self, user, include_config=True):
result = float_round(sum(move['amount'] for move in self.env['lunch.cashmove.report'].search_read(
[('user_id', '=', user.id)], ['amount'])), precision_digits=2)
if include_config:
result += user.company_id.lunch_minimum_threshold
return result
+13
View File
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class LunchLocation(models.Model):
_name = 'lunch.location'
_description = 'Lunch Locations'
name = fields.Char('Location Name', required=True)
address = fields.Text('Address')
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
+318
View File
@@ -0,0 +1,318 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError, UserError
from odoo.osv.expression import AND
class LunchOrder(models.Model):
_name = 'lunch.order'
_description = 'Lunch Order'
_order = 'id desc'
_display_name = 'product_id'
name = fields.Char(related='product_id.name', string="Product Name", readonly=True)
topping_ids_1 = fields.Many2many('lunch.topping', 'lunch_order_topping', 'order_id', 'topping_id', string='Extras 1', domain=[('topping_category', '=', 1)])
topping_ids_2 = fields.Many2many('lunch.topping', 'lunch_order_topping', 'order_id', 'topping_id', string='Extras 2', domain=[('topping_category', '=', 2)])
topping_ids_3 = fields.Many2many('lunch.topping', 'lunch_order_topping', 'order_id', 'topping_id', string='Extras 3', domain=[('topping_category', '=', 3)])
product_id = fields.Many2one('lunch.product', string="Product", required=True)
category_id = fields.Many2one(
string='Product Category', related='product_id.category_id', store=True)
date = fields.Date('Order Date', required=True, readonly=False,
default=fields.Date.context_today)
supplier_id = fields.Many2one(
string='Vendor', related='product_id.supplier_id', store=True, index=True)
available_today = fields.Boolean(related='supplier_id.available_today')
available_on_date = fields.Boolean(compute='_compute_available_on_date')
order_deadline_passed = fields.Boolean(compute='_compute_order_deadline_passed')
user_id = fields.Many2one('res.users', 'User', default=lambda self: self.env.uid)
lunch_location_id = fields.Many2one('lunch.location', default=lambda self: self.env.user.last_lunch_location_id)
note = fields.Text('Notes')
price = fields.Monetary('Total Price', compute='_compute_total_price', readonly=True, store=True)
active = fields.Boolean('Active', default=True)
state = fields.Selection([('new', 'To Order'),
('ordered', 'Ordered'), # "Internally" ordered
('sent', 'Sent'), # Order sent to the supplier
('confirmed', 'Received'), # Order received
('cancelled', 'Cancelled')],
'Status', readonly=True, index=True, default='new')
notified = fields.Boolean(default=False)
company_id = fields.Many2one('res.company', default=lambda self: self.env.company.id)
currency_id = fields.Many2one(related='company_id.currency_id', store=True)
quantity = fields.Float('Quantity', required=True, default=1)
display_toppings = fields.Text('Extras', compute='_compute_display_toppings', store=True)
product_description = fields.Html('Description', related='product_id.description')
topping_label_1 = fields.Char(related='product_id.supplier_id.topping_label_1')
topping_label_2 = fields.Char(related='product_id.supplier_id.topping_label_2')
topping_label_3 = fields.Char(related='product_id.supplier_id.topping_label_3')
topping_quantity_1 = fields.Selection(related='product_id.supplier_id.topping_quantity_1')
topping_quantity_2 = fields.Selection(related='product_id.supplier_id.topping_quantity_2')
topping_quantity_3 = fields.Selection(related='product_id.supplier_id.topping_quantity_3')
image_1920 = fields.Image(compute='_compute_product_images')
image_128 = fields.Image(compute='_compute_product_images')
available_toppings_1 = fields.Boolean(help='Are extras available for this product', compute='_compute_available_toppings')
available_toppings_2 = fields.Boolean(help='Are extras available for this product', compute='_compute_available_toppings')
available_toppings_3 = fields.Boolean(help='Are extras available for this product', compute='_compute_available_toppings')
display_reorder_button = fields.Boolean(compute='_compute_display_reorder_button')
display_add_button = fields.Boolean(compute='_compute_display_add_button')
@api.depends('product_id')
def _compute_product_images(self):
for line in self:
line.image_1920 = line.product_id.image_1920 or line.category_id.image_1920
line.image_128 = line.product_id.image_128 or line.category_id.image_128
@api.depends('category_id')
def _compute_available_toppings(self):
for order in self:
order.available_toppings_1 = bool(order.env['lunch.topping'].search_count([('supplier_id', '=', order.supplier_id.id), ('topping_category', '=', 1)]))
order.available_toppings_2 = bool(order.env['lunch.topping'].search_count([('supplier_id', '=', order.supplier_id.id), ('topping_category', '=', 2)]))
order.available_toppings_3 = bool(order.env['lunch.topping'].search_count([('supplier_id', '=', order.supplier_id.id), ('topping_category', '=', 3)]))
@api.depends('name')
def _compute_display_add_button(self):
new_orders = dict(self.env["lunch.order"]._read_group([
("date", "in", self.mapped("date")),
("user_id", "in", self.user_id.ids),
("state", "=", "new"),
], ['user_id'], ['id:recordset']))
for order in self:
user_new_orders = new_orders.get(order.user_id)
price = 0
if user_new_orders:
user_new_orders = user_new_orders.filtered(lambda lunch_order: lunch_order.date == order.date)
price = sum(order.price for order in user_new_orders)
wallet_amount = self.env['lunch.cashmove'].get_wallet_balance(order.user_id) - price
order.display_add_button = wallet_amount >= order.price
@api.depends_context('show_reorder_button')
@api.depends('state')
def _compute_display_reorder_button(self):
show_button = self.env.context.get('show_reorder_button')
for order in self:
order.display_reorder_button = show_button and order.state == 'confirmed' and order.supplier_id.available_today
@api.depends('date', 'supplier_id')
def _compute_available_on_date(self):
for order in self:
order.available_on_date = order.supplier_id._available_on_date(order.date)
@api.depends('supplier_id', 'date')
def _compute_order_deadline_passed(self):
today = fields.Date.context_today(self)
for order in self:
if order.date < today:
order.order_deadline_passed = True
elif order.date == today:
order.order_deadline_passed = order.supplier_id.order_deadline_passed
else:
order.order_deadline_passed = False
def init(self):
self._cr.execute("""CREATE INDEX IF NOT EXISTS lunch_order_user_product_date ON %s (user_id, product_id, date)"""
% self._table)
def _get_topping_ids(self, field, values):
return list(self._fields[field].convert_to_cache(values, self))
def _extract_toppings(self, values):
"""
If called in api.multi then it will pop topping_ids_1,2,3 from values
"""
topping_ids = []
for i in range(1, 4):
topping_field = f'topping_ids_{i}'
topping_values = values.get(topping_field, False)
if self.ids:
# TODO This is not taking into account all the toppings for each individual order, this is usually not a problem
# since in the interface you usually don't update more than one order at a time but this is a bug nonetheless
topping_ids += self._get_topping_ids(topping_field, values.pop(topping_field)) \
if topping_values else self[:1][topping_field].ids
else:
topping_ids += self._get_topping_ids(topping_field, topping_values) if topping_values else []
return topping_ids
@api.constrains('topping_ids_1', 'topping_ids_2', 'topping_ids_3')
def _check_topping_quantity(self):
errors = {
'1_more': _('You should order at least one %s'),
'1': _('You have to order one and only one %s'),
}
for line in self:
for index in range(1, 4):
availability = line['available_toppings_%s' % index]
quantity = line['topping_quantity_%s' % index]
toppings = line['topping_ids_%s' % index].filtered(lambda x: x.topping_category == index)
label = line['topping_label_%s' % index]
if availability and quantity != '0_more':
check = bool(len(toppings) == 1 if quantity == '1' else toppings)
if not check:
raise ValidationError(errors[quantity] % label)
@api.model_create_multi
def create(self, vals_list):
orders = self.env['lunch.order']
for vals in vals_list:
lines = self._find_matching_lines({
**vals,
'toppings': self._extract_toppings(vals),
'state': 'new',
})
if lines:
# YTI FIXME This will update multiple lines in the case there are multiple
# matching lines which should not happen through the interface
lines.update_quantity(1)
orders |= lines[:1]
else:
orders |= super().create(vals)
return orders
def write(self, values):
change_topping = 'topping_ids_1' in values or 'topping_ids_2' in values or 'topping_ids_3' in values
merge_needed = 'note' in values or change_topping or 'state' in values
default_location_id = self.env.user.last_lunch_location_id and self.env.user.last_lunch_location_id.id or False
if merge_needed:
lines_to_deactivate = self.env['lunch.order']
for line in self.filtered(lambda line: line.state not in ['sent', 'confirmed']):
# Only write on topping_ids_1 because they all share the same table
# and we don't want to remove all the records
# _extract_toppings will pop topping_ids_1, topping_ids_2 and topping_ids_3 from values
# This also forces us to invalidate the cache for topping_ids_2 and topping_ids_3 that
# could have changed through topping_ids_1 without the cache knowing about it
toppings = self._extract_toppings(values)
if change_topping:
self.invalidate_model(['topping_ids_2', 'topping_ids_3'])
values['topping_ids_1'] = [(6, 0, toppings)]
matching_lines = self._find_matching_lines({
'user_id': values.get('user_id', line.user_id.id),
'product_id': values.get('product_id', line.product_id.id),
'note': values.get('note', line.note or False),
'toppings': toppings,
'lunch_location_id': values.get('lunch_location_id', default_location_id),
'state': values.get('state'),
}) - line
if matching_lines:
lines_to_deactivate |= line
matching_lines.update_quantity(line.quantity)
lines_to_deactivate.write({'active': False})
return super(LunchOrder, self - lines_to_deactivate).write(values)
return super().write(values)
@api.model
def _find_matching_lines(self, values):
default_location_id = self.env.user.last_lunch_location_id and self.env.user.last_lunch_location_id.id or False
domain = [
('user_id', '=', values.get('user_id', self.default_get(['user_id'])['user_id'])),
('product_id', '=', values.get('product_id', False)),
('date', '=', values.get('date', fields.Date.today())),
('note', '=', values.get('note', False)),
('lunch_location_id', '=', values.get('lunch_location_id', default_location_id)),
]
if values.get('state'):
domain = AND([domain, [('state', '=', values['state'])]])
toppings = values.get('toppings', [])
return self.search(domain).filtered(lambda line: (line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3).ids == toppings)
@api.depends('topping_ids_1', 'topping_ids_2', 'topping_ids_3', 'product_id', 'quantity')
def _compute_total_price(self):
for line in self:
line.price = line.quantity * (line.product_id.price + sum((line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3).mapped('price')))
@api.depends('topping_ids_1', 'topping_ids_2', 'topping_ids_3')
def _compute_display_toppings(self):
for line in self:
toppings = line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3
line.display_toppings = ' + '.join(toppings.mapped('name'))
def update_quantity(self, increment):
for line in self.filtered(lambda line: line.state not in ['sent', 'confirmed']):
if line.quantity <= -increment:
# TODO: maybe unlink the order?
line.active = False
else:
line.quantity += increment
self._check_wallet()
def add_to_cart(self):
"""
This method currently does nothing, we currently need it in order to
be able to reuse this model in place of a wizard
"""
# YTI FIXME: Find a way to drop this.
return True
def _check_wallet(self):
self.env.flush_all()
for line in self:
if self.env['lunch.cashmove'].get_wallet_balance(line.user_id) < 0:
raise ValidationError(_('Your wallet does not contain enough money to order that. To add some money to your wallet, please contact your lunch manager.'))
def action_order(self):
for order in self:
if not order.available_on_date:
raise UserError(_('The vendor related to this order is not available at the selected date.'))
if self.filtered(lambda line: not line.product_id.active):
raise ValidationError(_('Product is no longer available.'))
self.write({
'state': 'ordered',
})
self._check_wallet()
def action_reorder(self):
self.ensure_one()
if not self.supplier_id.available_today:
raise UserError(_('The vendor related to this order is not available today.'))
self.copy({
'date': fields.Date.context_today(self),
'state': 'ordered',
})
action = self.env['ir.actions.act_window']._for_xml_id('lunch.lunch_order_action')
return action
def action_confirm(self):
self.write({'state': 'confirmed'})
def action_cancel(self):
self.write({'state': 'cancelled'})
def action_reset(self):
self.write({'state': 'ordered'})
def action_send(self):
self.state = 'sent'
def action_notify(self):
self -= self.filtered('notified')
if not self:
return
notified_users = set()
# (company, lang): (subject, body)
translate_cache = dict()
for order in self:
user = order.user_id
if user in notified_users:
continue
_key = (order.company_id, user.lang)
if _key not in translate_cache:
context = {'lang': user.lang}
translate_cache[_key] = (_('Lunch notification'), order.company_id.with_context(lang=user.lang).lunch_notify_message)
del context
subject, body = translate_cache[_key]
user.partner_id.message_notify(
subject=subject,
body=body,
partner_ids=user.partner_id.ids,
email_layout_xmlid='mail.mail_notification_light',
)
notified_users.add(user)
self.write({'notified': True})
+129
View File
@@ -0,0 +1,129 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from collections import defaultdict
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.osv import expression
class LunchProduct(models.Model):
""" Products available to order. A product is linked to a specific vendor. """
_name = 'lunch.product'
_description = 'Lunch Product'
_inherit = 'image.mixin'
_order = 'name'
_check_company_auto = True
name = fields.Char('Product Name', required=True, translate=True)
category_id = fields.Many2one('lunch.product.category', 'Product Category', check_company=True, required=True)
description = fields.Html('Description', translate=True)
price = fields.Float('Price', digits='Account', required=True)
supplier_id = fields.Many2one('lunch.supplier', 'Vendor', check_company=True, required=True)
active = fields.Boolean(default=True)
company_id = fields.Many2one('res.company', related='supplier_id.company_id', readonly=False, store=True)
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
new_until = fields.Date('New Until')
is_new = fields.Boolean(compute='_compute_is_new')
favorite_user_ids = fields.Many2many('res.users', 'lunch_product_favorite_user_rel', 'product_id', 'user_id', check_company=True)
is_favorite = fields.Boolean(compute='_compute_is_favorite', inverse='_inverse_is_favorite')
last_order_date = fields.Date(compute='_compute_last_order_date')
product_image = fields.Image(compute='_compute_product_image')
# This field is used only for searching
is_available_at = fields.Many2one('lunch.location', 'Product Availability', compute='_compute_is_available_at', search='_search_is_available_at')
@api.depends('image_128', 'category_id.image_128')
def _compute_product_image(self):
for product in self:
product.product_image = product.image_128 or product.category_id.image_128
@api.depends('new_until')
def _compute_is_new(self):
today = fields.Date.context_today(self)
for product in self:
if product.new_until:
product.is_new = today <= product.new_until
else:
product.is_new = False
@api.depends_context('uid')
@api.depends('favorite_user_ids')
def _compute_is_favorite(self):
for product in self:
product.is_favorite = self.env.user in product.favorite_user_ids
@api.depends_context('uid')
def _compute_last_order_date(self):
all_orders = self.env['lunch.order'].search([
('user_id', '=', self.env.user.id),
('product_id', 'in', self.ids),
])
mapped_orders = defaultdict(lambda: self.env['lunch.order'])
for order in all_orders:
mapped_orders[order.product_id] |= order
for product in self:
if not mapped_orders[product]:
product.last_order_date = False
else:
product.last_order_date = max(mapped_orders[product].mapped('date'))
def _compute_is_available_at(self):
"""
Is available_at is always false when browsing it
this field is there only to search (see _search_is_available_at)
"""
for product in self:
product.is_available_at = False
def _search_is_available_at(self, operator, value):
supported_operators = ['in', 'not in', '=', '!=']
if not operator in supported_operators:
return expression.TRUE_DOMAIN
if isinstance(value, int):
value = [value]
if operator in expression.NEGATIVE_TERM_OPERATORS:
return expression.AND([[('supplier_id.available_location_ids', 'not in', value)], [('supplier_id.available_location_ids', '!=', False)]])
return expression.OR([[('supplier_id.available_location_ids', 'in', value)], [('supplier_id.available_location_ids', '=', False)]])
def _sync_active_from_related(self):
""" Archive/unarchive product after related field is archived/unarchived """
return self.filtered(lambda p: (p.category_id.active and p.supplier_id.active) != p.active).toggle_active()
def toggle_active(self):
invalid_products = self.filtered(lambda product: not product.active and not product.category_id.active)
if invalid_products:
raise UserError(_("The following product categories are archived. You should either unarchive the categories or change the category of the product.\n%s", '\n'.join(invalid_products.category_id.mapped('name'))))
invalid_products = self.filtered(lambda product: not product.active and not product.supplier_id.active)
if invalid_products:
raise UserError(_("The following suppliers are archived. You should either unarchive the suppliers or change the supplier of the product.\n%s", '\n'.join(invalid_products.supplier_id.mapped('name'))))
return super().toggle_active()
def _inverse_is_favorite(self):
""" Handled in the write() """
return
def write(self, vals):
if 'is_favorite' in vals:
if vals.pop('is_favorite'):
commands = [(4, product.id) for product in self]
else:
commands = [(3, product.id) for product in self]
self.env.user.write({
'favorite_lunch_product_ids': commands,
})
if not vals:
return True
return super().write(vals)
@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from odoo import api, fields, models
from odoo.tools.misc import file_open
class LunchProductCategory(models.Model):
""" Category of the product such as pizza, sandwich, pasta, chinese, burger... """
_name = 'lunch.product.category'
_inherit = 'image.mixin'
_description = 'Lunch Product Category'
@api.model
def _default_image(self):
return base64.b64encode(file_open('lunch/static/img/lunch.png', 'rb').read())
name = fields.Char('Product Category', required=True, translate=True)
company_id = fields.Many2one('res.company')
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
product_count = fields.Integer(compute='_compute_product_count', help="The number of products related to this category")
active = fields.Boolean(string='Active', default=True)
image_1920 = fields.Image(default=_default_image)
def _compute_product_count(self):
product_data = self.env['lunch.product']._read_group([('category_id', 'in', self.ids)], ['category_id'], ['__count'])
data = {category.id: count for category, count in product_data}
for category in self:
category.product_count = data.get(category.id, 0)
def toggle_active(self):
""" Archiving related lunch product """
res = super().toggle_active()
Product = self.env['lunch.product'].with_context(active_test=False)
all_products = Product.search([('category_id', 'in', self.ids)])
all_products._sync_active_from_related()
return res
+392
View File
@@ -0,0 +1,392 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import math
import pytz
from collections import defaultdict
from datetime import datetime, time, timedelta
from textwrap import dedent
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.osv import expression
from odoo.tools import float_round
from odoo.addons.base.models.res_partner import _tz_get
WEEKDAY_TO_NAME = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
CRON_DEPENDS = {'name', 'active', 'send_by', 'automatic_email_time', 'moment', 'tz'}
def float_to_time(hours, moment='am'):
""" Convert a number of hours into a time object. """
if hours == 12.0 and moment == 'pm':
return time.max
fractional, integral = math.modf(hours)
if moment == 'pm':
integral += 12
return time(int(integral), int(float_round(60 * fractional, precision_digits=0)), 0)
def time_to_float(t):
return float_round(t.hour + t.minute/60 + t.second/3600, precision_digits=2)
class LunchSupplier(models.Model):
_name = 'lunch.supplier'
_description = 'Lunch Supplier'
_inherit = ['mail.thread', 'mail.activity.mixin']
partner_id = fields.Many2one('res.partner', string='Vendor', required=True)
name = fields.Char('Name', related='partner_id.name', readonly=False)
email = fields.Char(related='partner_id.email', readonly=False)
email_formatted = fields.Char(related='partner_id.email_formatted', readonly=True)
phone = fields.Char(related='partner_id.phone', readonly=False)
street = fields.Char(related='partner_id.street', readonly=False)
street2 = fields.Char(related='partner_id.street2', readonly=False)
zip_code = fields.Char(related='partner_id.zip', readonly=False)
city = fields.Char(related='partner_id.city', readonly=False)
state_id = fields.Many2one("res.country.state", related='partner_id.state_id', readonly=False)
country_id = fields.Many2one('res.country', related='partner_id.country_id', readonly=False)
company_id = fields.Many2one('res.company', related='partner_id.company_id', readonly=False, store=True)
responsible_id = fields.Many2one('res.users', string="Responsible", domain=lambda self: [('groups_id', 'in', self.env.ref('lunch.group_lunch_manager').id)],
default=lambda self: self.env.user,
help="The responsible is the person that will order lunch for everyone. It will be used as the 'from' when sending the automatic email.")
send_by = fields.Selection([
('phone', 'Phone'),
('mail', 'Email'),
], 'Send Order By', default='phone')
automatic_email_time = fields.Float('Order Time', default=12.0, required=True)
cron_id = fields.Many2one('ir.cron', ondelete='cascade', required=True, readonly=True)
mon = fields.Boolean(default=True)
tue = fields.Boolean(default=True)
wed = fields.Boolean(default=True)
thu = fields.Boolean(default=True)
fri = fields.Boolean(default=True)
sat = fields.Boolean()
sun = fields.Boolean()
recurrency_end_date = fields.Date('Until', help="This field is used in order to ")
available_location_ids = fields.Many2many('lunch.location', string='Location')
available_today = fields.Boolean('This is True when if the supplier is available today',
compute='_compute_available_today', search='_search_available_today')
order_deadline_passed = fields.Boolean(compute='_compute_order_deadline_passed')
tz = fields.Selection(_tz_get, string='Timezone', required=True, default=lambda self: self.env.user.tz or 'UTC')
active = fields.Boolean(default=True)
moment = fields.Selection([
('am', 'AM'),
('pm', 'PM'),
], default='am', required=True)
delivery = fields.Selection([
('delivery', 'Delivery'),
('no_delivery', 'No Delivery')
], default='no_delivery')
topping_label_1 = fields.Char('Extra 1 Label', required=True, default='Extras')
topping_label_2 = fields.Char('Extra 2 Label', required=True, default='Beverages')
topping_label_3 = fields.Char('Extra 3 Label', required=True, default='Extra Label 3')
topping_ids_1 = fields.One2many('lunch.topping', 'supplier_id', domain=[('topping_category', '=', 1)])
topping_ids_2 = fields.One2many('lunch.topping', 'supplier_id', domain=[('topping_category', '=', 2)])
topping_ids_3 = fields.One2many('lunch.topping', 'supplier_id', domain=[('topping_category', '=', 3)])
topping_quantity_1 = fields.Selection([
('0_more', 'None or More'),
('1_more', 'One or More'),
('1', 'Only One')], 'Extra 1 Quantity', default='0_more', required=True)
topping_quantity_2 = fields.Selection([
('0_more', 'None or More'),
('1_more', 'One or More'),
('1', 'Only One')], 'Extra 2 Quantity', default='0_more', required=True)
topping_quantity_3 = fields.Selection([
('0_more', 'None or More'),
('1_more', 'One or More'),
('1', 'Only One')], 'Extra 3 Quantity', default='0_more', required=True)
show_order_button = fields.Boolean(compute='_compute_buttons')
show_confirm_button = fields.Boolean(compute='_compute_buttons')
_sql_constraints = [
('automatic_email_time_range',
'CHECK(automatic_email_time >= 0 AND automatic_email_time <= 12)',
'Automatic Email Sending Time should be between 0 and 12'),
]
@api.depends('phone')
def _compute_display_name(self):
for supplier in self:
if supplier.phone:
supplier.display_name = f'{supplier.name} {supplier.phone}'
else:
supplier.display_name = supplier.name
def _sync_cron(self):
for supplier in self:
supplier = supplier.with_context(tz=supplier.tz)
sendat_tz = pytz.timezone(supplier.tz).localize(datetime.combine(
fields.Date.context_today(supplier),
float_to_time(supplier.automatic_email_time, supplier.moment)))
cron = supplier.cron_id.sudo()
lc = cron.lastcall
if ((
lc and sendat_tz.date() <= fields.Datetime.context_timestamp(supplier, lc).date()
) or (
not lc and sendat_tz <= fields.Datetime.context_timestamp(supplier, fields.Datetime.now())
)):
sendat_tz += timedelta(days=1)
sendat_utc = sendat_tz.astimezone(pytz.UTC).replace(tzinfo=None)
cron.active = supplier.active and supplier.send_by == 'mail'
cron.name = f"Lunch: send automatic email to {supplier.name}"
cron.nextcall = sendat_utc
cron.code = dedent(f"""\
# This cron is dynamically controlled by {self._description}.
# Do NOT modify this cron, modify the related record instead.
env['{self._name}'].browse([{supplier.id}])._send_auto_email()""")
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
for topping in vals.get('topping_ids_2', []):
topping[2].update({'topping_category': 2})
for topping in vals.get('topping_ids_3', []):
topping[2].update({'topping_category': 3})
crons = self.env['ir.cron'].sudo().create([
{
'user_id': self.env.ref('base.user_root').id,
'active': False,
'interval_type': 'days',
'interval_number': 1,
'name': "Lunch: send automatic email",
'model_id': self.env['ir.model']._get_id(self._name),
'state': 'code',
'code': "",
}
for _ in range(len(vals_list))
])
self.env['ir.model.data'].sudo().create([{
'name': f'lunch_supplier_cron_sa_{cron.ir_actions_server_id.id}',
'module': 'lunch',
'res_id': cron.ir_actions_server_id.id,
'model': 'ir.actions.server',
# noupdate is set to true to avoid to delete record at module update
'noupdate': True,
} for cron in crons])
for vals, cron in zip(vals_list, crons):
vals['cron_id'] = cron.id
suppliers = super().create(vals_list)
suppliers._sync_cron()
return suppliers
def write(self, values):
for topping in values.get('topping_ids_2', []):
topping_values = topping[2] if len(topping) > 2 else False
if topping_values:
topping_values.update({'topping_category': 2})
for topping in values.get('topping_ids_3', []):
topping_values = topping[2] if len(topping) > 2 else False
if topping_values:
topping_values.update({'topping_category': 3})
if values.get('company_id'):
self.env['lunch.order'].search([('supplier_id', 'in', self.ids)]).write({'company_id': values['company_id']})
res = super().write(values)
if not CRON_DEPENDS.isdisjoint(values):
# flush automatic_email_time field to call _sql_constraints
if 'automatic_email_time' in values:
self.flush_model(['automatic_email_time'])
self._sync_cron()
days_removed = [val for val in values if val in ('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun') and not values[val]]
if days_removed:
self._cancel_future_days(days_removed)
return res
def unlink(self):
crons = self.cron_id.sudo()
server_actions = crons.ir_actions_server_id
res = super().unlink()
crons.unlink()
server_actions.unlink()
return res
def toggle_active(self):
""" Archiving related lunch product """
res = super().toggle_active()
active_suppliers = self.filtered(lambda s: s.active)
inactive_suppliers = self - active_suppliers
Product = self.env['lunch.product'].with_context(active_test=False)
Product.search([('supplier_id', 'in', active_suppliers.ids)]).write({'active': True})
Product.search([('supplier_id', 'in', inactive_suppliers.ids)]).write({'active': False})
return res
def _cancel_future_days(self, weekdays):
weekdays_n = [WEEKDAY_TO_NAME.index(wd) for wd in weekdays]
self.env['lunch.order'].search([
('supplier_id', 'in', self.ids),
('state', 'in', ('new', 'ordered')),
('date', '>=', fields.Date.context_today(self.with_context(tz=self.tz))),
]).filtered(lambda lo: lo.date.weekday() in weekdays_n).write({'state': 'cancelled'})
def _get_current_orders(self, state='ordered'):
""" Returns today's orders """
available_today = self.filtered('available_today')
if not available_today:
return self.env['lunch.order']
orders = self.env['lunch.order'].search([
('supplier_id', 'in', available_today.ids),
('state', '=', state),
('date', '=', fields.Date.context_today(self.with_context(tz=self.tz))),
], order="user_id, product_id")
return orders
def _send_auto_email(self):
""" Send an email to the supplier with the order of the day """
# Called daily by cron
self.ensure_one()
if not self.available_today:
return
if self.send_by != 'mail':
raise UserError(_("Cannot send an email to this supplier!"))
orders = self._get_current_orders()
if not orders:
return
order = {
'company_name': orders[0].company_id.name,
'currency_id': orders[0].currency_id.id,
'supplier_id': self.partner_id.id,
'supplier_name': self.name,
'email_from': self.responsible_id.email_formatted,
'amount_total': sum(order.price for order in orders),
}
sites = orders.mapped('user_id.last_lunch_location_id').sorted(lambda x: x.name)
orders_per_site = orders.sorted(lambda x: x.user_id.last_lunch_location_id.id)
email_orders = [{
'product': order.product_id.name,
'note': order.note,
'quantity': order.quantity,
'price': order.price,
'toppings': order.display_toppings,
'username': order.user_id.name,
'site': order.user_id.last_lunch_location_id.name,
} for order in orders_per_site]
email_sites = [{
'name': site.name,
'address': site.address,
} for site in sites]
self.env.ref('lunch.lunch_order_mail_supplier').with_context(
order=order, lines=email_orders, sites=email_sites
).send_mail(self.id)
orders.action_send()
@api.depends('recurrency_end_date', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun')
def _compute_available_today(self):
now = fields.Datetime.now().replace(tzinfo=pytz.UTC)
for supplier in self:
supplier_date = now.astimezone(pytz.timezone(supplier.tz))
supplier.available_today = supplier._available_on_date(supplier_date)
def _available_on_date(self, date):
self.ensure_one()
fieldname = WEEKDAY_TO_NAME[date.weekday()]
return not (self.recurrency_end_date and date.date() >= self.recurrency_end_date) and self[fieldname]
@api.depends('available_today', 'automatic_email_time', 'send_by')
def _compute_order_deadline_passed(self):
now = fields.Datetime.now().replace(tzinfo=pytz.UTC)
for supplier in self:
if supplier.send_by == 'mail':
now = now.astimezone(pytz.timezone(supplier.tz))
email_time = pytz.timezone(supplier.tz).localize(datetime.combine(
fields.Date.context_today(supplier),
float_to_time(supplier.automatic_email_time, supplier.moment)))
supplier.order_deadline_passed = supplier.available_today and now > email_time
else:
supplier.order_deadline_passed = not supplier.available_today
def _search_available_today(self, operator, value):
if (not operator in ['=', '!=']) or (not value in [True, False]):
return []
searching_for_true = (operator == '=' and value) or (operator == '!=' and not value)
now = fields.Datetime.now().replace(tzinfo=pytz.UTC).astimezone(pytz.timezone(self.env.user.tz or 'UTC'))
fieldname = WEEKDAY_TO_NAME[now.weekday()]
recurrency_domain = expression.OR([
[('recurrency_end_date', '=', False)],
[('recurrency_end_date', '>' if searching_for_true else '<', now)]
])
return expression.AND([
recurrency_domain,
[(fieldname, operator, value)]
])
def _compute_buttons(self):
self.env.cr.execute("""
SELECT supplier_id, state, COUNT(*)
FROM lunch_order
WHERE supplier_id IN %s
AND state in ('ordered', 'sent')
AND date = %s
AND active
GROUP BY supplier_id, state
""", (tuple(self.ids), fields.Date.context_today(self)))
supplier_orders = defaultdict(dict)
for order in self.env.cr.fetchall():
supplier_orders[order[0]][order[1]] = order[2]
for supplier in self:
supplier.show_order_button = supplier_orders[supplier.id].get('ordered', False)
supplier.show_confirm_button = supplier_orders[supplier.id].get('sent', False)
def action_send_orders(self):
no_auto_mail = self.filtered(lambda s: s.send_by != 'mail')
for supplier in self - no_auto_mail:
supplier._send_auto_email()
orders = no_auto_mail._get_current_orders()
orders.action_send()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'success',
'message': _('The orders have been sent!'),
'next': {'type': 'ir.actions.act_window_close'},
}
}
def action_confirm_orders(self):
orders = self._get_current_orders(state='sent')
orders.action_confirm()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'success',
'message': _('The orders have been confirmed!'),
'next': {'type': 'ir.actions.act_window_close'},
}
}
+26
View File
@@ -0,0 +1,26 @@
# -*- 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 formatLang
class LunchTopping(models.Model):
_name = 'lunch.topping'
_description = 'Lunch Extras'
name = fields.Char('Name', required=True)
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
price = fields.Monetary('Price', required=True)
supplier_id = fields.Many2one('lunch.supplier', ondelete='cascade')
topping_category = fields.Integer('Topping Category', required=True, default=1)
@api.depends('price')
@api.depends_context('company')
def _compute_display_name(self):
currency_id = self.env.company.currency_id
for topping in self:
price = formatLang(self.env, topping.price, currency_obj=currency_id)
topping.display_name = f'{topping.name} {price}'
+13
View File
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
class Company(models.Model):
_inherit = 'res.company'
lunch_minimum_threshold = fields.Float()
lunch_notify_message = fields.Html(
default="""Your lunch has been delivered.
Enjoy your meal!""", translate=True)
@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
company_lunch_minimum_threshold = fields.Float(string="Maximum Allowed Overdraft", readonly=False, related='company_id.lunch_minimum_threshold')
company_lunch_notify_message = fields.Html(string="Lunch notification message", readonly=False, related="company_id.lunch_notify_message")
+11
View File
@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResUsers(models.Model):
_inherit = 'res.users'
last_lunch_location_id = fields.Many2one('lunch.location')
favorite_lunch_product_ids = fields.Many2many('lunch.product', 'lunch_product_favorite_user_rel', 'user_id', 'product_id')
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import lunch_cashmove_report
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, tools, _
class CashmoveReport(models.Model):
_name = "lunch.cashmove.report"
_description = 'Cashmoves report'
_auto = False
_order = "date desc"
id = fields.Integer('ID')
amount = fields.Float('Amount')
date = fields.Date('Date')
currency_id = fields.Many2one('res.currency', string='Currency')
user_id = fields.Many2one('res.users', string='User')
description = fields.Text('Description')
def _compute_display_name(self):
for cashmove in self:
cashmove.display_name = '{} {}'.format(_('Lunch Cashmove'), '#%d' % cashmove.id)
def init(self):
tools.drop_view_if_exists(self._cr, self._table)
self._cr.execute("""
CREATE or REPLACE view %s as (
SELECT
lc.id as id,
lc.amount as amount,
lc.date as date,
lc.currency_id as currency_id,
lc.user_id as user_id,
lc.description as description
FROM lunch_cashmove lc
UNION ALL
SELECT
-lol.id as id,
-lol.price as amount,
lol.date as date,
lol.currency_id as currency_id,
lol.user_id as user_id,
format('Order: %%s x %%s %%s', lol.quantity::text, lp.name->>'en_US', lol.display_toppings) as description
FROM lunch_order lol
JOIN lunch_product lp ON lp.id = lol.product_id
WHERE
lol.state in ('ordered', 'confirmed')
AND lol.active = True
);
""" % self._table)
@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="lunch_cashmove_report_view_search" model="ir.ui.view">
<field name='name'>lunch.cashmove.report.search</field>
<field name='model'>lunch.cashmove.report</field>
<field name='arch' type='xml'>
<search string="lunch employee payment">
<field name="description"/>
<field name="user_id"/>
<filter name='is_payment' string="Payment" domain="[('amount', '>', 0)]"/>
<separator/>
<filter name='is_mine_group' string="My Account grouped" domain="[('user_id','=',uid)]" context="{'group_by':'user_id'}"/>
<filter name="group_by_user" string="By User" context="{'group_by':'user_id'}"/>
</search>
</field>
</record>
<record id="lunch_cashmove_report_view_search_2" model="ir.ui.view">
<field name='name'>lunch.cashmove.report.search</field>
<field name='model'>lunch.cashmove.report</field>
<field name='arch' type='xml'>
<search string="lunch cashmove">
<field name="description"/>
<field name="user_id"/>
<group expand="0" string="Group By">
<filter name='group_by_user' string="By Employee" context="{'group_by':'user_id'}"/>
</group>
</search>
</field>
</record>
<record id="lunch_cashmove_report_view_tree" model="ir.ui.view">
<field name="name">lunch.cashmove.report.list</field>
<field name="model">lunch.cashmove.report</field>
<field name="arch" type="xml">
<list string="cashmove list">
<field name="currency_id" column_invisible="True"/>
<field name="date"/>
<field name="user_id" widget="many2one_avatar_user"/>
<field name="description"/>
<field name="amount" sum="Total" widget="monetary"/>
</list>
</field>
</record>
<record id="lunch_cashmove_report_view_tree_2" model="ir.ui.view">
<field name="name">lunch.cashmove.report.list</field>
<field name="model">lunch.cashmove.report</field>
<field name="arch" type="xml">
<list string="cashmove list" create='false'>
<field name="currency_id" column_invisible="True"/>
<field name="date"/>
<field name="description"/>
<field name="amount" sum="Total" widget="monetary"/>
</list>
</field>
</record>
<record id="lunch_cashmove_report_view_form" model="ir.ui.view">
<field name="name">lunch.cashmove.report.form</field>
<field name="model">lunch.cashmove.report</field>
<field name="arch" type="xml">
<form string="cashmove form">
<sheet>
<group>
<field name="currency_id" invisible="1"/>
<field name="user_id" required="1" widget="many2one_avatar"/>
<field name="date"/>
<field name="amount" widget="monetary"/>
</group>
<label for='description'/>
<field name="description"/>
</sheet>
</form>
</field>
</record>
<record id="view_lunch_cashmove_report_kanban" model="ir.ui.view">
<field name="name">lunch.cashmove.report.kanban</field>
<field name="model">lunch.cashmove.report</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile">
<field name="currency_id"/>
<templates>
<t t-name="card">
<div class="row mb4">
<div class="col-8 fw-bold fs-5">
<field name="description" />
</div>
<div class="col-4 text-end badge rounded-pill fw-bolder pe-3 pt-1">
<i class="fa fa-money" role="img" aria-label="Amount" title="Amount"/> <field name="amount" widget="monetary"/>
</div>
</div>
<div class="row">
<div class="col-6">
<i class="fa fa-clock-o" role="img" aria-label="Date" title="Date"/> <field name="date"/>
</div>
<div class="col-6">
<field name="user_id" widget="many2one_avatar_user" class="float-end"/>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="lunch_cashmove_report_action_account" model="ir.actions.act_window">
<field name="name">My Account</field>
<field name="res_model">lunch.cashmove.report</field>
<field name="view_mode">list</field>
<field name="search_view_id" ref="lunch_cashmove_report_view_search"/>
<field name="domain">[('user_id','=',uid)]</field>
<field name="view_id" ref="lunch_cashmove_report_view_tree_2"/>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
No cash move yet
</p><p>
Here you can see your cash moves.<br/>A cash move can either be an expense or a payment.
An expense is automatically created when an order is received while a payment is a reimbursement to the company encoded by the manager.
</p>
</field>
</record>
<record id="lunch_cashmove_report_action_control_accounts" model="ir.actions.act_window">
<field name="name">Control Accounts</field>
<field name="res_model">lunch.cashmove.report</field>
<field name="view_mode">list,kanban,form</field>
<field name="search_view_id" ref="lunch_cashmove_report_view_search_2"/>
<field name="context">{"search_default_group_by_user":1}</field>
<field name="view_id" ref="lunch_cashmove_report_view_tree"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new payment
</p><p>
A cashmove can either be an expense or a payment.<br/>
An expense is automatically created at the order receipt.<br/>
A payment represents the employee reimbursement to the company.
</p>
</field>
</record>
</odoo>
+18
View File
@@ -0,0 +1,18 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
cashmove_user,"Cashmove user",model_lunch_cashmove,group_lunch_user,1,0,0,0
cashmove_manager,"Cashmove user",model_lunch_cashmove,group_lunch_manager,1,1,1,1
order_user,"Order Line user",model_lunch_order,group_lunch_user,1,1,1,1
order_manager,"Order Line user",model_lunch_order,group_lunch_manager,1,1,1,1
product_user,"Product user",model_lunch_product,group_lunch_user,1,0,0,0
product_manager,"Product user",model_lunch_product,group_lunch_manager,1,1,1,1
product_category_user,"Product category user",model_lunch_product_category,group_lunch_user,1,0,0,0
product_category_manager,"Product category user",model_lunch_product_category,group_lunch_manager,1,1,1,1
lunch_alert_access,access_lunch_alert_user,model_lunch_alert,base.group_user,1,0,0,0
lunch_alert_manager,access_lunch_alert_manager,model_lunch_alert,group_lunch_manager,1,1,1,1
lunch_cashmove_report_manager,access_lunch_cashmove_report,model_lunch_cashmove_report,base.group_user,1,0,0,0
lunch_location_user,access_lunch_location,model_lunch_location,group_lunch_user,1,1,0,0
lunch_location_manager,access_lunch_location,model_lunch_location,group_lunch_manager,1,1,1,1
lunch_topping_user,access_lunch_topping,model_lunch_topping,group_lunch_user,1,0,0,0
lunch_topping_manager,access_lunch_topping,model_lunch_topping,group_lunch_manager,1,1,1,1
lunch_supplier_user,"Lunch Supplier User Rights",model_lunch_supplier,group_lunch_user,1,0,0,0
lunch_supplier_manager,"Lunch Supplier Manager Rights",model_lunch_supplier,group_lunch_manager,1,1,1,1
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
2 cashmove_user Cashmove user model_lunch_cashmove group_lunch_user 1 0 0 0
3 cashmove_manager Cashmove user model_lunch_cashmove group_lunch_manager 1 1 1 1
4 order_user Order Line user model_lunch_order group_lunch_user 1 1 1 1
5 order_manager Order Line user model_lunch_order group_lunch_manager 1 1 1 1
6 product_user Product user model_lunch_product group_lunch_user 1 0 0 0
7 product_manager Product user model_lunch_product group_lunch_manager 1 1 1 1
8 product_category_user Product category user model_lunch_product_category group_lunch_user 1 0 0 0
9 product_category_manager Product category user model_lunch_product_category group_lunch_manager 1 1 1 1
10 lunch_alert_access access_lunch_alert_user model_lunch_alert base.group_user 1 0 0 0
11 lunch_alert_manager access_lunch_alert_manager model_lunch_alert group_lunch_manager 1 1 1 1
12 lunch_cashmove_report_manager access_lunch_cashmove_report model_lunch_cashmove_report base.group_user 1 0 0 0
13 lunch_location_user access_lunch_location model_lunch_location group_lunch_user 1 1 0 0
14 lunch_location_manager access_lunch_location model_lunch_location group_lunch_manager 1 1 1 1
15 lunch_topping_user access_lunch_topping model_lunch_topping group_lunch_user 1 0 0 0
16 lunch_topping_manager access_lunch_topping model_lunch_topping group_lunch_manager 1 1 1 1
17 lunch_supplier_user Lunch Supplier User Rights model_lunch_supplier group_lunch_user 1 0 0 0
18 lunch_supplier_manager Lunch Supplier Manager Rights model_lunch_supplier group_lunch_manager 1 1 1 1
+98
View File
@@ -0,0 +1,98 @@
<?xml version="1.0" ?>
<odoo>
<record model="ir.module.category" id="module_lunch_category">
<field name="name">Lunch</field>
<field name="description">Helps you handle your lunch needs, if you are a manager you will be able to create new products, cashmoves and to confirm or cancel orders.</field>
<field name="sequence">16</field>
</record>
<record id="group_lunch_user" model="res.groups">
<field name="name">User : Order your meal</field>
<field name="category_id" ref="base.module_category_human_resources_lunch"/>
</record>
<record id="group_lunch_manager" model="res.groups">
<field name="name">Administrator</field>
<field name="implied_ids" eval="[(4, ref('group_lunch_user'))]"/>
<field name="category_id" ref="base.module_category_human_resources_lunch"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
<data noupdate="1">
<record id="lunch_mind_your_own_food_money" model="ir.rule">
<field name="name">lunch.cashmove: do not see other people's cashmove</field>
<field name="model_id" ref="model_lunch_cashmove"/>
<field name="groups" eval="[(4, ref('group_lunch_user'))]"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
</record>
<record id="lunch_mind_other_food_money" model="ir.rule">
<field name="name">lunch.cashmove: do see other people's cashmove</field>
<field name="model_id" ref="model_lunch_cashmove"/>
<field name="groups" eval="[(4, ref('group_lunch_manager'))]"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
<record id="lunch_order_rule_delete" model="ir.rule">
<field name="name">lunch.order: Only new and cancelled order lines deleted.</field>
<field name="model_id" ref="lunch.model_lunch_order"/>
<field name="domain_force">[('state', 'in', ('new', 'cancelled'))]</field>
<field name="perm_read" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="1" />
<field name="groups" eval="[(4,ref('lunch.group_lunch_user'))]"/>
</record>
<record id="lunch_order_rule_write" model="ir.rule">
<field name="name">lunch.order: Don't change confirmed order</field>
<field name="model_id" ref="lunch.model_lunch_order"/>
<field name="domain_force">[('state', '!=', 'confirmed'), ('user_id', '=', user.id)]</field>
<field name="perm_read" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="0"/>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="lunch_order_rule_write_manager" model="ir.rule">
<field name="name">manager can do whatever</field>
<field name="model_id" ref="lunch.model_lunch_order"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="perm_read" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="0"/>
<field name="groups" eval="[(4, ref('lunch.group_lunch_manager'))]"/>
</record>
<record id="base.default_user" model="res.users">
<field name="groups_id" eval="[(4,ref('lunch.group_lunch_manager'))]"/>
</record>
<record id="ir_rule_lunch_supplier_multi_company" model="ir.rule">
<field name="name">Lunch supplier: Multi Company</field>
<field name="model_id" ref="model_lunch_supplier"/>
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
</record>
<record id="ir_rule_lunch_order_multi_company" model="ir.rule">
<field name="name">Lunch order: Multi Company</field>
<field name="model_id" ref="model_lunch_order"/>
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
</record>
<record id="ir_rule_lunch_product_multi_company" model="ir.rule">
<field name="name">Lunch product: Multi Company</field>
<field name="model_id" ref="model_lunch_product"/>
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
</record>
<record id="ir_rule_lunch_product_category_multi_company" model="ir.rule">
<field name="name">Lunch product category: Multi Company</field>
<field name="model_id" ref="model_lunch_product_category"/>
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
</record>
<record id="ir_rule_lunch_location_multi_company" model="ir.rule">
<field name="name">Lunch location: Multi Company</field>
<field name="model_id" ref="model_lunch_location"/>
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
</record>
</data>
</odoo>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M42 25c0 9.389-7.611 17-17 17S8 34.389 8 25 15.611 8 25 8s17 7.611 17 17Z" fill="#F78613"/><path d="M19.008 40.914C12.576 38.49 8 32.28 8 25c0-9.389 7.611-17 17-17 7.28 0 13.492 4.576 15.914 11.01-2.887 10.638-11.267 19.017-21.906 21.904Z" fill="#FBB945"/><path d="M19.678 15.36c0-1.808-1.52-3.33-3.19-2.639a8.338 8.338 0 0 0-4.513 4.513c-.692 1.67.831 3.19 2.64 3.19h1.063a4 4 0 0 0 4-4v-1.063Z" fill="#fff"/><path d="M8.258 42.696c-.115-.31-.012-.748.232-.976L45.304 7.136c.243-.229.533-.162.649.149.115.311.011.748-.232.976L8.907 42.846c-.244.228-.534.161-.65-.15Z" fill="#1AD3BB"/><path d="M4.036 42.658a.892.892 0 0 1 .332-.961l38.438-25.991c.293-.198.62-.088.729.244a.892.892 0 0 1-.332.961L4.764 42.902c-.293.198-.619.089-.728-.244Z" fill="#03AF89"/></svg>

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Some files were not shown because too many files have changed in this diff Show More