summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorRémy Voet (ryv) <ryv@odoo.com>2020-08-06 06:03:01 +0000
committerRémy Voet (ryv) <ryv@odoo.com>2020-08-18 07:48:27 +0000
commit8d62eacfff5f94f692661c221c7b43f39bd3b891 (patch)
treede330591caabad93d9abc7a48e28d505da1ad83b /addons
parent57f805f71e9357870dfc2498c5ef72ebd8ab7273 (diff)
[FIX] *stock*, mrp: fix effective_date
- Avoid to truncate the hour/min/sec in the computation of the effective date which is related to the `date_done` of the first incoming picking. - With manufacturing in 3-step (pbm_sam), the store picking had a source document == "New". It was the case because the `_get_move_finished_values` was call before the create and the name is set only on create. To fix this issue, during the `create` set the origin of all move. - To avoid to maintain old views unused, remove unused view of stock move. - The computation of the products_availability json is too expensive to be in tree view (2.5 times long with it) of picking. It will be hard to optimize because the half the time is due to _get_report_lines method - already in batch (on 80 records). - Review buttons of the replenishment list to be more Odooer. - In some cases the delay alert date was wrongly compute: - If we new dest_move_ids of a existant move, the delay alert date won't be compute. - If the state (to done) and the date are save in same write call the alert date of next move won't be reset. To avoids these issues, refactor the alert date to be a compute stored fields. - To avoid any confusion between date expected in the replenishement (in date) and the purchase order receipt date (datetime), we put the receipt date at the middle of the day. Also On-Time Delivery Rate computation counts only when date are bigger effective date trunced to date. Also used the timezone of the company by default (if not user timezone) task-2246665 closes odoo/odoo#55379 Related: odoo/upgrade#1586 Related: odoo/enterprise#12309 Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
Diffstat (limited to 'addons')
-rw-r--r--addons/mrp/models/mrp_production.py1
-rw-r--r--addons/purchase/controllers/portal.py2
-rw-r--r--addons/purchase/models/purchase.py8
-rw-r--r--addons/purchase_stock/models/purchase.py9
-rw-r--r--addons/purchase_stock/models/res_partner.py4
-rw-r--r--addons/purchase_stock/tests/test_purchase_lead_time.py6
-rw-r--r--addons/stock/models/stock_move.py58
-rw-r--r--addons/stock/views/stock_move_views.xml135
-rw-r--r--addons/stock/views/stock_orderpoint_views.xml6
-rw-r--r--addons/stock/views/stock_picking_views.xml4
10 files changed, 31 insertions, 202 deletions
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index 21a41e59792..7f84efbf104 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -733,6 +733,7 @@ class MrpProduction(models.Model):
production = super(MrpProduction, self).create(values)
(production.move_raw_ids | production.move_finished_ids).write({
'group_id': production.procurement_group_id.id,
+ 'origin': production.name
})
production.move_raw_ids.write({'date': production.date_planned_start})
production.move_finished_ids.write({'date': production.date_planned_finished})
diff --git a/addons/purchase/controllers/portal.py b/addons/purchase/controllers/portal.py
index 72a60ca2782..ddecdf1cefb 100644
--- a/addons/purchase/controllers/portal.py
+++ b/addons/purchase/controllers/portal.py
@@ -145,7 +145,7 @@ class CustomerPortal(CustomerPortal):
return request.redirect(order_sudo.get_portal_url())
try:
- updated_date = line._convert_to_last_minute_of_day(datetime.strptime(date_str, '%Y-%m-%d'))
+ updated_date = line._convert_to_middle_of_day(datetime.strptime(date_str, '%Y-%m-%d'))
except ValueError:
continue
diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py
index 9678848ca7e..09c2697cd2e 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -954,7 +954,7 @@ class PurchaseOrderLine(models.Model):
date_planned = date_order + relativedelta(days=seller.delay if seller else 0)
else:
date_planned = datetime.today() + relativedelta(days=seller.delay if seller else 0)
- return self._convert_to_last_minute_of_day(date_planned)
+ return self._convert_to_middle_of_day(date_planned)
@api.depends('product_id', 'date_order')
def _compute_analytic_id_and_tag_ids(self):
@@ -975,7 +975,7 @@ class PurchaseOrderLine(models.Model):
return
# Reset date, price and quantity since _onchange_quantity will provide default values
- self.date_planned = self.order_id.date_planned or self._convert_to_last_minute_of_day(datetime.today())
+ self.date_planned = self.order_id.date_planned or self._convert_to_middle_of_day(datetime.today())
self.price_unit = self.product_qty = 0.0
self._product_id_change()
@@ -1165,11 +1165,11 @@ class PurchaseOrderLine(models.Model):
'order_id': po.id,
}
- def _convert_to_last_minute_of_day(self, date):
+ def _convert_to_middle_of_day(self, date):
"""Return a datetime which is the last minute of the input date(time)
according to order user's time zone, convert to UTC time.
"""
- return timezone(self.order_id.user_id.tz or 'UTC').localize(datetime.combine(date, time.max)).astimezone(UTC).replace(tzinfo=None, microsecond=0, second=0)
+ return timezone(self.order_id.user_id.tz or self.company_id.partner_id.tz or 'UTC').localize(datetime.combine(date, time(12))).astimezone(UTC).replace(tzinfo=None)
def _update_date_planned(self, updated_date):
self.date_planned = updated_date
diff --git a/addons/purchase_stock/models/purchase.py b/addons/purchase_stock/models/purchase.py
index e68c8cddafb..095b5f9ccc5 100644
--- a/addons/purchase_stock/models/purchase.py
+++ b/addons/purchase_stock/models/purchase.py
@@ -49,8 +49,7 @@ class PurchaseOrder(models.Model):
def _compute_effective_date(self):
for order in self:
pickings = order.picking_ids.filtered(lambda x: x.state == 'done' and x.location_dest_id.usage == 'internal' and x.date_done)
- dates_list = pickings.mapped('date_done')
- order.effective_date = min(dates_list).date() if dates_list else False
+ order.effective_date = min(pickings.mapped('date_done'), default=False)
@api.depends('picking_ids', 'picking_ids.state')
def _compute_is_shipped(self):
@@ -490,11 +489,7 @@ class PurchaseOrderLine(models.Model):
values.append(val)
line.move_dest_ids.created_purchase_line_id = False
- moves = self.env['stock.move'].create(values)
- for move in moves:
- # update delay alert
- move._delay_alert_check()
- return moves
+ return self.env['stock.move'].create(values)
def _find_candidate(self, product_id, product_qty, product_uom, location_id, name, origin, company_id, values):
""" Return the record in self where the procument with values passed as
diff --git a/addons/purchase_stock/models/res_partner.py b/addons/purchase_stock/models/res_partner.py
index 951c46afd5f..b2419c7dc80 100644
--- a/addons/purchase_stock/models/res_partner.py
+++ b/addons/purchase_stock/models/res_partner.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from datetime import timedelta
+from datetime import timedelta, datetime, time
from odoo import api, fields, models
@@ -25,7 +25,7 @@ class ResPartner(models.Model):
for line in order_lines:
on_time, ordered = partner_dict.get(line.partner_id, (0, 0))
ordered += line.product_uom_qty
- on_time += sum(line.mapped('move_ids').filtered(lambda m: m.state == 'done' and m.date <= m.purchase_line_id.date_planned).mapped('quantity_done'))
+ on_time += sum(line.mapped('move_ids').filtered(lambda m: m.state == 'done' and m.date <= datetime.combine(m.purchase_line_id.date_planned, time.min)).mapped('quantity_done'))
partner_dict[line.partner_id] = (on_time, ordered)
seen_partner = self.env['res.partner']
for partner, numbers in partner_dict.items():
diff --git a/addons/purchase_stock/tests/test_purchase_lead_time.py b/addons/purchase_stock/tests/test_purchase_lead_time.py
index 89773f52041..fb71d87dfec 100644
--- a/addons/purchase_stock/tests/test_purchase_lead_time.py
+++ b/addons/purchase_stock/tests/test_purchase_lead_time.py
@@ -33,7 +33,7 @@ class TestPurchaseLeadTime(PurchaseTestCommon):
self.assertEqual(purchase.date_order, order_date, 'Order date should be equal to: Date of the procurement order - Purchase Lead Time - Delivery Lead Time.')
# Check scheduled date of purchase order
- schedule_date = datetime.combine(order_date + timedelta(days=self.product_1.seller_ids.delay), time.max).replace(microsecond=0, second=0)
+ schedule_date = datetime.combine(order_date + timedelta(days=self.product_1.seller_ids.delay), time.min).replace(tzinfo=None, hour=12)
self.assertEqual(purchase.order_line.date_planned, schedule_date, 'Schedule date should be equal to: Order date of Purchase order + Delivery Lead Time.')
# check the picking created or not
@@ -71,11 +71,11 @@ class TestPurchaseLeadTime(PurchaseTestCommon):
self.assertEqual(purchase2.date_order, order_date, 'Order date should be equal to: Date of the procurement order - Delivery Lead Time.')
# Check scheduled date of purchase order line for product_1
- schedule_date_1 = datetime.combine(order_date + timedelta(days=self.product_1.seller_ids.delay), time.max).replace(microsecond=0, second=0)
+ schedule_date_1 = datetime.combine(order_date + timedelta(days=self.product_1.seller_ids.delay), time.min).replace(tzinfo=None, hour=12)
self.assertEqual(order_line_pro_1.date_planned, schedule_date_1, 'Schedule date of purchase order line for product_1 should be equal to: Order date of purchase order + Delivery Lead Time of product_1.')
# Check scheduled date of purchase order line for product_2
- schedule_date_2 = datetime.combine(order_date + timedelta(days=self.product_2.seller_ids.delay), time.max).replace(microsecond=0, second=0)
+ schedule_date_2 = datetime.combine(order_date + timedelta(days=self.product_2.seller_ids.delay), time.min).replace(tzinfo=None, hour=12)
self.assertEqual(order_line_pro_2.date_planned, schedule_date_2, 'Schedule date of purchase order line for product_2 should be equal to: Order date of purchase order + Delivery Lead Time of product_2.')
# Check scheduled date of purchase order
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index f1d2cd7dfb1..0fa3fef230a 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -133,7 +133,7 @@ class StockMove(models.Model):
propagate_cancel = fields.Boolean(
'Propagate cancel and split', default=True,
help='If checked, when this move is cancelled, cancel the linked move too')
- delay_alert_date = fields.Datetime('Delay Alert Date', help='Process at this date to be on time')
+ delay_alert_date = fields.Datetime('Delay Alert Date', help='Process at this date to be on time', compute="_compute_delay_alert_date", store=True)
picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type', check_company=True)
inventory_id = fields.Many2one('stock.inventory', 'Inventory', check_company=True)
move_line_ids = fields.One2many('stock.move.line', 'move_id')
@@ -290,6 +290,19 @@ class StockMove(models.Model):
return self.move_line_nosuggest_ids
return self.move_line_ids
+ @api.depends('move_orig_ids.date', 'move_orig_ids.state', 'state', 'date')
+ def _compute_delay_alert_date(self):
+ for move in self:
+ if move.state in ('done', 'cancel'):
+ move.delay_alert_date = False
+ continue
+ prev_moves = move.move_orig_ids.filtered(lambda m: m.state not in ('done', 'cancel') and m.date)
+ prev_max_date = max(prev_moves.mapped("date"), default=False)
+ if prev_max_date and prev_max_date > move.date:
+ move.delay_alert_date = prev_max_date
+ else:
+ move.delay_alert_date = False
+
@api.depends('move_line_ids.qty_done', 'move_line_ids.product_uom_id', 'move_line_nosuggest_ids.qty_done', 'picking_type_id')
def _quantity_done_compute(self):
""" This field represents the sum of the move lines `qty_done`. It allows the user to know
@@ -514,11 +527,6 @@ class StockMove(models.Model):
# When editing the initial demand, directly run again action assign on receipt moves.
receipt_moves_to_reassign |= move_to_unreserve.filtered(lambda m: m.location_id.usage == 'supplier')
receipt_moves_to_reassign |= (self - move_to_unreserve).filtered(lambda m: m.location_id.usage == 'supplier' and m.state in ('partially_available', 'assigned'))
- # Handle alert date about the scheduled date
- if vals.get('date'):
- new_date = fields.Datetime.to_datetime(vals.get('date'))
- for move in self:
- move._delay_alert_check(new_date)
res = super(StockMove, self).write(vals)
if 'date_deadline' in vals:
self._set_date_deadline()
@@ -562,41 +570,6 @@ class StockMove(models.Model):
odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("base.partner_root")
doc.message_post(body=msg, author_id=odoobot_id, subject=msg_subject)
- def _delay_alert_check(self, new_date=None):
- """Set an alert on late moves by using the `delay_alert_date` field.
- The alert is always on the move that cannot be done because its preceding moves are late.
-
- :param new_date: the new expected date, to set when calling this method before `create` or `write`
- :param new_date: datetime, optional
- """
- self.ensure_one()
- if self.state in ('done', 'cancel'):
- return
-
- if new_date is None:
- new_date = self.date
-
- # Check if `self` is scheduled after the next moves. If so, the next moves are late.
- next_moves = self.move_dest_ids.filtered(lambda move: move.state != 'cancel')
- next_nondone_moves = next_moves.filtered(lambda move: move.state != 'done')
- next_moves_dates = next_moves.mapped('date')
- if next_moves_dates:
- next_moves_date = min(next_moves_dates)
- if new_date > next_moves_date:
- next_nondone_moves.write({'delay_alert_date': new_date})
- else:
- next_nondone_moves.write({'delay_alert_date': False})
-
- # Check if `self` is scheduled before the previous moves. If so, `self` if late.
- previous_moves = self.move_orig_ids.filtered(lambda move: move.state != 'cancel')
- previous_moves_dates = previous_moves.mapped('date')
- if previous_moves_dates:
- previous_moves_date = max(previous_moves_dates)
- if new_date < previous_moves_date:
- self.write({'delay_alert_date': previous_moves_date})
- else:
- self.write({'delay_alert_date': False})
-
def action_show_details(self):
""" Returns an action that will open a form view (in a popup) allowing to work on all the
move lines of a particular move. This form view is used when "show operations" is not
@@ -1362,7 +1335,6 @@ class StockMove(models.Model):
self.write({
'state': 'cancel',
'move_orig_ids': [(5, 0, 0)],
- 'delay_alert_date': False,
'procure_method': 'make_to_stock',
})
return True
@@ -1469,7 +1441,7 @@ class StockMove(models.Model):
if len(result_package.quant_ids.filtered(lambda q: not float_is_zero(abs(q.quantity) + abs(q.reserved_quantity), precision_rounding=q.product_uom_id.rounding)).mapped('location_id')) > 1:
raise UserError(_('You cannot move the same package content more than once in the same transfer or split the same package into two location.'))
picking = moves_todo.mapped('picking_id')
- moves_todo.write({'state': 'done', 'date': fields.Datetime.now(), 'delay_alert_date': False})
+ moves_todo.write({'state': 'done', 'date': fields.Datetime.now()})
move_dests_per_company = defaultdict(lambda: self.env['stock.move'])
for move_dest in moves_todo.move_dest_ids:
diff --git a/addons/stock/views/stock_move_views.xml b/addons/stock/views/stock_move_views.xml
index 7496de3784d..2681077b281 100644
--- a/addons/stock/views/stock_move_views.xml
+++ b/addons/stock/views/stock_move_views.xml
@@ -1,25 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
- <record id="stock_move_tree" model="ir.ui.view">
- <field name="name">Stock Moves</field>
- <field name="model">stock.move</field>
- <field name="arch" type="xml">
- <tree decoration-muted="state == 'cancel'" string="Moves" create="false">
- <field name="product_id"/>
- <field name="product_uom_qty"/>
- <field name="product_uom" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/>
- <field name="picking_id"/>
- <field name="location_id" options="{'no_create': True}" groups="stock.group_stock_multi_locations"/>
- <field name="location_dest_id" options="{'no_create': True}" groups="stock.group_stock_multi_locations"/>
- <field name="create_date" groups="base.group_no_one"/>
- <field name="date" groups="base.group_no_one"/>
- <field name="state"/>
- </tree>
- </field>
- </record>
-
-
<record id="view_move_pivot" model="ir.ui.view">
<field name="name">stock.move.pivot</field>
<field name="model">stock.move</field>
@@ -143,31 +124,6 @@
</field>
</record>
- <record id="view_move_picking_tree" model="ir.ui.view">
- <field name="name">stock.move.tree</field>
- <field name="model">stock.move</field>
- <field eval="12" name="priority"/>
- <field name="arch" type="xml">
- <tree decoration-muted="scrapped == True" string="Stock Moves" editable="bottom">
- <field name="company_id" invisible="1"/>
- <field name="product_id"/>
- <field name="name" invisible="1"/>
- <field name="product_uom_category_id" invisible="1"/>
- <field name="product_uom_qty"/>
- <field name="product_uom" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/>
- <field name="location_id" options="{'no_create': True}" invisible="1"/>
- <field name="create_date" options="{'no_create': True}" invisible="1" />
- <field name="date" invisible="1" />
- <field name="picking_type_id" invisible="1"/>
- <field name="scrapped" invisible="1"/>
- <field name="availability" invisible="1"/>
- <field name="reserved_availability" invisible="1"/>
- <field name="location_dest_id" invisible="1"/>
- <field name="state"/>
- </tree>
- </field>
- </record>
-
<record id="view_stock_move_operations" model="ir.ui.view">
<field name="name">stock.move.operations.form</field>
<field name="model">stock.move</field>
@@ -487,97 +443,6 @@
</field>
</record>
- <record id="stock_move_view_kanban" model="ir.ui.view">
- <field name="name">stock.move.kanban2</field>
- <field name="model">stock.move</field>
- <field name="arch" type="xml">
- <kanban class="o_kanban_mobile" quick_create="false">
- <templates>
- <t t-name="kanban-box">
- <div t-attf-class="oe_kanban_global_click">
- <div class="row mb4">
- <div class="col-8">
- <strong><span><field name="product_id"/></span></strong>
- </div>
- <div class="col-4">
- <span class="float-right text-right">
- <field name="state" widget="label_selection" readonly="1" options="{'classes': {'draft': 'default', 'cancel': 'danger',
- 'assigned': 'success', 'confirmed': 'default'}}"/>
- </span>
- </div>
- </div>
- <div class="row">
- <div class="col-4">
- <strong><span><field name="priority" widget="priority"/></span></strong>
- </div>
- <div class="col-8">
- <span class="float-right">
- <i class="fa fa-clock-o" role="img" aria-label="Date" title="Date"/> <field name="date"/>
- </span>
- </div>
- </div>
- </div>
- </t>
- </templates>
- </kanban>
- </field>
- </record>
-
- <!-- test -->
- <record id="view_move_tree_receipt_picking_board" model="ir.ui.view">
- <field name="name">stock.move.tree3</field>
- <field name="model">stock.move</field>
- <field eval="6" name="priority"/>
- <field name="arch" type="xml">
- <tree string="Moves">
- <field name="company_id" invisible="1"/>
- <field name="picking_id" string="Reference"/>
- <field name="product_id"/>
- <field name="product_uom_qty"/>
- <field name="product_uom" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/>
- <field name="date" groups="base.group_no_one"/>
- </tree>
- </field>
- </record>
-
- <record id="view_stock_move_kanban" model="ir.ui.view">
- <field name="name">stock.move.kanban</field>
- <field name="model">stock.move</field>
- <field name="arch" type="xml">
- <kanban class="o_kanban_mobile">
- <field name="product_id"/>
- <field name="state"/>
- <field name="product_uom_qty"/>
- <field name="priority"/>
- <templates>
- <t t-name="kanban-box">
- <div t-attf-class="oe_kanban_global_click">
- <div class="row mb4">
- <div class="col-6">
- <strong><field name="product_id"/></strong>
- </div>
- <div class="col-6 text-right">
- <strong>Qty: </strong><field name="product_uom_qty"/>
- </div>
- </div>
- <div class="row">
- <div class="col-6 text-muted">
- <field name="priority" widget="priority"/>
- </div>
- <div class="col-6">
- <span class="float-right">
- <field name="state" widget="label_selection" options="{'classes': {'draft': 'info', 'cancel': 'default',
- 'assigned': 'success', 'done': 'success'}}"/>
- </span>
- </div>
- </div>
- </div>
- </t>
- </templates>
- </kanban>
- </field>
- </record>
-
<menuitem action="stock_move_action" id="stock_move_menu" parent="stock.menu_warehouse_report" sequence="140" groups="base.group_no_one"/>
</odoo>
diff --git a/addons/stock/views/stock_orderpoint_views.xml b/addons/stock/views/stock_orderpoint_views.xml
index 49f3cff16a0..b377ef81870 100644
--- a/addons/stock/views/stock_orderpoint_views.xml
+++ b/addons/stock/views/stock_orderpoint_views.xml
@@ -58,11 +58,11 @@
<field name="qty_to_order"/>
<field name="product_uom_name" string="UoM" groups="uom.group_uom"/>
<field name="company_id" optional="hide" readonly="1" groups="base.group_multi_company"/>
- <button name="action_replenish" string="Order Once" type="object" class="btn-secondary pt-0 pb-0 o_replenish_buttons"
+ <button name="action_replenish" string="Order Once" type="object" class="o_replenish_buttons" icon="fa-truck"
attrs="{'invisible': [('qty_to_order', '&lt;', 1.0)]}"/>
- <button name="action_replenish_auto" string="Automate Orders" type="object" class="btn-secondary pt-0 pb-0 o_replenish_buttons"
+ <button name="action_replenish_auto" string="Automate Orders" type="object" class="o_replenish_buttons" icon="fa-refresh"
attrs="{'invisible': ['|', ('qty_to_order', '&lt;', 1.0), ('trigger', '=', 'auto')]}"/>
- <button name="%(action_orderpoint_snooze)d" string="Snooze" type="action" class="btn-secondary pt-0 pb-0"
+ <button name="%(action_orderpoint_snooze)d" string="Snooze" type="action" class="text-warning" icon="fa-bell-slash"
attrs="{'invisible': [('trigger', '!=', 'manual')]}" context="{'default_orderpoint_ids': [id]}"/>
</tree>
</field>
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index 901afe3b37f..ddef51d0e5a 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -200,10 +200,6 @@
<field name="partner_id" optional="show"/>
<field name="signature" string="Signed" optional="hide" widget="boolean" groups="stock.group_stock_sign_delivery"/>
<field name="user_id" optional="hide" widget="many2one_avatar_user"/>
- <field name="products_availability_state" invisible="1"/>
- <field name="products_availability" optional="show"
- decoration-danger="products_availability_state == 'late'"
- decoration-warning="products_availability_state == 'expected'"/>
<field name="scheduled_date" optional="show" widget="remaining_days" attrs="{'invisible':[('state', 'in', ('done', 'cancel'))]}"/>
<field name="date_deadline" optional="hide" widget="remaining_days" attrs="{'invisible':[('state', 'in', ('done', 'cancel'))]}"/>
<field name="origin" optional="show"/>