[IMP] lunch: add 'Sent' status

Added a new 'Sent' status, meaning the orders have been made to the
supplier. Once an order is 'Sent' it cannot be modified anymore.

It's no longer possible to order a product after the automatic email has
been sent.

closes odoo/odoo#80584

Taskid: 2678064
Signed-off-by: Kevin Baptiste <kba@odoo.com>
This commit is contained in:
Kevin Baptiste
2022-03-14 17:27:54 +00:00
parent e8129cd0e7
commit 58c7e17a68
8 changed files with 158 additions and 62 deletions
+9 -11
View File
@@ -18,19 +18,19 @@ class LunchController(http.Controller):
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,
'state': line.state, # Only used for _get_state
'note': line.note} for line in lines]
raw_state, state = self._get_state(lines)
'raw_state': line.state,
'state': translated_states[line.state],
'note': line.note} for line in lines.sorted('date')]
infos.update({
'total': float_repr(float_round(sum(line['price'] for line in lines), 2), 2),
'raw_state': raw_state,
'state': state,
'raw_state': self._get_state(lines),
'lines': lines,
})
return infos
@@ -41,6 +41,7 @@ class LunchController(http.Controller):
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()
@@ -134,10 +135,7 @@ class LunchController(http.Controller):
eg: [confirmed, confirmed, new] will return ('new', 'To Order')
"""
states_to_int = {'new': 0, 'ordered': 1, 'confirmed': 2, 'cancelled': 3}
int_to_states = ['new', 'ordered', 'confirmed', 'cancelled']
translated_states = dict(request.env['lunch.order']._fields['state']._description_selection(request.env))
states_to_int = {'new': 0, 'ordered': 1, 'sent': 2, 'confirmed': 3, 'cancelled': 4}
int_to_states = ['new', 'ordered', 'sent', 'confirmed', 'cancelled']
state = int_to_states[min(states_to_int[line['state']] for line in lines)]
return (state, translated_states[state])
return int_to_states[min(states_to_int[line['raw_state']] for line in lines)]
+11 -5
View File
@@ -23,6 +23,8 @@ class LunchOrder(models.Model):
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)
@@ -31,8 +33,9 @@ class LunchOrder(models.Model):
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'),
('confirmed', 'Received'),
('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)
@@ -75,7 +78,7 @@ class LunchOrder(models.Model):
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'
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)"""
@@ -124,7 +127,7 @@ class LunchOrder(models.Model):
**vals,
'toppings': self._extract_toppings(vals),
})
if lines:
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)
@@ -187,7 +190,7 @@ class LunchOrder(models.Model):
line.display_toppings = ' + '.join(toppings.mapped('name'))
def update_quantity(self, increment):
for line in self.filtered(lambda line: line.state != 'confirmed'):
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
@@ -242,6 +245,9 @@ class LunchOrder(models.Model):
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:
+90 -11
View File
@@ -3,10 +3,11 @@
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 import api, fields, models, _
from odoo.osv import expression
from odoo.tools import float_round
@@ -72,6 +73,7 @@ class LunchSupplier(models.Model):
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')
@@ -106,6 +108,9 @@ class LunchSupplier(models.Model):
('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)',
@@ -215,6 +220,19 @@ class LunchSupplier(models.Model):
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
@@ -226,11 +244,7 @@ class LunchSupplier(models.Model):
if self.send_by != 'mail':
raise ValueError("Cannot send an email to this supplier")
orders = self.env['lunch.order'].search([
('supplier_id', '=', self.id),
('state', '=', 'ordered'),
('date', '=', fields.Date.context_today(self.with_context(tz=self.tz))),
], order="user_id, name")
orders = self._get_current_orders()
if not orders:
return
@@ -265,7 +279,7 @@ class LunchSupplier(models.Model):
order=order, lines=email_orders, sites=email_sites
).send_mail(self.id)
orders.action_confirm()
orders.action_send()
@api.depends('recurrency_end_date', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun')
def _compute_available_today(self):
@@ -274,11 +288,27 @@ class LunchSupplier(models.Model):
for supplier in self:
now = now.astimezone(pytz.timezone(supplier.tz))
if supplier.recurrency_end_date and now.date() >= supplier.recurrency_end_date:
supplier.available_today = False
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:
fieldname = WEEKDAY_TO_NAME[now.weekday()]
supplier.available_today = supplier[fieldname]
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]):
@@ -298,3 +328,52 @@ class LunchSupplier(models.Model):
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'},
}
}
+1
View File
@@ -127,6 +127,7 @@ class LunchOrder(models.Model):
('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))),
]
@@ -30,28 +30,26 @@
</div>
<div class="o_lunch_widget_info col-12 col-md-4 card border-0">
<t t-if="!_.isEmpty(widget.lines)">
<t t-if="widget.raw_state == 'ordered'">
<t t-set="state_class" t-value="'badge-warning o_lunch_ordered'"/>
</t>
<t t-else="widget.raw_state == 'confirmed'">
<t t-set="state_class" t-value="'badge-success o_lunch_confirmed'"/>
</t>
<div class="card-body">
<h4 class="card-title">
Your order
<button t-if="widget.raw_state != 'confirmed'" class="btn btn-sm btn-icon btn-link fa fa-trash o_lunch_widget_unlink"/>
<span t-if="widget.raw_state != 'new'" t-esc="widget.state" t-attf-class="badge badge-pill {{ state_class }}"/>
</h4>
<ul class="list-unstyled o_lunch_widget_lines">
<t t-set="states_mapping" t-value="{'new': 'warning', 'confirmed': 'success', 'sent': 'info', 'ordered': 'danger'}" />
<li t-foreach="widget.lines" t-as="line" t-key="line_index">
<t t-set="can_modify" t-value="!['sent', 'confirmed'].includes(line.raw_state)" />
<div class="d-flex align-items-center">
<div class="flex-grow-0 flex-shrink-0 o_lunch_product_quantity">
<button class="btn btn-sm btn-icon btn-link fa fa-minus-circle o_remove_product" t-if="widget.raw_state != 'confirmed'" t-attf-data-id="{{ line.id }}"/>
<button class="btn btn-sm btn-icon btn-link fa fa-minus-circle o_remove_product" t-if="can_modify" t-attf-data-id="{{ line.id }}"/>
<span t-esc="line.quantity"/>
<button class="btn btn-sm btn-icon btn-link fa fa-plus-circle o_add_product" t-if="widget.raw_state != 'confirmed'" t-attf-data-id="{{ line.id }}"/>
<button class="btn btn-sm btn-icon btn-link fa fa-plus-circle o_add_product" t-if="can_modify" t-attf-data-id="{{ line.id }}"/>
</div>
<div class="flex-grow-1 pl-2">
<button t-esc="line.product[1]" class="btn btn-link o_lunch_open_wizard" t-attf-data-product-id="{{ line.product[0] }}" t-attf-data-id="{{ line.id }}"/>
<button t-if="can_modify" t-esc="line.product[1]" t-attf-class="btn btn-link o_lunch_open_wizard" t-attf-data-product-id="{{ line.product[0] }}" t-attf-data-id="{{ line.id }}"/>
<t t-else="" t-esc="line.product[1]" />
<t t-set="state_pill" t-value="states_mapping[line.raw_state]" />
<span t-esc="line.state" t-attf-class="badge badge-pill badge-{{ state_pill }}" />
</div>
<div class="flex-grow-0">
<t t-call="currency_field">
@@ -414,7 +414,7 @@ QUnit.module('LunchKanbanView', {
});
QUnit.test('ordered cart', async function (assert) {
assert.expect(15);
assert.expect(13);
const kanban = await createLunchView({
View: LunchKanbanView,
@@ -438,6 +438,7 @@ QUnit.module('LunchKanbanView', {
product: [1, "Tuna sandwich", "3.00"],
toppings: [],
quantity: 1.0,
raw_state: 'ordered',
},
],
}),
@@ -457,10 +458,6 @@ QUnit.module('LunchKanbanView', {
assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_unlink',
"should have a button to clear the order");
assert.containsOnce($widgetSecondColumn, '.badge.badge-warning.o_lunch_ordered',
"should have an ordered state badge");
assert.strictEqual($widgetSecondColumn.find('.o_lunch_ordered').text().trim(), "Ordered",
"should have 'Ordered' in the state badge");
assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_lines > li',
"should have 1 order line");
@@ -490,7 +487,7 @@ QUnit.module('LunchKanbanView', {
});
QUnit.test('confirmed cart', async function (assert) {
assert.expect(15);
assert.expect(12);
const kanban = await createLunchView({
View: LunchKanbanView,
@@ -514,6 +511,8 @@ QUnit.module('LunchKanbanView', {
product: [1, "Tuna sandwich", "3.00"],
toppings: [],
quantity: 1.0,
raw_state: 'confirmed',
state: 'Confirmed',
},
],
}),
@@ -533,10 +532,6 @@ QUnit.module('LunchKanbanView', {
assert.containsNone($widgetSecondColumn, '.o_lunch_widget_unlink',
"shouldn't have a button to clear the order");
assert.containsOnce($widgetSecondColumn, '.badge.badge-success.o_lunch_confirmed',
"should have a confirmed state badge");
assert.strictEqual($widgetSecondColumn.find('.o_lunch_confirmed').text().trim(), "Received",
"should have 'Received' in the state badge");
assert.containsOnce($widgetSecondColumn, '.o_lunch_widget_lines > li',
"should have 1 order line");
@@ -550,10 +545,8 @@ QUnit.module('LunchKanbanView', {
"should have the line's quantity");
assert.strictEqual($firstLine.find('.o_lunch_product_quantity').text().trim(), "1",
"should have 1 as the line's quantity");
assert.containsOnce($firstLine, '.o_lunch_open_wizard',
"should have the line's product name to open the wizard");
assert.strictEqual($firstLine.find('.o_lunch_open_wizard').text().trim(), "Tuna sandwich",
"should have 'Tuna sandwich' as the line's product name");
assert.containsNone($firstLine, '.o_lunch_open_wizard',
"shouldn't have the line's product name to open the wizard");
assert.containsOnce($firstLine, '.o_field_monetary',
"should have the line's amount");
assert.strictEqual($firstLine.find('.o_field_monetary').text().trim(), "3.00€",
+4 -4
View File
@@ -95,7 +95,7 @@ env['lunch.supplier'].browse([{self.supplier_kothai.id}])._send_auto_email()""")
self.supplier_pizza_inn._send_auto_email()
assert line.state == 'confirmed'
assert line.state == 'sent'
line = self.env['lunch.order'].create({
'product_id': self.product_pizza.id,
@@ -115,21 +115,21 @@ env['lunch.supplier'].browse([{self.supplier_kothai.id}])._send_auto_email()""")
self.supplier_pizza_inn._send_auto_email()
assert line.state == 'confirmed'
assert line.state == 'sent'
assert line2.state == 'ordered'
line_1 = self.env['lunch.order'].create({
'product_id': self.product_pizza.id,
'quantity': 2,
'date': self.monday_1pm.date(),
'supplier_id': self.product_pizza.id,
'supplier_id': self.supplier_pizza_inn.id,
})
line_2 = self.env['lunch.order'].create({
'product_id': self.product_pizza.id,
'topping_ids_1': [(6, 0, [self.topping_olives.id])],
'date': self.monday_1pm.date(),
'supplier_id': self.product_pizza.id,
'supplier_id': self.supplier_pizza_inn.id,
})
line_3 = self.env['lunch.order'].create({
+28 -7
View File
@@ -30,7 +30,7 @@
<field name="name">lunch.order.tree</field>
<field name="model">lunch.order</field>
<field name="arch" type="xml">
<tree string="Order lines Tree" create="false" edit="false" decoration-muted="state == 'cancelled'" class="o_lunch_list">
<tree string="Order lines Tree" create="false" edit="false" decoration-muted="state == 'cancelled'" class="o_lunch_list" expand="1">
<header>
<button name="action_confirm" type="object" string="Receive"/>
</header>
@@ -43,15 +43,21 @@
<field name="lunch_location_id"/>
<field name="currency_id" invisible="1"/>
<field name='price' sum="Total" string="Price" widget="monetary"/>
<field name='state'/>
<field name='state' widget="badge" decoration-warning="state == 'new'" decoration-success="state == 'confirmed'" decoration-info="state == 'sent'" decoration-danger="state == 'ordered'"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="display_reorder_button" invisible="1"/>
<field name="notified" invisible="1"/>
<button name="action_reorder" string="Re-order" type="object" icon="fa-history" attrs="{'invisible': [('display_reorder_button', '=', False)]}" groups="lunch.group_lunch_user"/>
<button name="action_confirm" string="Confirm" type="object" icon="fa-check" attrs="{'invisible': [('state', '!=', 'ordered')]}" groups="lunch.group_lunch_manager"/>
<button name="action_confirm" string="Confirm" type="object" icon="fa-check" attrs="{'invisible': [('state', '!=', 'sent')]}" groups="lunch.group_lunch_manager"/>
<button name="action_cancel" string="Cancel" type="object" icon="fa-times" attrs="{'invisible': [('state', 'in', ['cancelled', 'confirmed'])]}" groups="lunch.group_lunch_manager"/>
<button name="action_reset" string="Reset" type="object" icon="fa-undo" attrs="{'invisible': [('state', '!=', 'cancelled')]}" groups="lunch.group_lunch_manager"/>
<button name="action_notify" string="Send Notification" type="object" icon="fa-envelope" attrs="{'invisible': ['|', ('state', '!=', 'confirmed'), ('notified', '=', True)]}" groups="lunch.group_lunch_manager"/>
<groupby name="supplier_id">
<field name="show_order_button" invisible="1" />
<field name="show_confirm_button" invisible="1" />
<button string="Send Orders" type="object" name="action_send_orders" attrs="{'invisible': [('show_order_button', '=', False)]}"/>
<button string="Confirm Orders" type="object" name="action_confirm_orders" attrs="{'invisible': [('show_confirm_button', '=', False)]}"/>
</groupby>
</tree>
</field>
</record>
@@ -91,13 +97,16 @@
</div>
<div class="row mt4">
<div class="col-6">
<a class="btn btn-sm btn-success" role="button" name="action_order" string="Order" type="object" attrs="{'invisible': ['|',('state','=','confirmed'),('state','=','ordered')]}" groups="lunch.group_lunch_manager">
<a class="btn btn-sm btn-success" role="button" name="action_order" string="Order" type="object" attrs="{'invisible': [('state', 'in', ['sent', 'ordered', 'confirmed'])]}" groups="lunch.group_lunch_manager">
<i class="fa fa-phone" role="img" aria-label="Order button" title="Order button"/>
</a>
<a class="btn btn-sm btn-info" role="button" name="action_confirm" string="Receive" type="object" attrs="{'invisible': [('state','!=','ordered')]}" groups="lunch.group_lunch_manager">
<a class="btn btn-sm btn-primary" role="button" name="action_send" string="Send" type="object" attrs="{'invisible': [('state', '!=', 'ordered')]}" groups="lunch.group_lunch_manager">
<i class="fa fa-paper-plane" role="img" aria-label="Send button" title="Send button"/>
</a>
<a class="btn btn-sm btn-info" role="button" name="action_confirm" string="Receive" type="object" attrs="{'invisible': [('state','!=','sent')]}" groups="lunch.group_lunch_manager">
<i class="fa fa-check" role="img" aria-label="Receive button" title="Receive button"/>
</a>
<a class="btn btn-sm btn-danger" role="button" name="action_cancel" string="Cancel" type="object" attrs="{'invisible': [('state','=','cancelled')]}" groups="lunch.group_lunch_manager">
<a class="btn btn-sm btn-danger" role="button" name="action_cancel" string="Cancel" type="object" attrs="{'invisible': [('state','in', ['cancelled', 'confirmed'])]}" groups="lunch.group_lunch_manager">
<i class="fa fa-times" role="img" aria-label="Cancel button" title="Cancel button"/>
</a>
<a class="btn btn-sm btn-info" role="button" name="action_notify" string="Send Notification" type="object" attrs="{'invisible': ['|', ('state', '!=', 'confirmed'), ('notified', '=', True)]}" groups="lunch.group_lunch_manager">
@@ -193,14 +202,18 @@
<field name="arch" type="xml">
<form>
<field name="company_id" invisible="1"/>
<field name="date" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="quantity" invisible="1"/>
<field name="product_id" invisible="1"/>
<field name="state" invisible="1"/>
<field name="category_id" invisible="1"/>
<field name="available_toppings_1" invisible="1"/>
<field name="available_toppings_2" invisible="1"/>
<field name="available_toppings_3" invisible="1"/>
<field name="supplier_id" invisible="1"/>
<field name="order_deadline_passed" invisible="1"/>
<field name="available_today" invisible="1"/>
<div class="d-flex">
<div class="flex-grow-0 pr-5">
<field name="image_1920" widget="image" class="o_lunch_image" options="{'image_preview': 'image_128'}"/>
@@ -251,10 +264,18 @@
<field name="note" nolabel="1" placeholder="Information, allergens, ..." />
</div>
</div>
<div class="row" attrs="{'invisible': [('order_deadline_passed', '=', False)]}">
<div class="col">
<div class="alert alert-warning" role="alert">
The orders for this vendor have already been sent.
</div>
</div>
</div>
</div>
<footer>
<button string="Save" special="save" data-hotkey="v" class="oe_highlight" invisible="not context.get('active_id', False)"/>
<button string="Add To Cart" name="add_to_cart" type="object" class="oe_highlight" invisible="context.get('active_id', False)" data-hotkey="w"/>
<button string="Add To Cart" name="add_to_cart" type="object" class="oe_highlight" invisible="context.get('active_id', False)" attrs="{'invisible': [('order_deadline_passed', '=', True)]}" data-hotkey="w"/>
<button string="Discard" special="cancel" data-hotkey="z"/>
</footer>
</form>