Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[14.0][IMP] ddmrp: Refactoring of incomings calculation #311

Open
wants to merge 1 commit into
base: 14.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
167 changes: 118 additions & 49 deletions ddmrp/models/stock_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import operator as py_operator
import threading
from collections import defaultdict
from datetime import datetime, timedelta
from datetime import datetime, time, timedelta
from math import pi

from odoo import _, api, exceptions, fields, models
Expand Down Expand Up @@ -1167,12 +1167,56 @@
string="Incoming (Outside DLT)",
readonly=True,
)
stock_moves_inside_dlt_ids = fields.Many2many(
comodel_name="stock.move",
relation="stock_moves_inside_dlt",
)
stock_moves_inside_dlt_qty = fields.Float(
string="Stock Moves Qty (Within DLT)",
readonly=True,
)
stock_moves_outside_dlt_ids = fields.Many2many(
comodel_name="stock.move",
relation="stock_moves_outside_dlt",
)
stock_moves_outside_dlt_qty = fields.Float(
string="Stock Moves Qty (Outside DLT)",
readonly=True,
)
rfq_inside_dlt_ids = fields.Many2many(
comodel_name="purchase.order.line",
relation="rfq_inside_dlt",
)
rfq_inside_dlt_qty = fields.Float(
string="RFQ Qty (Inside DLT)",
readonly=True,
help="Request for Quotation total quantity that is planned inside of "
"the DLT horizon.",
)
rfq_outside_dlt_ids = fields.Many2many(
comodel_name="purchase.order.line",
relation="rfq_outside_dlt",
)
rfq_outside_dlt_qty = fields.Float(
string="RFQ Qty (Outside DLT)",
readonly=True,
help="Request for Quotation total quantity that is planned outside of "
"the DLT horizon.",
)
stock_moves_inside_dlt_qty_to_subtract = fields.Float(
string="Stock Moves Qty (Within DLT) To Subtract",
help="If a Stock Move contains quantities of open RFQs and confirmed "
"Purchase Orders, we will maintain the Stock Move information but "
"save in this field the units that need to be subtracted.",
readonly=True,
)
stock_moves_outside_dlt_qty_to_subtract = fields.Float(
string="Stock Moves Qty (Outside DLT) To Subtract",
help="If a Stock Move contains quantities of open RFQs and confirmed "
"Purchase Orders, we will maintain the Stock Move information but "
"save in this field the units that need to be subtracted.",
readonly=True,
)
net_flow_position = fields.Float(
string="Net flow position",
digits="Product Unit of Measure",
Expand Down Expand Up @@ -1542,7 +1586,13 @@
# The safety factor allows to control the date limit
factor = self.warehouse_id.nfp_incoming_safety_factor or 1
horizon = int(self.dlt) * factor
return self._get_date_planned(force_lt=horizon)
datetime_planned = self._get_date_planned(force_lt=horizon)
target_time = time(12, 0, 0)
if datetime_planned.time() < target_time:
datetime_planned = datetime_planned.replace(
hour=12, minute=0, second=0, microsecond=0
)
return datetime_planned

def _search_stock_moves_incoming_domain(self, outside_dlt=False):
date_to = self._get_incoming_supply_date_limit()
Expand All @@ -1557,22 +1607,41 @@
("date", date_operator, date_to),
]

def _get_all_upstream_moves(self, moves):
upstream_moves = moves
while upstream_moves.mapped("move_orig_ids"):
upstream_moves = upstream_moves.mapped("move_orig_ids")
return upstream_moves.filtered(lambda x: x.state != "cancel")

def _search_stock_moves_incoming(self, outside_dlt=False):
domain = self._search_stock_moves_incoming_domain(outside_dlt=outside_dlt)
moves = self.env["stock.move"].search(domain)
moves = moves.filtered(
lambda move: not move.location_id.is_sublocation_of(self.location_id)
and move.location_dest_id.is_sublocation_of(self.location_id)
)
if self.warehouse_id.reception_steps != "one_step":
attr = f'stock_moves_{"outside_" if outside_dlt else "inside_"}dlt_qty_to_subtract'
upstream_moves = self._get_all_upstream_moves(moves)
qty_to_subtract = sum(moves.mapped("product_uom_qty")) - sum(
upstream_moves.mapped("product_uom_qty")
)
setattr(self, attr, qty_to_subtract)
return moves

