changeset 3909a1bb849d in modules/stock_supply_production:default
details:
https://hg.tryton.org/modules/stock_supply_production?cmd=changeset&node=3909a1bb849d
description:
Compute shortage with a single call to products_by_location
By grouping by date we can make a single call over the full date range.
issue11640
review437251003
diffstat:
production.py | 146 ++++++++++++++--------------
tests/scenario_stock_supply_production.rst | 38 +++++-
2 files changed, 103 insertions(+), 81 deletions(-)
diffs (282 lines):
diff -r 164a15323520 -r 3909a1bb849d production.py
--- a/production.py Mon May 02 17:47:51 2022 +0200
+++ b/production.py Thu Sep 08 13:14:03 2022 +0200
@@ -89,43 +89,53 @@
])
# compute requests
today = Date.today()
+
+ # aggregate product by supply period
+ date2products = defaultdict(list)
+ for product in products:
+ min_date = today
+ max_date = today + product.get_supply_period()
+ date2products[min_date, max_date].append(product)
+
requests = []
- for sub_products in grouped_slice(products):
- sub_products = list(sub_products)
- product_ids = [p.id for p in sub_products]
- with Transaction().set_context(forecast=True,
- stock_date_end=today):
- pbl = Product.products_by_location(
- warehouse_ids,
- with_childs=True,
- grouping_filter=(product_ids,))
-
- # order product by supply period
- products_period = sorted((p.get_supply_period(), p)
- for p in sub_products)
+ for (min_date, max_date), dates_products in date2products.items():
+ for sub_products in grouped_slice(products):
+ sub_products = list(sub_products)
+ product_ids = [p.id for p in sub_products]
+ with Transaction().set_context(
+ forecast=True,
+ stock_date_end=min_date):
+ pbl = Product.products_by_location(
+ warehouse_ids,
+ with_childs=True,
+ grouping_filter=(product_ids,))
- for warehouse in warehouses:
- quantities = defaultdict(int,
- ((x, pbl.pop((warehouse.id, x), 0)) for x in product_ids))
- # Do not compute shortage for product
- # with different order point
- product_ids = [
- p.id for p in sub_products
- if (warehouse.id, p.id) not in product2ops_other]
- shortages = cls.get_shortage(warehouse.id, product_ids, today,
- quantities, products_period, product2ops)
+ for warehouse in warehouses:
+ min_date_qties = defaultdict(int,
+ ((x, pbl.pop((warehouse.id, x), 0))
+ for x in product_ids))
+ # Do not compute shortage for product
+ # with different order point
+ product_ids = [
+ p.id for p in sub_products
+ if (warehouse.id, p.id) not in product2ops_other]
+ # Search for shortage between min-max
+ shortages = cls.get_shortage(
+ warehouse.id, product_ids, min_date, max_date,
+ min_date_qties=min_date_qties,
+ order_points=product2ops)
- for product in sub_products:
- if product.id not in shortages:
- continue
- for date, quantity in shortages[product.id]:
- order_point = product2ops.get(
- (warehouse.id, product.id))
- req = cls.compute_request(product, warehouse,
- quantity, date, company, order_point)
- req.planned_start_date = (
- req.on_change_with_planned_start_date())
- requests.append(req)
+ for product in sub_products:
+ if product.id not in shortages:
+ continue
+ for date, quantity in shortages[product.id]:
+ order_point = product2ops.get(
+ (warehouse.id, product.id))
+ req = cls.compute_request(product, warehouse,
+ quantity, date, company, order_point)
+ req.planned_start_date = (
+ req.on_change_with_planned_start_date())
+ requests.append(req)
cls.save(requests)
cls.set_moves(requests)
return requests
@@ -165,8 +175,8 @@
)
@classmethod
- def get_shortage(cls, location_id, product_ids, date, quantities,
- products_period, order_points):
+ def get_shortage(cls, location_id, product_ids, min_date, max_date,
+ min_date_qties, order_points):
"""
Return for each product a list of dates where the stock quantity is
less than the minimal quantity and the quantity to reach the maximal
@@ -175,33 +185,40 @@
The minimal and maximal quantities come from the order point or are
zero.
- quantities is the quantities for each product at the date.
- products_period is an ordered list of periods and products.
+ min_date_qty is the quantities for each product at the min date.
order_points is a dictionary that links products to order points.
"""
pool = Pool()
Product = pool.get('product.product')
- shortages = {}
-
- min_quantities = {}
- target_quantities = {}
+ shortages = defaultdict(list)
+ min_quantities = defaultdict(float)
+ target_quantities = defaultdict(float)
for product_id in product_ids:
order_point = order_points.get((location_id, product_id))
if order_point:
min_quantities[product_id] = order_point.min_quantity
target_quantities[product_id] = order_point.target_quantity
- else:
- min_quantities[product_id] = 0.0
- target_quantities[product_id] = 0.0
- shortages[product_id] = []
- products_period = products_period[:]
- current_date = date
- current_qties = quantities.copy()
- product_ids = product_ids[:]
- while product_ids:
- for product_id in product_ids:
+ with Transaction().set_context(
+ forecast=True,
+ stock_date_start=min_date,
+ stock_date_end=max_date):
+ pbl = Product.products_by_location(
+ [location_id],
+ with_childs=True,
+ grouping=('date', 'product'),
+ grouping_filter=(None, product_ids))
+ pbl_dates = defaultdict(dict)
+ for key, qty in pbl.items():
+ date, product_id = key[1:]
+ pbl_dates[date][product_id] = qty
+
+ current_date = min_date
+ current_qties = min_date_qties.copy()
+ products_to_check = product_ids.copy()
+ while (current_date < max_date) or (current_date == min_date):
+ for product_id in products_to_check:
current_qty = current_qties[product_id]
min_quantity = min_quantities[product_id]
if min_quantity is not None and current_qty < min_quantity:
@@ -210,27 +227,14 @@
shortages[product_id].append((current_date, quantity))
current_qties[product_id] += quantity
- # Remove product with smaller period
- while (products_period
- and products_period[0][0] <= (current_date - date)):
- _, product = products_period.pop(0)
- try:
- product_ids.remove(product.id)
- except ValueError:
- # product may have been already removed on get_shortages
- pass
+ if current_date == datetime.date.max:
+ break
current_date += datetime.timedelta(1)
- # Update current quantities with next moves
- with Transaction().set_context(forecast=True,
- stock_date_start=current_date,
- stock_date_end=current_date):
- pbl = Product.products_by_location(
- [location_id],
- with_childs=True,
- grouping_filter=(product_ids,))
- for key, qty in pbl.items():
- _, product_id = key
+ pbl = pbl_dates[current_date]
+ products_to_check.clear()
+ for product_id, qty in pbl.items():
current_qties[product_id] += qty
+ products_to_check.append(product_id)
return shortages
diff -r 164a15323520 -r 3909a1bb849d tests/scenario_stock_supply_production.rst
--- a/tests/scenario_stock_supply_production.rst Mon May 02 17:47:51
2022 +0200
+++ b/tests/scenario_stock_supply_production.rst Thu Sep 08 13:14:03
2022 +0200
@@ -4,12 +4,14 @@
Imports::
- >>> from datetime import timedelta
+ >>> import datetime as dt
>>> from decimal import Decimal
>>> from proteus import Model, Wizard
>>> from trytond.tests.tools import activate_modules
>>> from trytond.modules.company.tests.tools import create_company, \
... get_company
+ >>> from trytond.modules.stock.exceptions import MoveFutureWarning
+ >>> today = dt.date.today()
Activate modules::
@@ -39,7 +41,7 @@
>>> ProductionConfiguration = Model.get('production.configuration')
>>> production_configuration = ProductionConfiguration(1)
- >>> production_configuration.supply_period = timedelta(1)
+ >>> production_configuration.supply_period = dt.timedelta(days=30)
>>> production_configuration.save()
Get stock locations::
@@ -49,7 +51,7 @@
>>> storage_loc, = Location.find([('code', '=', 'STO')])
>>> lost_loc, = Location.find([('type', '=', 'lost_found')])
-Create a need for product::
+Create needs for product::
>>> Move = Model.get('stock.move')
>>> move = Move()
@@ -61,6 +63,17 @@
>>> move.state
'done'
+ >>> move, = move.duplicate(
+ ... default={'effective_date': today + dt.timedelta(days=10)})
+ >>> try:
+ ... move.click('do')
+ ... except MoveFutureWarning as warning:
+ ... _, (key, *_) = warning.args
+
+ >>> Warning = Model.get('res.user.warning')
+ >>> Warning(user=config.user, name=key).save()
+ >>> move.click('do')
+
The is no production request::
>>> Production = Model.get('production')
@@ -74,13 +87,18 @@
There is now a production request::
- >>> production, = Production.find([])
- >>> production.state
- 'request'
- >>> production.product == product
+ >>> productions = Production.find([])
+ >>> len(productions)
+ 2
+ >>> {p.state for p in productions}
+ {'request'}
+ >>> all(p.product == product for p in productions)
True
- >>> production.quantity
- 1.0
+ >>> sum(p.quantity for p in productions)
+ 2.0
+ >>> sorted(p.planned_date for p in productions) == [
+ ... today, today + dt.timedelta(days=9)]
+ True
Create an order point negative minimal quantity::
@@ -89,7 +107,7 @@
>>> order_point.type = 'production'
>>> order_point.product = product
>>> order_point.warehouse_location = warehouse_loc
- >>> order_point.min_quantity = -1
+ >>> order_point.min_quantity = -2
>>> order_point.target_quantity = 10
>>> order_point.save()