mirror of
https://github.com/bringout/oca-ocb-vertical-industry.git
synced 2026-04-23 16:52:01 +02:00
Initial commit: Vertical Industry packages
This commit is contained in:
commit
d5567a0017
766 changed files with 733028 additions and 0 deletions
14
odoo-bringout-oca-ocb-lunch/lunch/models/__init__.py
Normal file
14
odoo-bringout-oca-ocb-lunch/lunch/models/__init__.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import lunch_alert
|
||||
from . import lunch_cashmove
|
||||
from . import lunch_location
|
||||
from . import lunch_order
|
||||
from . import lunch_product
|
||||
from . import lunch_product_category
|
||||
from . import lunch_topping
|
||||
from . import lunch_supplier
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import res_users
|
||||
198
odoo-bringout-oca-ocb-lunch/lunch/models/lunch_alert.py
Normal file
198
odoo-bringout-oca-ocb-lunch/lunch/models/lunch_alert.py
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
# 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):
|
||||
super().write(values)
|
||||
if not CRON_DEPENDS.isdisjoint(values):
|
||||
self._sync_cron()
|
||||
|
||||
def unlink(self):
|
||||
crons = self.cron_id.sudo()
|
||||
server_actions = crons.ir_actions_server_id
|
||||
super().unlink()
|
||||
crons.unlink()
|
||||
server_actions.unlink()
|
||||
|
||||
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
|
||||
)
|
||||
30
odoo-bringout-oca-ocb-lunch/lunch/models/lunch_cashmove.py
Normal file
30
odoo-bringout-oca-ocb-lunch/lunch/models/lunch_cashmove.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# -*- 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 name_get(self):
|
||||
return [(cashmove.id, '%s %s' % (_('Lunch Cashmove'), '#%d' % cashmove.id)) for cashmove in self]
|
||||
|
||||
@api.model
|
||||
def get_wallet_balance(self, user, include_config=True):
|
||||
result = float_round(sum(move['amount'] for move in self.env['lunch.cashmove.report'].search_read(
|
||||
[('user_id', '=', user.id)], ['amount'])), precision_digits=2)
|
||||
if include_config:
|
||||
result += user.company_id.lunch_minimum_threshold
|
||||
return result
|
||||
13
odoo-bringout-oca-ocb-lunch/lunch/models/lunch_location.py
Normal file
13
odoo-bringout-oca-ocb-lunch/lunch/models/lunch_location.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class LunchLocation(models.Model):
|
||||
_name = 'lunch.location'
|
||||
_description = 'Lunch Locations'
|
||||
|
||||
name = fields.Char('Location Name', required=True)
|
||||
address = fields.Text('Address')
|
||||
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
|
||||
275
odoo-bringout-oca-ocb-lunch/lunch/models/lunch_order.py
Normal file
275
odoo-bringout-oca-ocb-lunch/lunch/models/lunch_order.py
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
# -*- 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", store=True, 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=True,
|
||||
states={'new': [('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=True,
|
||||
states={'new': [('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
|
||||
"""
|
||||
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_ids_1' in values else self[:1].topping_ids_1.ids
|
||||
topping_2 = values.pop('topping_ids_2')[0][2] if 'topping_ids_2' in values else self[:1].topping_ids_2.ids
|
||||
topping_3 = values.pop('topping_ids_3')[0][2] if 'topping_ids_3' in values else self[:1].topping_ids_3.ids
|
||||
else:
|
||||
topping_1 = values['topping_ids_1'][0][2] if 'topping_ids_1' in values else []
|
||||
topping_2 = values['topping_ids_2'][0][2] if 'topping_ids_2' in values else []
|
||||
topping_3 = values['topping_ids_3'][0][2] if 'topping_ids_3' in 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
odoo-bringout-oca-ocb-lunch/lunch/models/lunch_product.py
Normal file
129
odoo-bringout-oca-ocb-lunch/lunch/models/lunch_product.py
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
class LunchProduct(models.Model):
|
||||
""" Products available to order. A product is linked to a specific vendor. """
|
||||
_name = 'lunch.product'
|
||||
_description = 'Lunch Product'
|
||||
_inherit = 'image.mixin'
|
||||
_order = 'name'
|
||||
_check_company_auto = True
|
||||
|
||||
name = fields.Char('Product Name', required=True, translate=True)
|
||||
category_id = fields.Many2one('lunch.product.category', 'Product Category', check_company=True, required=True)
|
||||
description = fields.Html('Description', translate=True)
|
||||
price = fields.Float('Price', digits='Account', required=True)
|
||||
supplier_id = fields.Many2one('lunch.supplier', 'Vendor', check_company=True, required=True)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
company_id = fields.Many2one('res.company', related='supplier_id.company_id', readonly=False, store=True)
|
||||
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
|
||||
|
||||
new_until = fields.Date('New Until')
|
||||
is_new = fields.Boolean(compute='_compute_is_new')
|
||||
|
||||
favorite_user_ids = fields.Many2many('res.users', 'lunch_product_favorite_user_rel', 'product_id', 'user_id', check_company=True)
|
||||
is_favorite = fields.Boolean(compute='_compute_is_favorite', inverse='_inverse_is_favorite')
|
||||
|
||||
last_order_date = fields.Date(compute='_compute_last_order_date')
|
||||
|
||||
product_image = fields.Image(compute='_compute_product_image')
|
||||
# This field is used only for searching
|
||||
is_available_at = fields.Many2one('lunch.location', 'Product Availability', compute='_compute_is_available_at', search='_search_is_available_at')
|
||||
|
||||
@api.depends('image_128', 'category_id.image_128')
|
||||
def _compute_product_image(self):
|
||||
for product in self:
|
||||
product.product_image = product.image_128 or product.category_id.image_128
|
||||
|
||||
@api.depends('new_until')
|
||||
def _compute_is_new(self):
|
||||
today = fields.Date.context_today(self)
|
||||
for product in self:
|
||||
if product.new_until:
|
||||
product.is_new = today <= product.new_until
|
||||
else:
|
||||
product.is_new = False
|
||||
|
||||
@api.depends_context('uid')
|
||||
@api.depends('favorite_user_ids')
|
||||
def _compute_is_favorite(self):
|
||||
for product in self:
|
||||
product.is_favorite = self.env.user in product.favorite_user_ids
|
||||
|
||||
@api.depends_context('uid')
|
||||
def _compute_last_order_date(self):
|
||||
all_orders = self.env['lunch.order'].search([
|
||||
('user_id', '=', self.env.user.id),
|
||||
('product_id', 'in', self.ids),
|
||||
])
|
||||
mapped_orders = defaultdict(lambda: self.env['lunch.order'])
|
||||
for order in all_orders:
|
||||
mapped_orders[order.product_id] |= order
|
||||
for product in self:
|
||||
if not mapped_orders[product]:
|
||||
product.last_order_date = False
|
||||
else:
|
||||
product.last_order_date = max(mapped_orders[product].mapped('date'))
|
||||
|
||||
def _compute_is_available_at(self):
|
||||
"""
|
||||
Is available_at is always false when browsing it
|
||||
this field is there only to search (see _search_is_available_at)
|
||||
"""
|
||||
for product in self:
|
||||
product.is_available_at = False
|
||||
|
||||
def _search_is_available_at(self, operator, value):
|
||||
supported_operators = ['in', 'not in', '=', '!=']
|
||||
|
||||
if not operator in supported_operators:
|
||||
return expression.TRUE_DOMAIN
|
||||
|
||||
if isinstance(value, int):
|
||||
value = [value]
|
||||
|
||||
if operator in expression.NEGATIVE_TERM_OPERATORS:
|
||||
return expression.AND([[('supplier_id.available_location_ids', 'not in', value)], [('supplier_id.available_location_ids', '!=', False)]])
|
||||
|
||||
return expression.OR([[('supplier_id.available_location_ids', 'in', value)], [('supplier_id.available_location_ids', '=', False)]])
|
||||
|
||||
def _sync_active_from_related(self):
|
||||
""" Archive/unarchive product after related field is archived/unarchived """
|
||||
return self.filtered(lambda p: (p.category_id.active and p.supplier_id.active) != p.active).toggle_active()
|
||||
|
||||
def toggle_active(self):
|
||||
invalid_products = self.filtered(lambda product: not product.active and not product.category_id.active)
|
||||
if invalid_products:
|
||||
raise UserError(_("The following product categories are archived. You should either unarchive the categories or change the category of the product.\n%s", '\n'.join(invalid_products.category_id.mapped('name'))))
|
||||
invalid_products = self.filtered(lambda product: not product.active and not product.supplier_id.active)
|
||||
if invalid_products:
|
||||
raise UserError(_("The following suppliers are archived. You should either unarchive the suppliers or change the supplier of the product.\n%s", '\n'.join(invalid_products.supplier_id.mapped('name'))))
|
||||
return super().toggle_active()
|
||||
|
||||
def _inverse_is_favorite(self):
|
||||
""" Handled in the write() """
|
||||
return
|
||||
|
||||
def write(self, vals):
|
||||
if 'is_favorite' in vals:
|
||||
if vals.pop('is_favorite'):
|
||||
commands = [(4, product.id) for product in self]
|
||||
else:
|
||||
commands = [(3, product.id) for product in self]
|
||||
self.env.user.write({
|
||||
'favorite_lunch_product_ids': commands,
|
||||
})
|
||||
|
||||
if not vals:
|
||||
return True
|
||||
return super().write(vals)
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# -*- 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.modules.module import get_module_resource
|
||||
|
||||
|
||||
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):
|
||||
image_path = get_module_resource('lunch', 'static/img', 'lunch.png')
|
||||
return base64.b64encode(open(image_path, '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'], ['category_id'])
|
||||
data = {product['category_id'][0]: product['category_id_count'] for product 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
odoo-bringout-oca-ocb-lunch/lunch/models/lunch_supplier.py
Normal file
383
odoo-bringout-oca-ocb-lunch/lunch/models/lunch_supplier.py
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import math
|
||||
import pytz
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, time, timedelta
|
||||
from textwrap import dedent
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import float_round
|
||||
|
||||
from odoo.addons.base.models.res_partner import _tz_get
|
||||
|
||||
|
||||
WEEKDAY_TO_NAME = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
|
||||
CRON_DEPENDS = {'name', 'active', 'send_by', 'automatic_email_time', 'moment', 'tz'}
|
||||
|
||||
def float_to_time(hours, moment='am'):
|
||||
""" Convert a number of hours into a time object. """
|
||||
if hours == 12.0 and moment == 'pm':
|
||||
return time.max
|
||||
fractional, integral = math.modf(hours)
|
||||
if moment == 'pm':
|
||||
integral += 12
|
||||
return time(int(integral), int(float_round(60 * fractional, precision_digits=0)), 0)
|
||||
|
||||
def time_to_float(t):
|
||||
return float_round(t.hour + t.minute/60 + t.second/3600, precision_digits=2)
|
||||
|
||||
class LunchSupplier(models.Model):
|
||||
_name = 'lunch.supplier'
|
||||
_description = 'Lunch Supplier'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
partner_id = fields.Many2one('res.partner', string='Vendor', required=True)
|
||||
|
||||
name = fields.Char('Name', related='partner_id.name', readonly=False)
|
||||
|
||||
email = fields.Char(related='partner_id.email', readonly=False)
|
||||
email_formatted = fields.Char(related='partner_id.email_formatted', readonly=True)
|
||||
phone = fields.Char(related='partner_id.phone', readonly=False)
|
||||
street = fields.Char(related='partner_id.street', readonly=False)
|
||||
street2 = fields.Char(related='partner_id.street2', readonly=False)
|
||||
zip_code = fields.Char(related='partner_id.zip', readonly=False)
|
||||
city = fields.Char(related='partner_id.city', readonly=False)
|
||||
state_id = fields.Many2one("res.country.state", related='partner_id.state_id', readonly=False)
|
||||
country_id = fields.Many2one('res.country', related='partner_id.country_id', readonly=False)
|
||||
company_id = fields.Many2one('res.company', related='partner_id.company_id', readonly=False, store=True)
|
||||
|
||||
responsible_id = fields.Many2one('res.users', string="Responsible", domain=lambda self: [('groups_id', 'in', self.env.ref('lunch.group_lunch_manager').id)],
|
||||
default=lambda self: self.env.user,
|
||||
help="The responsible is the person that will order lunch for everyone. It will be used as the 'from' when sending the automatic email.")
|
||||
|
||||
send_by = fields.Selection([
|
||||
('phone', 'Phone'),
|
||||
('mail', 'Email'),
|
||||
], 'Send Order By', default='phone')
|
||||
automatic_email_time = fields.Float('Order Time', default=12.0, required=True)
|
||||
cron_id = fields.Many2one('ir.cron', ondelete='cascade', required=True, readonly=True)
|
||||
|
||||
mon = fields.Boolean(default=True)
|
||||
tue = fields.Boolean(default=True)
|
||||
wed = fields.Boolean(default=True)
|
||||
thu = fields.Boolean(default=True)
|
||||
fri = fields.Boolean(default=True)
|
||||
sat = fields.Boolean()
|
||||
sun = fields.Boolean()
|
||||
|
||||
recurrency_end_date = fields.Date('Until', help="This field is used in order to ")
|
||||
|
||||
available_location_ids = fields.Many2many('lunch.location', string='Location')
|
||||
available_today = fields.Boolean('This is True when if the supplier is available today',
|
||||
compute='_compute_available_today', search='_search_available_today')
|
||||
order_deadline_passed = fields.Boolean(compute='_compute_order_deadline_passed')
|
||||
|
||||
tz = fields.Selection(_tz_get, string='Timezone', required=True, default=lambda self: self.env.user.tz or 'UTC')
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
moment = fields.Selection([
|
||||
('am', 'AM'),
|
||||
('pm', 'PM'),
|
||||
], default='am', required=True)
|
||||
|
||||
delivery = fields.Selection([
|
||||
('delivery', 'Delivery'),
|
||||
('no_delivery', 'No Delivery')
|
||||
], default='no_delivery')
|
||||
|
||||
topping_label_1 = fields.Char('Extra 1 Label', required=True, default='Extras')
|
||||
topping_label_2 = fields.Char('Extra 2 Label', required=True, default='Beverages')
|
||||
topping_label_3 = fields.Char('Extra 3 Label', required=True, default='Extra Label 3')
|
||||
topping_ids_1 = fields.One2many('lunch.topping', 'supplier_id', domain=[('topping_category', '=', 1)])
|
||||
topping_ids_2 = fields.One2many('lunch.topping', 'supplier_id', domain=[('topping_category', '=', 2)])
|
||||
topping_ids_3 = fields.One2many('lunch.topping', 'supplier_id', domain=[('topping_category', '=', 3)])
|
||||
topping_quantity_1 = fields.Selection([
|
||||
('0_more', 'None or More'),
|
||||
('1_more', 'One or More'),
|
||||
('1', 'Only One')], 'Extra 1 Quantity', default='0_more', required=True)
|
||||
topping_quantity_2 = fields.Selection([
|
||||
('0_more', 'None or More'),
|
||||
('1_more', 'One or More'),
|
||||
('1', 'Only One')], 'Extra 2 Quantity', default='0_more', required=True)
|
||||
topping_quantity_3 = fields.Selection([
|
||||
('0_more', 'None or More'),
|
||||
('1_more', 'One or More'),
|
||||
('1', 'Only One')], 'Extra 3 Quantity', default='0_more', required=True)
|
||||
|
||||
show_order_button = fields.Boolean(compute='_compute_buttons')
|
||||
show_confirm_button = fields.Boolean(compute='_compute_buttons')
|
||||
|
||||
_sql_constraints = [
|
||||
('automatic_email_time_range',
|
||||
'CHECK(automatic_email_time >= 0 AND automatic_email_time <= 12)',
|
||||
'Automatic Email Sending Time should be between 0 and 12'),
|
||||
]
|
||||
|
||||
def name_get(self):
|
||||
res = []
|
||||
for supplier in self:
|
||||
if supplier.phone:
|
||||
res.append((supplier.id, '%s %s' % (supplier.name, supplier.phone)))
|
||||
else:
|
||||
res.append((supplier.id, supplier.name))
|
||||
return res
|
||||
|
||||
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']})
|
||||
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()
|
||||
|
||||
def unlink(self):
|
||||
crons = self.cron_id.sudo()
|
||||
server_actions = crons.ir_actions_server_id
|
||||
super().unlink()
|
||||
crons.unlink()
|
||||
server_actions.unlink()
|
||||
|
||||
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, name")
|
||||
return orders
|
||||
|
||||
def _send_auto_email(self):
|
||||
""" Send an email to the supplier with the order of the day """
|
||||
# Called daily by cron
|
||||
self.ensure_one()
|
||||
|
||||
if not self.available_today:
|
||||
return
|
||||
|
||||
if self.send_by != 'mail':
|
||||
raise UserError(_("Cannot send an email to this supplier!"))
|
||||
|
||||
orders = self._get_current_orders()
|
||||
if not orders:
|
||||
return
|
||||
|
||||
order = {
|
||||
'company_name': orders[0].company_id.name,
|
||||
'currency_id': orders[0].currency_id.id,
|
||||
'supplier_id': self.partner_id.id,
|
||||
'supplier_name': self.name,
|
||||
'email_from': self.responsible_id.email_formatted,
|
||||
'amount_total': sum(order.price for order in orders),
|
||||
}
|
||||
|
||||
sites = orders.mapped('user_id.last_lunch_location_id').sorted(lambda x: x.name)
|
||||
orders_per_site = orders.sorted(lambda x: x.user_id.last_lunch_location_id.id)
|
||||
|
||||
email_orders = [{
|
||||
'product': order.product_id.name,
|
||||
'note': order.note,
|
||||
'quantity': order.quantity,
|
||||
'price': order.price,
|
||||
'toppings': order.display_toppings,
|
||||
'username': order.user_id.name,
|
||||
'site': order.user_id.last_lunch_location_id.name,
|
||||
} for order in orders_per_site]
|
||||
|
||||
email_sites = [{
|
||||
'name': site.name,
|
||||
'address': site.address,
|
||||
} for site in sites]
|
||||
|
||||
self.env.ref('lunch.lunch_order_mail_supplier').with_context(
|
||||
order=order, lines=email_orders, sites=email_sites
|
||||
).send_mail(self.id)
|
||||
|
||||
orders.action_send()
|
||||
|
||||
@api.depends('recurrency_end_date', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun')
|
||||
def _compute_available_today(self):
|
||||
now = fields.Datetime.now().replace(tzinfo=pytz.UTC)
|
||||
|
||||
for supplier in self:
|
||||
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
odoo-bringout-oca-ocb-lunch/lunch/models/lunch_topping.py
Normal file
26
odoo-bringout-oca-ocb-lunch/lunch/models/lunch_topping.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import 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)
|
||||
|
||||
def name_get(self):
|
||||
currency_id = self.env.company.currency_id
|
||||
res = dict(super(LunchTopping, self).name_get())
|
||||
for topping in self:
|
||||
price = formatLang(self.env, topping.price, currency_obj=currency_id)
|
||||
res[topping.id] = '%s %s' % (topping.name, price)
|
||||
return list(res.items())
|
||||
13
odoo-bringout-oca-ocb-lunch/lunch/models/res_company.py
Normal file
13
odoo-bringout-oca-ocb-lunch/lunch/models/res_company.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class Company(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
lunch_minimum_threshold = fields.Float()
|
||||
lunch_notify_message = fields.Html(
|
||||
default="""Your lunch has been delivered.
|
||||
Enjoy your meal!""", translate=True)
|
||||
|
|
@ -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
odoo-bringout-oca-ocb-lunch/lunch/models/res_users.py
Normal file
11
odoo-bringout-oca-ocb-lunch/lunch/models/res_users.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
last_lunch_location_id = fields.Many2one('lunch.location')
|
||||
favorite_lunch_product_ids = fields.Many2many('lunch.product', 'lunch_product_favorite_user_rel', 'user_id', 'product_id')
|
||||
Loading…
Add table
Add a link
Reference in a new issue