def _get_incoming_by_days(self):
self.ensure_one()
moves = self._search_stock_moves_incoming()
incoming_by_days = {}
pols = self.rfq_inside_dlt_ids
pol_dates = [dt.date() for dt in pols.mapped("date_planned")]
moves = self.stock_moves_inside_dlt_ids
move_dates = [dt.date() for dt in moves.mapped("date")]
for move_date in move_dates:
incoming_by_days[move_date] = 0.0
dates = list(set(pol_dates) | set(move_dates))
for date in dates:
incoming_by_days[date] = 0.0
for pol in pols:
date = pol.date_planned.date()
incoming_by_days[date] += pol.product_qty

Check warning on line 1644 in ddmrp/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

ddmrp/models/stock_buffer.py#L1643-L1644

Added lines #L1643 - L1644 were not covered by tests
for move in moves:
date = move.date.date()
incoming_by_days[date] += move.product_qty
Expand Down Expand Up @@ -1653,24 +1722,47 @@
rec.qualified_demand_mrp_move_ids = mrp_moves
return True

def _calc_incoming_dlt_qty(self):
for rec in self:
moves = self._search_stock_moves_incoming()
rec.incoming_dlt_qty = sum(moves.mapped("product_qty"))
outside_dlt_moves = self._search_stock_moves_incoming(outside_dlt=True)
rec.incoming_outside_dlt_qty = sum(outside_dlt_moves.mapped("product_qty"))
if rec.item_type == "purchased":
cut_date = rec._get_incoming_supply_date_limit()
# FIXME: filter using order_id.state while
# https://github.com/odoo/odoo/pull/58966 is not merged.
# Can be changed in v14.
pols = rec.purchase_line_ids.filtered(
lambda l: l.date_planned > fields.Datetime.to_datetime(cut_date)
and l.order_id.state in ("draft", "sent")
def _get_rfq_dlt(self, outside_dlt=False):
self.ensure_one()
if self.item_type == "purchased":
cut_date = self._get_incoming_supply_date_limit()
if not outside_dlt:
pols = self.purchase_line_ids.filtered(
lambda l: l.date_planned <= fields.Datetime.to_datetime(cut_date)
and l.state in ("draft", "sent")
)
rec.rfq_outside_dlt_qty = sum(pols.mapped("product_qty"))
else:
rec.rfq_outside_dlt_qty = 0.0
pols = self.purchase_line_ids.filtered(
lambda l: l.date_planned > fields.Datetime.to_datetime(cut_date)
and l.state in ("draft", "sent")
)
return pols
return self.env["purchase.order.line"]

def _calc_incoming_dlt_qty(self):
for rec in self:
rec.rfq_inside_dlt_ids = rec._get_rfq_dlt()
rec.rfq_outside_dlt_ids = rec._get_rfq_dlt(outside_dlt=True)
rec.rfq_inside_dlt_qty = sum(rec.rfq_inside_dlt_ids.mapped("product_qty"))
rec.rfq_outside_dlt_qty = sum(rec.rfq_outside_dlt_ids.mapped("product_qty"))

rec.stock_moves_inside_dlt_ids = rec._search_stock_moves_incoming()
rec.stock_moves_outside_dlt_ids = rec._search_stock_moves_incoming(
outside_dlt=True
)
rec.stock_moves_inside_dlt_qty = (
sum(rec.stock_moves_inside_dlt_ids.mapped("product_qty"))
- rec.stock_moves_inside_dlt_qty_to_subtract
)
rec.stock_moves_outside_dlt_qty = (
sum(rec.stock_moves_outside_dlt_ids.mapped("product_qty"))
- rec.stock_moves_outside_dlt_qty_to_subtract
)

rec.incoming_dlt_qty = rec.stock_moves_inside_dlt_qty
rec.incoming_outside_dlt_qty = (
rec.rfq_outside_dlt_qty + rec.stock_moves_outside_dlt_qty
)
rec.incoming_total_qty = rec.incoming_dlt_qty + rec.incoming_outside_dlt_qty
return True

Expand Down Expand Up @@ -1813,55 +1905,32 @@
def action_view_supply_moves(self):
result = self.env["ir.actions.actions"]._for_xml_id("stock.stock_move_action")
result["context"] = {}
moves = self._search_stock_moves_incoming() + self._search_stock_moves_incoming(
outside_dlt=True
)
moves = self.stock_moves_inside_dlt_ids + self.stock_moves_outside_dlt_ids

Check warning on line 1908 in ddmrp/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

ddmrp/models/stock_buffer.py#L1908

Added line #L1908 was not covered by tests
result["domain"] = [("id", "in", moves.ids)]
return result

def _get_rfq_dlt(self, outside_dlt=False):
self.ensure_one()
cut_date = self._get_incoming_supply_date_limit()
if not outside_dlt:
pols = self.purchase_line_ids.filtered(
lambda l: l.date_planned <= fields.Datetime.to_datetime(cut_date)
and l.state in ("draft", "sent")
)
else:
pols = self.purchase_line_ids.filtered(
lambda l: l.date_planned > fields.Datetime.to_datetime(cut_date)
and l.state in ("draft", "sent")
)
return pols

def action_view_supply_moves_inside_dlt_window(self):
result = self.env["ir.actions.actions"]._for_xml_id("stock.stock_move_action")
moves = self._search_stock_moves_incoming()
result["context"] = {}
result["domain"] = [("id", "in", moves.ids)]
result["domain"] = [("id", "in", self.stock_moves_inside_dlt_ids.ids)]

Check warning on line 1915 in ddmrp/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

ddmrp/models/stock_buffer.py#L1915

Added line #L1915 was not covered by tests
return result

def action_view_supply_moves_outside_dlt_window(self):
result = self.env["ir.actions.actions"]._for_xml_id("stock.stock_move_action")
moves = self._search_stock_moves_incoming(outside_dlt=True)
result["context"] = {}
result["domain"] = [("id", "in", moves.ids)]
result["domain"] = [("id", "in", self.stock_moves_outside_dlt_ids.ids)]

Check warning on line 1921 in ddmrp/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

ddmrp/models/stock_buffer.py#L1921

Added line #L1921 was not covered by tests
return result

def action_view_supply_rfq_inside_dlt_window(self):
result = self.env["ir.actions.actions"]._for_xml_id("purchase.purchase_rfq")
pols = self._get_rfq_dlt()
pos = pols.mapped("order_id")
result["context"] = {}
result["domain"] = [("id", "in", pos.ids)]
result["domain"] = [("id", "in", self.rfq_inside_dlt_ids.order_id.ids)]

Check warning on line 1927 in ddmrp/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

ddmrp/models/stock_buffer.py#L1927

Added line #L1927 was not covered by tests
return result

def action_view_supply_rfq_outside_dlt_window(self):
result = self.env["ir.actions.actions"]._for_xml_id("purchase.purchase_rfq")
pols = self._get_rfq_dlt(outside_dlt=True)
pos = pols.mapped("order_id")
result["context"] = {}
result["domain"] = [("id", "in", pos.ids)]
result["domain"] = [("id", "in", self.rfq_outside_dlt_ids.order_id.ids)]

Check warning on line 1933 in ddmrp/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

ddmrp/models/stock_buffer.py#L1933

Added line #L1933 was not covered by tests
return result

def action_view_qualified_demand_moves(self):
Expand Down
125 changes: 125 additions & 0 deletions ddmrp/tests/test_ddmrp.py
Original file line number Diff line number Diff line change
Expand Up @@ -1288,3 +1288,128 @@
]
)
self.assertFalse(op_a)

