Начальное наполнение

This commit is contained in:
parent c3cecf6e92
commit 2db00632fd
158 changed files with 173762 additions and 0 deletions

7
__init__.py Normal file
View 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
View 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 employees preferences.
If you want to save your employees' time and avoid them to always have coins in their pockets, this module is essential.
""",
'data': [
'security/lunch_security.xml',
'security/ir.model.access.csv',
'report/lunch_cashmove_report_views.xml',
'views/lunch_templates.xml',
'views/lunch_alert_views.xml',
'views/lunch_cashmove_views.xml',
'views/lunch_location_views.xml',
'views/lunch_orders_views.xml',
'views/lunch_product_views.xml',
'views/lunch_supplier_views.xml',
'views/res_config_settings.xml',
'views/lunch_views.xml',
'data/mail_template_data.xml',
'data/lunch_data.xml',
],
'demo': ['data/lunch_demo.xml'],
'installable': True,
'application': True,
'assets': {
'web.assets_backend': [
'lunch/static/src/components/*',
'lunch/static/src/mixins/*.js',
'lunch/static/src/views/*',
'lunch/static/src/scss/lunch_view.scss',
'lunch/static/src/scss/lunch_kanban.scss',
],
'web.assets_tests': [
'lunch/static/tests/tours/*.js',
],
'web.qunit_suite_tests': [
'lunch/static/tests/lunch_kanban_tests.js',
],
},
'license': 'LGPL-3',
}

4
controllers/__init__.py Normal file
View 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
View 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
View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="lunch_location_main" model="lunch.location" forcecreate="0">
<field name="name">HQ Office</field>
</record>
<record model="lunch.product.category" id="categ_sandwich" forcecreate="0">
<field name="name">Sandwich</field>
</record>
<record id="categ_pizza" model="lunch.product.category" forcecreate="0">
<field name="name">Pizza</field>
<field name="image_1920" type="base64" file="lunch/static/img/pizza.png"/>
</record>
<record id="categ_burger" model="lunch.product.category" forcecreate="0">
<field name="name">Burger</field>
<field name="image_1920" type="base64" file="lunch/static/img/burger.png"/>
</record>
<record id="categ_drinks" model="lunch.product.category" forcecreate="0">
<field name="name">Drinks</field>
<field name="image_1920" type="base64" file="lunch/static/img/drink.png"/>
</record>
<record id="partner_hungry_dog" model="res.partner" forcecreate="0">
<field name="name">Lunch Supplier</field>
</record>
<record id="supplier_hungry_dog" model="lunch.supplier" forcecreate="0">
<field name="partner_id" ref="partner_hungry_dog"/>
<field name="available_location_ids" eval="[
(6, 0, [ref('lunch_location_main')]),
]"/>
</record>
</data>
<data>
<record id="lunch_order_action_confirm" model="ir.actions.server">
<field name="name">Lunch: Receive meals</field>
<field name="model_id" ref="model_lunch_order"/>
<field name="binding_model_id" ref="model_lunch_order"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">records.action_confirm()</field>
</record>
<record id="lunch_order_action_cancel" model="ir.actions.server">
<field name="name">Lunch: Cancel meals</field>
<field name="model_id" ref="model_lunch_order"/>
<field name="binding_model_id" ref="model_lunch_order"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">records.action_cancel()</field>
</record>
<record id="lunch_order_action_notify" model="ir.actions.server">
<field name="name">Lunch: Send notifications</field>
<field name="model_id" ref="model_lunch_order"/>
<field name="binding_model_id" ref="model_lunch_order"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">records.action_notify()</field>
</record>
</data>
</odoo>

481
data/lunch_demo.xml Normal file
View 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
View File

@ -0,0 +1,142 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<record id="lunch_order_mail_supplier" model="mail.template">
<field name="name">Lunch: Supplier Order</field>
<field name="model_id" ref="lunch.model_lunch_supplier"/>
<field name="email_from">{{ ctx['order']['email_from'] }}</field>
<field name="partner_to">{{ ctx['order']['supplier_id'] }}</field>
<field name="subject">Orders for {{ ctx['order']['company_name'] }}</field>
<field name="lang">{{ ctx.get('default_lang') }}</field>
<field name="description">Sent to vendor with the order of the day</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
<tbody>
<!-- HEADER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle">
<span style="font-size: 10px;">Lunch Order</span><br/>
</td><td valign="middle" align="right" t-if="not user.company_id.uses_default_logo">
<img t-attf-src="/logo.png?company={{ user.company_id.id }}" style="padding: 0px; margin: 0px; height: auto; width: 80px;" t-att-alt="user.company_id.name"/>
</td></tr>
<tr><td colspan="2" style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin:16px 0px 16px 0px;"/>
</td></tr>
</table>
</td>
</tr>
<!-- CONTENT -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr>
<td valign="top" style="font-size: 13px;">
<div>
<t t-set="lines" t-value="ctx.get('lines', [])"/>
<t t-set="order" t-value="ctx.get('order')"/>
<t t-set="currency" t-value="user.env['res.currency'].browse(order.get('currency_id'))"/>
<p>
Dear <t t-out="order.get('supplier_name', '')">Laurie Poiret</t>,
</p><p>
Here is, today orders for <t t-out="order.get('company_name', '')">LunchCompany</t>:
</p>
<t t-if="sites">
<br/>
<p>Location</p>
<t t-foreach="site" t-as="site">
<p><t t-out="site['name'] or ''"></t> : <t t-out="site['address'] or ''"></t></p>
</t>
<br/>
</t>
<table>
<thead>
<tr style="background-color:rgb(233,232,233);">
<th style="width: 100%; min-width: 96px; font-size: 13px;"><strong>Product</strong></th>
<th style="width: 100%; min-width: 96px; font-size: 13px;"><strong>Comments</strong></th>
<th style="width: 100%; min-width: 96px; font-size: 13px;"><strong>Person</strong></th>
<th style="width: 100%; min-width: 96px; font-size: 13px;"><strong>Site</strong></th>
<th style="width: 100%; min-width: 96px; font-size: 13px;" align="center"><strong>Qty</strong></th>
<th style="width: 100%; min-width: 96px; font-size: 13px;" align="center"><strong>Price</strong></th>
</tr>
</thead>
<tbody>
<tr t-foreach="lines" t-as="line">
<td style="width: 100%; font-size: 13px;" valign="top" t-out="line['product'] or ''">Sushi salmon</td>
<td style="width: 100%; font-size: 13px;" valign="top">
<t t-if="line['toppings']">
<t t-out="line['toppings'] or ''">Soy sauce</t>
</t>
<t t-if="line['note']">
<div style="color: rgb(173,181,189);" t-out="line['note'] or ''">With wasabi.</div>
</t>
</td>
<td style="width: 100%; font-size: 13px;" valign="top" t-out="line['username'] or ''">lap</td>
<td style="width: 100%; font-size: 13px;" valign="top" t-out="line['site'] or ''">Office 1</td>
<td style="width: 100%; font-size: 13px;" valign="top" align="right" t-out="line['quantity'] or ''">10</td>
<td style="width: 100%; font-size: 13px;" valign="top" align="right" t-out="format_amount(line['price'], currency) or ''">$ 1.00</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td style="width: 100%; font-size: 13px; border-top: 1px solid black;"><strong>Total</strong></td>
<td style="width: 100%; font-size: 13px; border-top: 1px solid black;" align="right"><strong t-out="format_amount(order['amount_total'], currency) or ''">$ 10.00</strong></td>
</tr>
</tbody>
</table>
<p>Do not hesitate to contact us if you have any questions.</p>
</div>
</td>
</tr>
<tr>
<td style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td>
</tr>
</table>
</td>
</tr>
<!-- FOOTER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle" align="left">
<t t-out="user.company_id.name or ''">YourCompany</t>
</td></tr>
<tr><td valign="middle" align="left" style="opacity: 0.7;">
<t t-out="user.company_id.phone or ''">+1 650-123-4567</t>
<t t-if="user.company_id.phone and (user.company_id.email or user.company_id.website)">|</t>
<t t-if="user.company_id.email">
<a t-attf-href="'mailto:%s' % {{ user.company_id.email }}" style="text-decoration:none; color: #454748;" t-out="user.company_id.email or ''">info@yourcompany.com</a>
</t>
<t t-if="user.company_id.email and user.company_id.website">|</t>
<t t-if="user.company_id.website">
<a t-attf-href="'%s' % {{ user.company_id.website }}" style="text-decoration:none; color: #454748;" t-out="user.company_id.website or ''">http://www.example.com</a>
</t>
</td></tr>
</table>
</td>
</tr>
</tbody>
</table>
</td></tr>
<!-- POWERED BY -->
<tr><td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: #F1F1F1; color: #454748; padding: 8px; border-collapse:separate;">
<tr><td style="text-align: center; font-size: 13px;">
Powered by <a target="_blank" href="https://www.odoo.com" style="color: #875A7B;">Odoo</a>
</td></tr>
</table>
</td></tr>
</table>
</field>
</record>
</data></odoo>

2213
i18n/af.po Normal file

File diff suppressed because it is too large Load Diff

2209
i18n/am.po Normal file

File diff suppressed because it is too large Load Diff

2412
i18n/ar.po Normal file

File diff suppressed because it is too large Load Diff

2218
i18n/az.po Normal file

File diff suppressed because it is too large Load Diff

2270
i18n/bg.po Normal file

File diff suppressed because it is too large Load Diff

2214
i18n/bs.po Normal file

File diff suppressed because it is too large Load Diff

2319
i18n/ca.po Normal file

File diff suppressed because it is too large Load Diff

2279
i18n/cs.po Normal file

File diff suppressed because it is too large Load Diff

2286
i18n/da.po Normal file

File diff suppressed because it is too large Load Diff

2424
i18n/de.po Normal file

File diff suppressed because it is too large Load Diff

2216
i18n/el.po Normal file

File diff suppressed because it is too large Load Diff

2211
i18n/en_AU.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/en_GB.po Normal file

File diff suppressed because it is too large Load Diff

2422
i18n/es.po Normal file

File diff suppressed because it is too large Load Diff

2424
i18n/es_419.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/es_BO.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/es_CL.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/es_CO.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/es_CR.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/es_DO.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/es_EC.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/es_PE.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/es_PY.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/es_VE.po Normal file

File diff suppressed because it is too large Load Diff

2436
i18n/et.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/eu.po Normal file

File diff suppressed because it is too large Load Diff

2272
i18n/fa.po Normal file

File diff suppressed because it is too large Load Diff

2311
i18n/fi.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/fo.po Normal file

File diff suppressed because it is too large Load Diff

2427
i18n/fr.po Normal file

File diff suppressed because it is too large Load Diff

2211
i18n/fr_BE.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/fr_CA.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/gl.po Normal file

File diff suppressed because it is too large Load Diff

2217
i18n/gu.po Normal file

File diff suppressed because it is too large Load Diff

2288
i18n/he.po Normal file

File diff suppressed because it is too large Load Diff

2211
i18n/hi.po Normal file

File diff suppressed because it is too large Load Diff

2227
i18n/hr.po Normal file

File diff suppressed because it is too large Load Diff

2275
i18n/hu.po Normal file

File diff suppressed because it is too large Load Diff

2422
i18n/id.po Normal file

File diff suppressed because it is too large Load Diff

2213
i18n/is.po Normal file

File diff suppressed because it is too large Load Diff

2425
i18n/it.po Normal file

File diff suppressed because it is too large Load Diff

2402
i18n/ja.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/ka.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/kab.po Normal file

File diff suppressed because it is too large Load Diff

2217
i18n/km.po Normal file

File diff suppressed because it is too large Load Diff

2396
i18n/ko.po Normal file

File diff suppressed because it is too large Load Diff

2213
i18n/lb.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/lo.po Normal file

File diff suppressed because it is too large Load Diff

2277
i18n/lt.po Normal file

File diff suppressed because it is too large Load Diff

2243
i18n/lunch.pot Normal file

File diff suppressed because it is too large Load Diff

2261
i18n/lv.po Normal file

File diff suppressed because it is too large Load Diff

2212
i18n/mk.po Normal file

File diff suppressed because it is too large Load Diff

2231
i18n/mn.po Normal file

File diff suppressed because it is too large Load Diff

2225
i18n/nb.po Normal file

File diff suppressed because it is too large Load Diff

2209
i18n/ne.po Normal file

File diff suppressed because it is too large Load Diff

2425
i18n/nl.po Normal file

File diff suppressed because it is too large Load Diff

2301
i18n/pl.po Normal file

File diff suppressed because it is too large Load Diff

2254
i18n/pt.po Normal file

File diff suppressed because it is too large Load Diff

2422
i18n/pt_BR.po Normal file

File diff suppressed because it is too large Load Diff

2231
i18n/ro.po Normal file

File diff suppressed because it is too large Load Diff

2431
i18n/ru.po Normal file

File diff suppressed because it is too large Load Diff

2268
i18n/sk.po Normal file

File diff suppressed because it is too large Load Diff

2273
i18n/sl.po Normal file

File diff suppressed because it is too large Load Diff

2218
i18n/sq.po Normal file

File diff suppressed because it is too large Load Diff

2294
i18n/sr.po Normal file

File diff suppressed because it is too large Load Diff

2216
i18n/sr@latin.po Normal file

File diff suppressed because it is too large Load Diff

2274
i18n/sv.po Normal file

File diff suppressed because it is too large Load Diff

2213
i18n/ta.po Normal file

File diff suppressed because it is too large Load Diff

2418
i18n/th.po Normal file

File diff suppressed because it is too large Load Diff

2317
i18n/tr.po Normal file

File diff suppressed because it is too large Load Diff

2425
i18n/uk.po Normal file

File diff suppressed because it is too large Load Diff

2286
i18n/vi.po Normal file

File diff suppressed because it is too large Load Diff

2403
i18n/zh_CN.po Normal file

File diff suppressed because it is too large Load Diff

2398
i18n/zh_TW.po Normal file

File diff suppressed because it is too large Load Diff

14
models/__init__.py Normal file
View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import lunch_alert
from . import lunch_cashmove
from . import lunch_location
from . import lunch_order
from . import lunch_product
from . import lunch_product_category
from . import lunch_topping
from . import lunch_supplier
from . import res_company
from . import res_config_settings
from . import res_users

201
models/lunch_alert.py Normal file
View File

@ -0,0 +1,201 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
import logging
from odoo import api, fields, models, _
from odoo.osv import expression
from .lunch_supplier import float_to_time
from datetime import datetime, timedelta
from textwrap import dedent
from odoo.addons.base.models.res_partner import _tz_get
_logger = logging.getLogger(__name__)
WEEKDAY_TO_NAME = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
CRON_DEPENDS = {'name', 'active', 'mode', 'until', 'notification_time', 'notification_moment', 'tz'}
class LunchAlert(models.Model):
""" Alerts to display during a lunch order. An alert can be specific to a
given day, weekly or daily. The alert is displayed from start to end hour. """
_name = 'lunch.alert'
_description = 'Lunch Alert'
_order = 'write_date desc, id'
name = fields.Char('Alert Name', required=True, translate=True)
message = fields.Html('Message', required=True, translate=True)
mode = fields.Selection([
('alert', 'Alert in app'),
('chat', 'Chat notification')], string='Display', default='alert')
recipients = fields.Selection([
('everyone', 'Everyone'),
('last_week', 'Employee who ordered last week'),
('last_month', 'Employee who ordered last month'),
('last_year', 'Employee who ordered last year')], string='Recipients', default='everyone')
notification_time = fields.Float(default=10.0, string='Notification Time')
notification_moment = fields.Selection([
('am', 'AM'),
('pm', 'PM')], default='am', required=True)
tz = fields.Selection(_tz_get, string='Timezone', required=True, default=lambda self: self.env.user.tz or 'UTC')
cron_id = fields.Many2one('ir.cron', ondelete='cascade', required=True, readonly=True)
until = fields.Date('Show Until')
mon = fields.Boolean(default=True)
tue = fields.Boolean(default=True)
wed = fields.Boolean(default=True)
thu = fields.Boolean(default=True)
fri = fields.Boolean(default=True)
sat = fields.Boolean(default=True)
sun = fields.Boolean(default=True)
available_today = fields.Boolean('Is Displayed Today',
compute='_compute_available_today', search='_search_available_today')
active = fields.Boolean('Active', default=True)
location_ids = fields.Many2many('lunch.location', string='Location')
_sql_constraints = [
('notification_time_range',
'CHECK(notification_time >= 0 and notification_time <= 12)',
'Notification time must be between 0 and 12')
]
@api.depends('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun')
def _compute_available_today(self):
today = fields.Date.context_today(self)
fieldname = WEEKDAY_TO_NAME[today.weekday()]
for alert in self:
alert.available_today = alert.until > today if alert.until else True and alert[fieldname]
def _search_available_today(self, operator, value):
if (not operator in ['=', '!=']) or (not value in [True, False]):
return []
searching_for_true = (operator == '=' and value) or (operator == '!=' and not value)
today = fields.Date.context_today(self)
fieldname = WEEKDAY_TO_NAME[today.weekday()]
return expression.AND([
[(fieldname, operator, value)],
expression.OR([
[('until', '=', False)],
[('until', '>' if searching_for_true else '<', today)],
])
])
def _sync_cron(self):
""" Synchronise the related cron fields to reflect this alert """
for alert in self:
alert = alert.with_context(tz=alert.tz)
cron_required = (
alert.active
and alert.mode == 'chat'
and (not alert.until or fields.Date.context_today(alert) <= alert.until)
)
sendat_tz = pytz.timezone(alert.tz).localize(datetime.combine(
fields.Date.context_today(alert, fields.Datetime.now()),
float_to_time(alert.notification_time, alert.notification_moment)))
cron = alert.cron_id.sudo()
lc = cron.lastcall
if ((
lc and sendat_tz.date() <= fields.Datetime.context_timestamp(alert, lc).date()
) or (
not lc and sendat_tz <= fields.Datetime.context_timestamp(alert, fields.Datetime.now())
)):
sendat_tz += timedelta(days=1)
sendat_utc = sendat_tz.astimezone(pytz.UTC).replace(tzinfo=None)
cron.name = f"Lunch: alert chat notification ({alert.name})"
cron.active = cron_required
cron.nextcall = sendat_utc
cron.code = dedent(f"""\
# This cron is dynamically controlled by {self._description}.
# Do NOT modify this cron, modify the related record instead.
env['{self._name}'].browse([{alert.id}])._notify_chat()""")
@api.model_create_multi
def create(self, vals_list):
crons = self.env['ir.cron'].sudo().create([
{
'user_id': self.env.ref('base.user_root').id,
'active': False,
'interval_type': 'days',
'interval_number': 1,
'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
View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.tools import float_round
class LunchCashMove(models.Model):
""" Two types of cashmoves: payment (credit) or order (debit) """
_name = 'lunch.cashmove'
_description = 'Lunch Cashmove'
_order = 'date desc'
currency_id = fields.Many2one('res.currency', default=lambda self: self.env.company.currency_id, required=True)
user_id = fields.Many2one('res.users', 'User',
default=lambda self: self.env.uid)
date = fields.Date('Date', required=True, default=fields.Date.context_today)
amount = fields.Float('Amount', required=True)
description = fields.Text('Description')
def _compute_display_name(self):
for cashmove in self:
cashmove.display_name = '{} {}'.format(_('Lunch Cashmove'), '#%d' % cashmove.id)
@api.model
def get_wallet_balance(self, user, include_config=True):
result = float_round(sum(move['amount'] for move in self.env['lunch.cashmove.report'].search_read(
[('user_id', '=', user.id)], ['amount'])), precision_digits=2)
if include_config:
result += user.company_id.lunch_minimum_threshold
return result

13
models/lunch_location.py Normal file
View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class LunchLocation(models.Model):
_name = 'lunch.location'
_description = 'Lunch Locations'
name = fields.Char('Location Name', required=True)
address = fields.Text('Address')
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)

276
models/lunch_order.py Normal file
View 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
View File

@ -0,0 +1,129 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from collections import defaultdict
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.osv import expression
class LunchProduct(models.Model):
""" Products available to order. A product is linked to a specific vendor. """
_name = 'lunch.product'
_description = 'Lunch Product'
_inherit = 'image.mixin'
_order = 'name'
_check_company_auto = True
name = fields.Char('Product Name', required=True, translate=True)
category_id = fields.Many2one('lunch.product.category', 'Product Category', check_company=True, required=True)
description = fields.Html('Description', translate=True)
price = fields.Float('Price', digits='Account', required=True)
supplier_id = fields.Many2one('lunch.supplier', 'Vendor', check_company=True, required=True)
active = fields.Boolean(default=True)
company_id = fields.Many2one('res.company', related='supplier_id.company_id', readonly=False, store=True)
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
new_until = fields.Date('New Until')
is_new = fields.Boolean(compute='_compute_is_new')
favorite_user_ids = fields.Many2many('res.users', 'lunch_product_favorite_user_rel', 'product_id', 'user_id', check_company=True)
is_favorite = fields.Boolean(compute='_compute_is_favorite', inverse='_inverse_is_favorite')
last_order_date = fields.Date(compute='_compute_last_order_date')
product_image = fields.Image(compute='_compute_product_image')
# This field is used only for searching
is_available_at = fields.Many2one('lunch.location', 'Product Availability', compute='_compute_is_available_at', search='_search_is_available_at')
@api.depends('image_128', 'category_id.image_128')
def _compute_product_image(self):
for product in self:
product.product_image = product.image_128 or product.category_id.image_128
@api.depends('new_until')
def _compute_is_new(self):
today = fields.Date.context_today(self)
for product in self:
if product.new_until:
product.is_new = today <= product.new_until
else:
product.is_new = False
@api.depends_context('uid')
@api.depends('favorite_user_ids')
def _compute_is_favorite(self):
for product in self:
product.is_favorite = self.env.user in product.favorite_user_ids
@api.depends_context('uid')
def _compute_last_order_date(self):
all_orders = self.env['lunch.order'].search([
('user_id', '=', self.env.user.id),
('product_id', 'in', self.ids),
])
mapped_orders = defaultdict(lambda: self.env['lunch.order'])
for order in all_orders:
mapped_orders[order.product_id] |= order
for product in self:
if not mapped_orders[product]:
product.last_order_date = False
else:
product.last_order_date = max(mapped_orders[product].mapped('date'))
def _compute_is_available_at(self):
"""
Is available_at is always false when browsing it
this field is there only to search (see _search_is_available_at)
"""
for product in self:
product.is_available_at = False
def _search_is_available_at(self, operator, value):
supported_operators = ['in', 'not in', '=', '!=']
if not operator in supported_operators:
return expression.TRUE_DOMAIN
if isinstance(value, int):
value = [value]
if operator in expression.NEGATIVE_TERM_OPERATORS:
return expression.AND([[('supplier_id.available_location_ids', 'not in', value)], [('supplier_id.available_location_ids', '!=', False)]])
return expression.OR([[('supplier_id.available_location_ids', 'in', value)], [('supplier_id.available_location_ids', '=', False)]])
def _sync_active_from_related(self):
""" Archive/unarchive product after related field is archived/unarchived """
return self.filtered(lambda p: (p.category_id.active and p.supplier_id.active) != p.active).toggle_active()
def toggle_active(self):
invalid_products = self.filtered(lambda product: not product.active and not product.category_id.active)
if invalid_products:
raise UserError(_("The following product categories are archived. You should either unarchive the categories or change the category of the product.\n%s", '\n'.join(invalid_products.category_id.mapped('name'))))
invalid_products = self.filtered(lambda product: not product.active and not product.supplier_id.active)
if invalid_products:
raise UserError(_("The following suppliers are archived. You should either unarchive the suppliers or change the supplier of the product.\n%s", '\n'.join(invalid_products.supplier_id.mapped('name'))))
return super().toggle_active()
def _inverse_is_favorite(self):
""" Handled in the write() """
return
def write(self, vals):
if 'is_favorite' in vals:
if vals.pop('is_favorite'):
commands = [(4, product.id) for product in self]
else:
commands = [(3, product.id) for product in self]
self.env.user.write({
'favorite_lunch_product_ids': commands,
})
if not vals:
return True
return super().write(vals)

View 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
View 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
View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.tools import formatLang
class LunchTopping(models.Model):
_name = 'lunch.topping'
_description = 'Lunch Extras'
name = fields.Char('Name', required=True)
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
price = fields.Monetary('Price', required=True)
supplier_id = fields.Many2one('lunch.supplier', ondelete='cascade')
topping_category = fields.Integer('Topping Category', required=True, default=1)
@api.depends('price')
@api.depends_context('company')
def _compute_display_name(self):
currency_id = self.env.company.currency_id
for topping in self:
price = formatLang(self.env, topping.price, currency_obj=currency_id)
topping.display_name = f'{topping.name} {price}'

13
models/res_company.py Normal file
View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
class Company(models.Model):
_inherit = 'res.company'
lunch_minimum_threshold = fields.Float()
lunch_notify_message = fields.Html(
default="""Your lunch has been delivered.
Enjoy your meal!""", translate=True)

View 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
View File

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResUsers(models.Model):
_inherit = 'res.users'
last_lunch_location_id = fields.Many2one('lunch.location')
favorite_lunch_product_ids = fields.Many2many('lunch.product', 'lunch_product_favorite_user_rel', 'user_id', 'product_id')

1
populate/__init__.py Normal file
View File

@ -0,0 +1 @@
from . import lunch

167
populate/lunch.py Normal file
View 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
View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import lunch_cashmove_report

View 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)

View 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>

View File

@ -0,0 +1,18 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
cashmove_user,"Cashmove user",model_lunch_cashmove,group_lunch_user,1,0,0,0
cashmove_manager,"Cashmove user",model_lunch_cashmove,group_lunch_manager,1,1,1,1
order_user,"Order Line user",model_lunch_order,group_lunch_user,1,1,1,1
order_manager,"Order Line user",model_lunch_order,group_lunch_manager,1,1,1,1
product_user,"Product user",model_lunch_product,group_lunch_user,1,0,0,0
product_manager,"Product user",model_lunch_product,group_lunch_manager,1,1,1,1
product_category_user,"Product category user",model_lunch_product_category,group_lunch_user,1,0,0,0
product_category_manager,"Product category user",model_lunch_product_category,group_lunch_manager,1,1,1,1
lunch_alert_access,access_lunch_alert_user,model_lunch_alert,base.group_user,1,0,0,0
lunch_alert_manager,access_lunch_alert_manager,model_lunch_alert,group_lunch_manager,1,1,1,1
lunch_cashmove_report_manager,access_lunch_cashmove_report,model_lunch_cashmove_report,base.group_user,1,0,0,0
lunch_location_user,access_lunch_location,model_lunch_location,group_lunch_user,1,1,0,0
lunch_location_manager,access_lunch_location,model_lunch_location,group_lunch_manager,1,1,1,1
lunch_topping_user,access_lunch_topping,model_lunch_topping,group_lunch_user,1,0,0,0
lunch_topping_manager,access_lunch_topping,model_lunch_topping,group_lunch_manager,1,1,1,1
lunch_supplier_user,"Lunch Supplier User Rights",model_lunch_supplier,group_lunch_user,1,0,0,0
lunch_supplier_manager,"Lunch Supplier Manager Rights",model_lunch_supplier,group_lunch_manager,1,1,1,1
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
2 cashmove_user Cashmove user model_lunch_cashmove group_lunch_user 1 0 0 0
3 cashmove_manager Cashmove user model_lunch_cashmove group_lunch_manager 1 1 1 1
4 order_user Order Line user model_lunch_order group_lunch_user 1 1 1 1
5 order_manager Order Line user model_lunch_order group_lunch_manager 1 1 1 1
6 product_user Product user model_lunch_product group_lunch_user 1 0 0 0
7 product_manager Product user model_lunch_product group_lunch_manager 1 1 1 1
8 product_category_user Product category user model_lunch_product_category group_lunch_user 1 0 0 0
9 product_category_manager Product category user model_lunch_product_category group_lunch_manager 1 1 1 1
10 lunch_alert_access access_lunch_alert_user model_lunch_alert base.group_user 1 0 0 0
11 lunch_alert_manager access_lunch_alert_manager model_lunch_alert group_lunch_manager 1 1 1 1
12 lunch_cashmove_report_manager access_lunch_cashmove_report model_lunch_cashmove_report base.group_user 1 0 0 0
13 lunch_location_user access_lunch_location model_lunch_location group_lunch_user 1 1 0 0
14 lunch_location_manager access_lunch_location model_lunch_location group_lunch_manager 1 1 1 1
15 lunch_topping_user access_lunch_topping model_lunch_topping group_lunch_user 1 0 0 0
16 lunch_topping_manager access_lunch_topping model_lunch_topping group_lunch_manager 1 1 1 1
17 lunch_supplier_user Lunch Supplier User Rights model_lunch_supplier group_lunch_user 1 0 0 0
18 lunch_supplier_manager Lunch Supplier Manager Rights model_lunch_supplier group_lunch_manager 1 1 1 1

View File

@ -0,0 +1,98 @@
<?xml version="1.0" ?>
<odoo>
<record model="ir.module.category" id="module_lunch_category">
<field name="name">Lunch</field>
<field name="description">Helps you handle your lunch needs, if you are a manager you will be able to create new products, cashmoves and to confirm or cancel orders.</field>
<field name="sequence">16</field>
</record>
<record id="group_lunch_user" model="res.groups">
<field name="name">User : Order your meal</field>
<field name="category_id" ref="base.module_category_human_resources_lunch"/>
</record>
<record id="group_lunch_manager" model="res.groups">
<field name="name">Administrator</field>
<field name="implied_ids" eval="[(4, ref('group_lunch_user'))]"/>
<field name="category_id" ref="base.module_category_human_resources_lunch"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
<data noupdate="1">
<record id="lunch_mind_your_own_food_money" model="ir.rule">
<field name="name">lunch.cashmove: do not see other people's cashmove</field>
<field name="model_id" ref="model_lunch_cashmove"/>
<field name="groups" eval="[(4, ref('group_lunch_user'))]"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
</record>
<record id="lunch_mind_other_food_money" model="ir.rule">
<field name="name">lunch.cashmove: do see other people's cashmove</field>
<field name="model_id" ref="model_lunch_cashmove"/>
<field name="groups" eval="[(4, ref('group_lunch_manager'))]"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
<record id="lunch_order_rule_delete" model="ir.rule">
<field name="name">lunch.order: Only new and cancelled order lines deleted.</field>
<field name="model_id" ref="lunch.model_lunch_order"/>
<field name="domain_force">[('state', 'in', ('new', 'cancelled'))]</field>
<field name="perm_read" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="1" />
<field name="groups" eval="[(4,ref('lunch.group_lunch_user'))]"/>
</record>
<record id="lunch_order_rule_write" model="ir.rule">
<field name="name">lunch.order: Don't change confirmed order</field>
<field name="model_id" ref="lunch.model_lunch_order"/>
<field name="domain_force">[('state', '!=', 'confirmed'), ('user_id', '=', user.id)]</field>
<field name="perm_read" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="0"/>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="lunch_order_rule_write_manager" model="ir.rule">
<field name="name">manager can do whatever</field>
<field name="model_id" ref="lunch.model_lunch_order"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="perm_read" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="0"/>
<field name="groups" eval="[(4, ref('lunch.group_lunch_manager'))]"/>
</record>
<record id="base.default_user" model="res.users">
<field name="groups_id" eval="[(4,ref('lunch.group_lunch_manager'))]"/>
</record>
<record id="ir_rule_lunch_supplier_multi_company" model="ir.rule">
<field name="name">Lunch supplier: Multi Company</field>
<field name="model_id" ref="model_lunch_supplier"/>
<field name="domain_force">['|', ('company_id', '=', 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