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()
 

Reply via email to