def test_47_action_view_supply_buffer_purchase(self):
"""
Verify that the view incoming quantities action for a purchased buffer
displays the correct results.
"""
buffer = self.buffer_purchase
buffer.auto_procure = True
buffer.auto_procure_option = "standard"
buffer.do_auto_procure()
buffer._calc_incoming_dlt_qty()
pol = self.pol_model.search([("product_id", "=", self.product_purchased.id)])
# Check that RFQs are correctly computed
self.assertEqual(buffer.rfq_inside_dlt_ids.ids, pol.ids)
self.assertEqual(len(buffer.rfq_outside_dlt_ids.ids), 0)
pol.date_planned += timedelta(days=1)
buffer._calc_incoming_dlt_qty()
self.assertEqual(len(buffer.rfq_inside_dlt_ids.ids), 0)
self.assertEqual(buffer.rfq_outside_dlt_ids.ids, pol.ids)
# Check that incoming quantities are correctly computed
pol.order_id.button_confirm()
buffer._calc_incoming_dlt_qty()
self.assertEqual(len(buffer.rfq_inside_dlt_ids.ids), 0)
self.assertEqual(len(buffer.rfq_outside_dlt_ids.ids), 0)
self.assertEqual(len(buffer.stock_moves_inside_dlt_ids.ids), 0)
self.assertEqual(buffer.stock_moves_outside_dlt_ids.ids, pol.move_ids.ids)
pol.mapped("move_ids.picking_id").scheduled_date -= timedelta(days=1)
buffer._calc_incoming_dlt_qty()
self.assertEqual(len(buffer.rfq_inside_dlt_ids.ids), 0)
self.assertEqual(len(buffer.rfq_outside_dlt_ids.ids), 0)
self.assertEqual(buffer.stock_moves_inside_dlt_ids.ids, pol.move_ids.ids)
self.assertEqual(len(buffer.stock_moves_outside_dlt_ids.ids), 0)

