Начальное наполнение
This commit is contained in:
parent
c3cecf6e92
commit
2db00632fd
7
__init__.py
Normal file
7
__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import controllers
|
||||||
|
from . import models
|
||||||
|
from . import report
|
||||||
|
from . import populate
|
59
__manifest__.py
Normal file
59
__manifest__.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
{
|
||||||
|
'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 employee’s 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.qunit_suite_tests': [
|
||||||
|
'lunch/static/tests/lunch_kanban_tests.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
4
controllers/__init__.py
Normal file
4
controllers/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import main
|
156
controllers/main.py
Normal file
156
controllers/main.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
# -*- 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.price, 2), 2)),
|
||||||
|
'toppings': [(topping.name, float_repr(float_round(topping.price, 2), 2))
|
||||||
|
for topping in line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3],
|
||||||
|
'quantity': line.quantity,
|
||||||
|
'price': line.price,
|
||||||
|
'raw_state': line.state,
|
||||||
|
'state': translated_states[line.state],
|
||||||
|
'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),
|
||||||
|
'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
data/lunch_data.xml
Normal file
67
data/lunch_data.xml
Normal 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>
|
481
data/lunch_demo.xml
Normal file
481
data/lunch_demo.xml
Normal file
@ -0,0 +1,481 @@
|
|||||||
|
<?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>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="partner_pizza_inn" model="res.partner">
|
||||||
|
<field name="name">Pizza Inn</field>
|
||||||
|
<field name="city">New Delhi TN</field>
|
||||||
|
<field name="country_id" ref="base.us"/>
|
||||||
|
<field name="street">#8, 1 st Floor,iscore complex</field>
|
||||||
|
<field name="street2">Gandhi Gramam,Gandhi Nagar</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>
|
||||||
|
</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>
|
||||||
|
</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
data/mail_template_data.xml
Normal file
142
data/mail_template_data.xml
Normal 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>
|
2213
i18n/af.po
Normal file
2213
i18n/af.po
Normal file
File diff suppressed because it is too large
Load Diff
2209
i18n/am.po
Normal file
2209
i18n/am.po
Normal file
File diff suppressed because it is too large
Load Diff
2412
i18n/ar.po
Normal file
2412
i18n/ar.po
Normal file
File diff suppressed because it is too large
Load Diff
2218
i18n/az.po
Normal file
2218
i18n/az.po
Normal file
File diff suppressed because it is too large
Load Diff
2270
i18n/bg.po
Normal file
2270
i18n/bg.po
Normal file
File diff suppressed because it is too large
Load Diff
2214
i18n/bs.po
Normal file
2214
i18n/bs.po
Normal file
File diff suppressed because it is too large
Load Diff
2319
i18n/ca.po
Normal file
2319
i18n/ca.po
Normal file
File diff suppressed because it is too large
Load Diff
2279
i18n/cs.po
Normal file
2279
i18n/cs.po
Normal file
File diff suppressed because it is too large
Load Diff
2286
i18n/da.po
Normal file
2286
i18n/da.po
Normal file
File diff suppressed because it is too large
Load Diff
2424
i18n/de.po
Normal file
2424
i18n/de.po
Normal file
File diff suppressed because it is too large
Load Diff
2216
i18n/el.po
Normal file
2216
i18n/el.po
Normal file
File diff suppressed because it is too large
Load Diff
2211
i18n/en_AU.po
Normal file
2211
i18n/en_AU.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/en_GB.po
Normal file
2212
i18n/en_GB.po
Normal file
File diff suppressed because it is too large
Load Diff
2422
i18n/es.po
Normal file
2422
i18n/es.po
Normal file
File diff suppressed because it is too large
Load Diff
2424
i18n/es_419.po
Normal file
2424
i18n/es_419.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/es_BO.po
Normal file
2212
i18n/es_BO.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/es_CL.po
Normal file
2212
i18n/es_CL.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/es_CO.po
Normal file
2212
i18n/es_CO.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/es_CR.po
Normal file
2212
i18n/es_CR.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/es_DO.po
Normal file
2212
i18n/es_DO.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/es_EC.po
Normal file
2212
i18n/es_EC.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/es_PE.po
Normal file
2212
i18n/es_PE.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/es_PY.po
Normal file
2212
i18n/es_PY.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/es_VE.po
Normal file
2212
i18n/es_VE.po
Normal file
File diff suppressed because it is too large
Load Diff
2436
i18n/et.po
Normal file
2436
i18n/et.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/eu.po
Normal file
2212
i18n/eu.po
Normal file
File diff suppressed because it is too large
Load Diff
2272
i18n/fa.po
Normal file
2272
i18n/fa.po
Normal file
File diff suppressed because it is too large
Load Diff
2311
i18n/fi.po
Normal file
2311
i18n/fi.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/fo.po
Normal file
2212
i18n/fo.po
Normal file
File diff suppressed because it is too large
Load Diff
2427
i18n/fr.po
Normal file
2427
i18n/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
2211
i18n/fr_BE.po
Normal file
2211
i18n/fr_BE.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/fr_CA.po
Normal file
2212
i18n/fr_CA.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/gl.po
Normal file
2212
i18n/gl.po
Normal file
File diff suppressed because it is too large
Load Diff
2217
i18n/gu.po
Normal file
2217
i18n/gu.po
Normal file
File diff suppressed because it is too large
Load Diff
2288
i18n/he.po
Normal file
2288
i18n/he.po
Normal file
File diff suppressed because it is too large
Load Diff
2211
i18n/hi.po
Normal file
2211
i18n/hi.po
Normal file
File diff suppressed because it is too large
Load Diff
2227
i18n/hr.po
Normal file
2227
i18n/hr.po
Normal file
File diff suppressed because it is too large
Load Diff
2275
i18n/hu.po
Normal file
2275
i18n/hu.po
Normal file
File diff suppressed because it is too large
Load Diff
2422
i18n/id.po
Normal file
2422
i18n/id.po
Normal file
File diff suppressed because it is too large
Load Diff
2213
i18n/is.po
Normal file
2213
i18n/is.po
Normal file
File diff suppressed because it is too large
Load Diff
2425
i18n/it.po
Normal file
2425
i18n/it.po
Normal file
File diff suppressed because it is too large
Load Diff
2402
i18n/ja.po
Normal file
2402
i18n/ja.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/ka.po
Normal file
2212
i18n/ka.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/kab.po
Normal file
2212
i18n/kab.po
Normal file
File diff suppressed because it is too large
Load Diff
2217
i18n/km.po
Normal file
2217
i18n/km.po
Normal file
File diff suppressed because it is too large
Load Diff
2396
i18n/ko.po
Normal file
2396
i18n/ko.po
Normal file
File diff suppressed because it is too large
Load Diff
2213
i18n/lb.po
Normal file
2213
i18n/lb.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/lo.po
Normal file
2212
i18n/lo.po
Normal file
File diff suppressed because it is too large
Load Diff
2277
i18n/lt.po
Normal file
2277
i18n/lt.po
Normal file
File diff suppressed because it is too large
Load Diff
2243
i18n/lunch.pot
Normal file
2243
i18n/lunch.pot
Normal file
File diff suppressed because it is too large
Load Diff
2261
i18n/lv.po
Normal file
2261
i18n/lv.po
Normal file
File diff suppressed because it is too large
Load Diff
2212
i18n/mk.po
Normal file
2212
i18n/mk.po
Normal file
File diff suppressed because it is too large
Load Diff
2231
i18n/mn.po
Normal file
2231
i18n/mn.po
Normal file
File diff suppressed because it is too large
Load Diff
2225
i18n/nb.po
Normal file
2225
i18n/nb.po
Normal file
File diff suppressed because it is too large
Load Diff
2209
i18n/ne.po
Normal file
2209
i18n/ne.po
Normal file
File diff suppressed because it is too large
Load Diff
2425
i18n/nl.po
Normal file
2425
i18n/nl.po
Normal file
File diff suppressed because it is too large
Load Diff
2301
i18n/pl.po
Normal file
2301
i18n/pl.po
Normal file
File diff suppressed because it is too large
Load Diff
2254
i18n/pt.po
Normal file
2254
i18n/pt.po
Normal file
File diff suppressed because it is too large
Load Diff
2422
i18n/pt_BR.po
Normal file
2422
i18n/pt_BR.po
Normal file
File diff suppressed because it is too large
Load Diff
2231
i18n/ro.po
Normal file
2231
i18n/ro.po
Normal file
File diff suppressed because it is too large
Load Diff
2431
i18n/ru.po
Normal file
2431
i18n/ru.po
Normal file
File diff suppressed because it is too large
Load Diff
2268
i18n/sk.po
Normal file
2268
i18n/sk.po
Normal file
File diff suppressed because it is too large
Load Diff
2273
i18n/sl.po
Normal file
2273
i18n/sl.po
Normal file
File diff suppressed because it is too large
Load Diff
2218
i18n/sq.po
Normal file
2218
i18n/sq.po
Normal file
File diff suppressed because it is too large
Load Diff
2294
i18n/sr.po
Normal file
2294
i18n/sr.po
Normal file
File diff suppressed because it is too large
Load Diff
2216
i18n/sr@latin.po
Normal file
2216
i18n/sr@latin.po
Normal file
File diff suppressed because it is too large
Load Diff
2274
i18n/sv.po
Normal file
2274
i18n/sv.po
Normal file
File diff suppressed because it is too large
Load Diff
2213
i18n/ta.po
Normal file
2213
i18n/ta.po
Normal file
File diff suppressed because it is too large
Load Diff
2418
i18n/th.po
Normal file
2418
i18n/th.po
Normal file
File diff suppressed because it is too large
Load Diff
2317
i18n/tr.po
Normal file
2317
i18n/tr.po
Normal file
File diff suppressed because it is too large
Load Diff
2425
i18n/uk.po
Normal file
2425
i18n/uk.po
Normal file
File diff suppressed because it is too large
Load Diff
2286
i18n/vi.po
Normal file
2286
i18n/vi.po
Normal file
File diff suppressed because it is too large
Load Diff
2403
i18n/zh_CN.po
Normal file
2403
i18n/zh_CN.po
Normal file
File diff suppressed because it is too large
Load Diff
2398
i18n/zh_TW.po
Normal file
2398
i18n/zh_TW.po
Normal file
File diff suppressed because it is too large
Load Diff
14
models/__init__.py
Normal file
14
models/__init__.py
Normal 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
models/lunch_alert.py
Normal file
201
models/lunch_alert.py
Normal 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,
|
||||||
|
'numbercall': -1,
|
||||||
|
'doall': False,
|
||||||
|
'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(
|
||||||
|
body=self.message,
|
||||||
|
partner_ids=partners.ids,
|
||||||
|
subject=_('Your Lunch Order'),
|
||||||
|
)
|
31
models/lunch_cashmove.py
Normal file
31
models/lunch_cashmove.py
Normal 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
models/lunch_location.py
Normal file
13
models/lunch_location.py
Normal 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)
|
276
models/lunch_order.py
Normal file
276
models/lunch_order.py
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import ValidationError, UserError
|
||||||
|
|
||||||
|
|
||||||
|
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')
|
||||||
|
order_deadline_passed = fields.Boolean(related='supplier_id.order_deadline_passed')
|
||||||
|
user_id = fields.Many2one('res.users', 'User', readonly=False,
|
||||||
|
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')
|
||||||
|
|
||||||
|
@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_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
|
||||||
|
|
||||||
|
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 _extract_toppings(self, values):
|
||||||
|
"""
|
||||||
|
If called in api.multi then it will pop topping_ids_1,2,3 from values
|
||||||
|
"""
|
||||||
|
topping_1_values = values.get('topping_ids_1', False)
|
||||||
|
topping_2_values = values.get('topping_ids_2', False)
|
||||||
|
topping_3_values = values.get('topping_ids_3', 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_1 = values.pop('topping_ids_1')[0][2] if topping_1_values else self[:1].topping_ids_1.ids
|
||||||
|
topping_2 = values.pop('topping_ids_2')[0][2] if topping_2_values else self[:1].topping_ids_2.ids
|
||||||
|
topping_3 = values.pop('topping_ids_3')[0][2] if topping_3_values else self[:1].topping_ids_3.ids
|
||||||
|
else:
|
||||||
|
topping_1 = values['topping_ids_1'][0][2] if topping_1_values else []
|
||||||
|
topping_2 = values['topping_ids_2'][0][2] if topping_2_values else []
|
||||||
|
topping_3 = values['topping_ids_3'][0][2] if topping_3_values else []
|
||||||
|
|
||||||
|
return topping_1 + topping_2 + topping_3
|
||||||
|
|
||||||
|
@api.constrains('topping_ids_1', 'topping_ids_2', 'topping_ids_3')
|
||||||
|
def _check_topping_quantity(self):
|
||||||
|
errors = {
|
||||||
|
'1_more': _('You should order at least one %s'),
|
||||||
|
'1': _('You have to order one and only one %s'),
|
||||||
|
}
|
||||||
|
for 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),
|
||||||
|
})
|
||||||
|
if lines.filtered(lambda l: l.state not in ['sent', 'confirmed']):
|
||||||
|
# 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):
|
||||||
|
merge_needed = 'note' in values or 'topping_ids_1' in values or 'topping_ids_2' in values or 'topping_ids_3' 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:
|
||||||
|
# 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)
|
||||||
|
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),
|
||||||
|
})
|
||||||
|
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', '=', fields.Date.today()),
|
||||||
|
('note', '=', values.get('note', False)),
|
||||||
|
('lunch_location_id', '=', values.get('lunch_location_id', default_location_id)),
|
||||||
|
]
|
||||||
|
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.supplier_id.available_today:
|
||||||
|
raise UserError(_('The vendor related to this order is not available today.'))
|
||||||
|
if self.filtered(lambda line: not line.product_id.active):
|
||||||
|
raise ValidationError(_('Product is no longer available.'))
|
||||||
|
self.write({
|
||||||
|
'state': 'ordered',
|
||||||
|
})
|
||||||
|
for order in self:
|
||||||
|
order.lunch_location_id = order.user_id.last_lunch_location_id
|
||||||
|
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
models/lunch_product.py
Normal file
129
models/lunch_product.py
Normal 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)
|
40
models/lunch_product_category.py
Normal file
40
models/lunch_product_category.py
Normal file
@ -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
|
383
models/lunch_supplier.py
Normal file
383
models/lunch_supplier.py
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
# 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.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,
|
||||||
|
'numbercall': -1,
|
||||||
|
'doall': False,
|
||||||
|
'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 topping_values:
|
||||||
|
topping_values.update({'topping_category': 2})
|
||||||
|
for topping in values.get('topping_ids_3', []):
|
||||||
|
topping_values = topping[2]
|
||||||
|
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()
|
||||||
|
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 _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 ValueError("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:
|
||||||
|
now = now.astimezone(pytz.timezone(supplier.tz))
|
||||||
|
|
||||||
|
supplier.available_today = supplier._available_on_date(now)
|
||||||
|
|
||||||
|
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
models/lunch_topping.py
Normal file
26
models/lunch_topping.py
Normal 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
models/res_company.py
Normal file
13
models/res_company.py
Normal 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)
|
12
models/res_config_settings.py
Normal file
12
models/res_config_settings.py
Normal file
@ -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
models/res_users.py
Normal file
11
models/res_users.py
Normal 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')
|
1
populate/__init__.py
Normal file
1
populate/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import lunch
|
167
populate/lunch.py
Normal file
167
populate/lunch.py
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
import logging
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from itertools import groupby
|
||||||
|
|
||||||
|
from odoo import models
|
||||||
|
from odoo.tools import populate
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LunchProductCategory(models.Model):
|
||||||
|
_inherit = 'lunch.product.category'
|
||||||
|
_populate_sizes = {'small': 5, 'medium': 150, 'large': 400}
|
||||||
|
_populate_dependencies = ['res.company']
|
||||||
|
|
||||||
|
def _populate_factories(self):
|
||||||
|
# TODO topping_ids_{1,2,3}, toppping_label_{1,2,3}, topping_quantity{1,2,3}
|
||||||
|
company_ids = self.env.registry.populated_models['res.company']
|
||||||
|
|
||||||
|
return [
|
||||||
|
('name', populate.constant('lunch_product_category_{counter}')),
|
||||||
|
('company_id', populate.iterate(
|
||||||
|
[False, self.env.ref('base.main_company').id] + company_ids,
|
||||||
|
[1, 1] + [2/(len(company_ids) or 1)]*len(company_ids))),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class LunchProduct(models.Model):
|
||||||
|
_inherit = 'lunch.product'
|
||||||
|
_populate_sizes = {'small': 10, 'medium': 150, 'large': 10000}
|
||||||
|
_populate_dependencies = ['lunch.product.category', 'lunch.supplier']
|
||||||
|
|
||||||
|
def _populate_factories(self):
|
||||||
|
category_ids = self.env.registry.populated_models['lunch.product.category']
|
||||||
|
category_records = self.env['lunch.product.category'].browse(category_ids)
|
||||||
|
category_by_company = {k: list(v) for k, v in groupby(category_records, key=lambda rec: rec['company_id'].id)}
|
||||||
|
|
||||||
|
supplier_ids = self.env.registry.populated_models['lunch.supplier']
|
||||||
|
company_by_supplier = {rec.id: rec.company_id.id for rec in self.env['lunch.supplier'].browse(supplier_ids)}
|
||||||
|
|
||||||
|
def get_category(random=None, values=None, **kwargs):
|
||||||
|
company_id = company_by_supplier[values['supplier_id']]
|
||||||
|
return random.choice(category_by_company[company_id]).id
|
||||||
|
|
||||||
|
return [
|
||||||
|
('active', populate.iterate([True, False], [0.9, 0.1])),
|
||||||
|
('name', populate.constant('lunch_product_{counter}')),
|
||||||
|
('price', populate.randfloat(0.1, 50)),
|
||||||
|
('supplier_id', populate.randomize(supplier_ids)),
|
||||||
|
('category_id', populate.compute(get_category)),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class LunchLocation(models.Model):
|
||||||
|
_inherit = 'lunch.location'
|
||||||
|
|
||||||
|
_populate_sizes = {'small': 3, 'medium': 50, 'large': 500}
|
||||||
|
_populate_dependencies = ['res.company']
|
||||||
|
|
||||||
|
def _populate_factories(self):
|
||||||
|
company_ids = self.env.registry.populated_models['res.company']
|
||||||
|
|
||||||
|
return [
|
||||||
|
('name', populate.constant('lunch_location_{counter}')),
|
||||||
|
('address', populate.constant('lunch_address_location_{counter}')),
|
||||||
|
('company_id', populate.randomize(company_ids))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class LunchSupplier(models.Model):
|
||||||
|
_inherit = 'lunch.supplier'
|
||||||
|
|
||||||
|
_populate_sizes = {'small': 3, 'medium': 50, 'large': 1500}
|
||||||
|
|
||||||
|
_populate_dependencies = ['lunch.location', 'res.partner', 'res.users']
|
||||||
|
|
||||||
|
def _populate_factories(self):
|
||||||
|
|
||||||
|
location_ids = self.env.registry.populated_models['lunch.location']
|
||||||
|
partner_ids = self.env.registry.populated_models['res.partner']
|
||||||
|
user_ids = self.env.registry.populated_models['res.users']
|
||||||
|
|
||||||
|
def get_location_ids(random=None, **kwargs):
|
||||||
|
nb_locations = random.randint(0, len(location_ids))
|
||||||
|
return [(6, 0, random.choices(location_ids, k=nb_locations))]
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
('active', populate.cartesian([True, False])),
|
||||||
|
('send_by', populate.cartesian(['phone', 'mail'])),
|
||||||
|
('delivery', populate.cartesian(['delivery', 'no_delivery'])),
|
||||||
|
('mon', populate.iterate([True, False], [0.9, 0.1])),
|
||||||
|
('tue', populate.iterate([True, False], [0.9, 0.1])),
|
||||||
|
('wed', populate.iterate([True, False], [0.9, 0.1])),
|
||||||
|
('thu', populate.iterate([True, False], [0.9, 0.1])),
|
||||||
|
('fri', populate.iterate([True, False], [0.9, 0.1])),
|
||||||
|
('sat', populate.iterate([False, True], [0.9, 0.1])),
|
||||||
|
('sun', populate.iterate([False, True], [0.9, 0.1])),
|
||||||
|
('available_location_ids', populate.iterate(
|
||||||
|
[[], [(6, 0, location_ids)]],
|
||||||
|
then=populate.compute(get_location_ids))),
|
||||||
|
('partner_id', populate.randomize(partner_ids)),
|
||||||
|
('responsible_id', populate.randomize(user_ids)),
|
||||||
|
('moment', populate.iterate(['am', 'pm'])),
|
||||||
|
('automatic_email_time', populate.randfloat(0, 12)),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class LunchOrder(models.Model):
|
||||||
|
_inherit = 'lunch.order'
|
||||||
|
_populate_sizes = {'small': 20, 'medium': 3000, 'large': 15000}
|
||||||
|
_populate_dependencies = ['lunch.product', 'res.users', 'res.company']
|
||||||
|
|
||||||
|
def _populate_factories(self):
|
||||||
|
# TODO topping_ids_{1,2,3}, topping_label_{1,3}, topping_quantity_{1,3}
|
||||||
|
user_ids = self.env.registry.populated_models['res.users']
|
||||||
|
product_ids = self.env.registry.populated_models['lunch.product']
|
||||||
|
company_ids = self.env.registry.populated_models['res.company']
|
||||||
|
|
||||||
|
return [
|
||||||
|
('active', populate.cartesian([True, False])),
|
||||||
|
('state', populate.cartesian(['new', 'confirmed', 'ordered', 'cancelled'])),
|
||||||
|
('product_id', populate.randomize(product_ids)),
|
||||||
|
('user_id', populate.randomize(user_ids)),
|
||||||
|
('note', populate.constant('lunch_note_{counter}')),
|
||||||
|
('company_id', populate.randomize(company_ids)),
|
||||||
|
('quantity', populate.randint(0, 10)),
|
||||||
|
('date', populate.randdatetime(relative_before=relativedelta(months=-3), relative_after=relativedelta(months=3))),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class LunchAlert(models.Model):
|
||||||
|
_inherit = 'lunch.alert'
|
||||||
|
_populate_sizes = {'small': 10, 'medium': 40, 'large': 150}
|
||||||
|
|
||||||
|
_populate_dependencies = ['lunch.location']
|
||||||
|
|
||||||
|
def _populate_factories(self):
|
||||||
|
|
||||||
|
location_ids = self.env.registry.populated_models['lunch.location']
|
||||||
|
|
||||||
|
def get_location_ids(random=None, **kwargs):
|
||||||
|
nb_max = len(location_ids)
|
||||||
|
start = random.randint(0, nb_max)
|
||||||
|
end = random.randint(start, nb_max)
|
||||||
|
return location_ids[start:end]
|
||||||
|
|
||||||
|
return [
|
||||||
|
('active', populate.cartesian([True, False])),
|
||||||
|
('recipients', populate.cartesian(['everyone', 'last_week', 'last_month', 'last_year'])),
|
||||||
|
('mode', populate.iterate(['alert', 'chat'])),
|
||||||
|
('mon', populate.iterate([True, False], [0.9, 0.1])),
|
||||||
|
('tue', populate.iterate([True, False], [0.9, 0.1])),
|
||||||
|
('wed', populate.iterate([True, False], [0.9, 0.1])),
|
||||||
|
('thu', populate.iterate([True, False], [0.9, 0.1])),
|
||||||
|
('fri', populate.iterate([True, False], [0.9, 0.1])),
|
||||||
|
('sat', populate.iterate([False, True], [0.9, 0.1])),
|
||||||
|
('sun', populate.iterate([False, True], [0.9, 0.1])),
|
||||||
|
('name', populate.constant('alert_{counter}')),
|
||||||
|
('message', populate.constant('<strong>alert message {counter}</strong>')),
|
||||||
|
('notification_time', populate.randfloat(0, 12)),
|
||||||
|
('notification_moment', populate.iterate(['am', 'pm'])),
|
||||||
|
('until', populate.randdatetime(relative_before=relativedelta(years=-2), relative_after=relativedelta(years=2))),
|
||||||
|
('location_ids', populate.compute(get_location_ids))
|
||||||
|
]
|
4
report/__init__.py
Normal file
4
report/__init__.py
Normal 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
|
51
report/lunch_cashmove_report.py
Normal file
51
report/lunch_cashmove_report.py
Normal file
@ -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)
|
155
report/lunch_cashmove_report_views.xml
Normal file
155
report/lunch_cashmove_report_views.xml
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="lunch_cashmove_report_view_search" model="ir.ui.view">
|
||||||
|
<field name='name'>lunch.cashmove.report.search</field>
|
||||||
|
<field name='model'>lunch.cashmove.report</field>
|
||||||
|
<field name='arch' type='xml'>
|
||||||
|
<search string="lunch employee payment">
|
||||||
|
<field name="description"/>
|
||||||
|
<field name="user_id"/>
|
||||||
|
<filter name='is_payment' string="Payment" domain="[('amount', '>', 0)]"/>
|
||||||
|
<separator/>
|
||||||
|
<filter name='is_mine_group' string="My Account grouped" domain="[('user_id','=',uid)]" context="{'group_by':'user_id'}"/>
|
||||||
|
<filter name="group_by_user" string="By User" context="{'group_by':'user_id'}"/>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="lunch_cashmove_report_view_search_2" model="ir.ui.view">
|
||||||
|
<field name='name'>lunch.cashmove.report.search</field>
|
||||||
|
<field name='model'>lunch.cashmove.report</field>
|
||||||
|
<field name='arch' type='xml'>
|
||||||
|
<search string="lunch cashmove">
|
||||||
|
<field name="description"/>
|
||||||
|
<field name="user_id"/>
|
||||||
|
<group expand="0" string="Group By">
|
||||||
|
<filter name='group_by_user' string="By Employee" context="{'group_by':'user_id'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="lunch_cashmove_report_view_tree" model="ir.ui.view">
|
||||||
|
<field name="name">lunch.cashmove.report.tree</field>
|
||||||
|
<field name="model">lunch.cashmove.report</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="cashmove tree">
|
||||||
|
<field name="currency_id" column_invisible="True"/>
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="user_id" widget="many2one_avatar_user"/>
|
||||||
|
<field name="description"/>
|
||||||
|
<field name="amount" sum="Total" widget="monetary"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="lunch_cashmove_report_view_tree_2" model="ir.ui.view">
|
||||||
|
<field name="name">lunch.cashmove.report.tree</field>
|
||||||
|
<field name="model">lunch.cashmove.report</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="cashmove tree" create='false'>
|
||||||
|
<field name="currency_id" column_invisible="True"/>
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="description"/>
|
||||||
|
<field name="amount" sum="Total" widget="monetary"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="lunch_cashmove_report_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">lunch.cashmove.report.form</field>
|
||||||
|
<field name="model">lunch.cashmove.report</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="cashmove form">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="currency_id" invisible="1"/>
|
||||||
|
<field name="user_id" required="1" 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="date"/>
|
||||||
|
<field name="user_id"/>
|
||||||
|
<field name="description"/>
|
||||||
|
<field name="amount"/>
|
||||||
|
<field name="currency_id" invisible="1"/>
|
||||||
|
<templates>
|
||||||
|
<t t-name="kanban-box">
|
||||||
|
<div t-attf-class="oe_kanban_global_click">
|
||||||
|
<div class="row mb4">
|
||||||
|
<div class="col-8">
|
||||||
|
<span>
|
||||||
|
<strong class="o_kanban_record_title"><t t-esc="record.description.value"/></strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 text-end">
|
||||||
|
<span class="badge rounded-pill">
|
||||||
|
<strong><i class="fa fa-money" role="img" aria-label="Amount" title="Amount"/> <field name="amount" widget="monetary"/></strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<i class="fa fa-clock-o" role="img" aria-label="Date" title="Date"/>
|
||||||
|
<t t-esc="record.date.value"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="float-end">
|
||||||
|
<field name="user_id" widget="many2one_avatar_user"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
</kanban>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="lunch_cashmove_report_action_account" model="ir.actions.act_window">
|
||||||
|
<field name="name">My Account</field>
|
||||||
|
<field name="res_model">lunch.cashmove.report</field>
|
||||||
|
<field name="view_mode">tree</field>
|
||||||
|
<field name="search_view_id" ref="lunch_cashmove_report_view_search"/>
|
||||||
|
<field name="domain">[('user_id','=',uid)]</field>
|
||||||
|
<field name="view_id" ref="lunch_cashmove_report_view_tree_2"/>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_empty_folder">
|
||||||
|
No cash move yet
|
||||||
|
</p><p>
|
||||||
|
Here you can see your cash moves.<br/>A cash move can either be an expense or a payment.
|
||||||
|
An expense is automatically created when an order is received while a payment is a reimbursement to the company encoded by the manager.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="lunch_cashmove_report_action_control_accounts" model="ir.actions.act_window">
|
||||||
|
<field name="name">Control Accounts</field>
|
||||||
|
<field name="res_model">lunch.cashmove.report</field>
|
||||||
|
<field name="view_mode">tree,kanban,form</field>
|
||||||
|
<field name="search_view_id" ref="lunch_cashmove_report_view_search_2"/>
|
||||||
|
<field name="context">{"search_default_group_by_user":1}</field>
|
||||||
|
<field name="view_id" ref="lunch_cashmove_report_view_tree"/>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Create a new payment
|
||||||
|
</p><p>
|
||||||
|
A cashmove can either be an expense or a payment.<br/>
|
||||||
|
An expense is automatically created at the order receipt.<br/>
|
||||||
|
A payment represents the employee reimbursement to the company.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
18
security/ir.model.access.csv
Normal file
18
security/ir.model.access.csv
Normal 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
|
|
98
security/lunch_security.xml
Normal file
98
security/lunch_security.xml
Normal 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', '=', False), ('company_id', 'in', company_ids)]</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', '=', False), ('company_id', 'in', company_ids)]</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', '=', False), ('company_id', 'in', company_ids)]</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', '=', False), ('company_id', 'in', company_ids)]</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', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user