def test_48_action_view_supply_buffer_manufacture(self):
"""
Verify that the view incoming quantities action for a manufactured buffer
displays the correct results.
"""
buffer = self.buffer_a
self.quant.quantity = 0
buffer.buffer_profile_id = self.buffer_profile_mmm.id
buffer.auto_procure = True
buffer.auto_procure_option = "standard"
buffer.cron_actions()
buffer.do_auto_procure()
buffer._calc_incoming_dlt_qty()
mo = self.env["mrp.production"].search([("product_id", "=", self.productA.id)])
# Check that MOs are correctly computed
self.assertEqual(
buffer.stock_moves_inside_dlt_ids.mapped("production_id.id"), mo.ids
)
self.assertEqual(
len(buffer.stock_moves_outside_dlt_ids.mapped("production_id.id")), 0
)
mo.date_planned_finished += timedelta(days=1)
buffer._calc_incoming_dlt_qty()
self.assertEqual(
len(buffer.stock_moves_inside_dlt_ids.mapped("production_id.id")), 0
)
self.assertEqual(
buffer.stock_moves_outside_dlt_ids.mapped("production_id.id"), mo.ids
)

def test_49_action_view_supply_buffer_purchase_3_steps(self):
"""
Verify that the view incoming quantities action for a purchased buffer displays
the correct results with a 3-step configuration.
"""
buffer = self.buffer_purchase
self.warehouse.reception_steps = "three_steps"
buffer.auto_procure = True
buffer.auto_procure_option = "standard"
buffer.do_auto_procure()
buffer._calc_incoming_dlt_qty()
pol = self.pol_model.search([("product_id", "=", self.product_purchased.id)])
# Check that RFQs are correctly computed
self.assertEqual(buffer.rfq_inside_dlt_ids.ids, pol.ids)
self.assertEqual(len(buffer.rfq_outside_dlt_ids.ids), 0)
# Check that incoming quantities are correctly computed
pol.order_id.button_confirm()
buffer._calc_incoming_dlt_qty()
moves = pol.mapped("move_ids")
while moves.mapped("move_dest_ids"):
moves = moves.mapped("move_dest_ids")
self.assertEqual(len(buffer.rfq_inside_dlt_ids.ids), 0)
self.assertEqual(len(buffer.rfq_outside_dlt_ids.ids), 0)
self.assertEqual(buffer.stock_moves_inside_dlt_ids.ids, moves.ids)
self.assertEqual(len(buffer.stock_moves_outside_dlt_ids.ids), 0)
moves.mapped("picking_id").scheduled_date += timedelta(days=1)
buffer._calc_incoming_dlt_qty()
self.assertEqual(len(buffer.rfq_inside_dlt_ids.ids), 0)
self.assertEqual(len(buffer.rfq_outside_dlt_ids.ids), 0)
self.assertEqual(len(buffer.stock_moves_inside_dlt_ids.ids), 0)
self.assertEqual(buffer.stock_moves_outside_dlt_ids.ids, moves.ids)

def test_50_action_view_supply_buffer_manufacture_3_steps(self):
"""
Verify that the view incoming quantities action for a manufactured buffer displays
the correct results with a 3-step configuration.
"""
buffer = self.buffer_a
self.warehouse.manufacture_steps = "pbm_sam"
self.quant.quantity = 0
buffer.buffer_profile_id = self.buffer_profile_mmm.id
buffer.auto_procure = True
buffer.auto_procure_option = "standard"
buffer.cron_actions()
buffer.do_auto_procure()
buffer._calc_incoming_dlt_qty()
mo = self.env["mrp.production"].search([("product_id", "=", self.productA.id)])
# Check that MOs are correctly computed
moves = buffer.stock_moves_inside_dlt_ids
while moves.mapped("move_orig_ids"):
moves = moves.mapped("move_orig_ids")
self.assertEqual(moves.mapped("production_id.id"), mo.ids)
self.assertEqual(
len(buffer.stock_moves_outside_dlt_ids.mapped("production_id.id")), 0
)
for picking in moves.mapped("picking_id"):
picking.scheduled_date += timedelta(days=1)

Check warning on line 1410 in ddmrp/tests/test_ddmrp.py

View check run for this annotation

Codecov / codecov/patch

ddmrp/tests/test_ddmrp.py#L1410

Added line #L1410 was not covered by tests
buffer._calc_incoming_dlt_qty()
self.assertEqual(
len(buffer.stock_moves_inside_dlt_ids.mapped("production_id.id")), 0
)
self.assertEqual(moves.mapped("production_id.id"), mo.ids)