diff --git a/CHANGELOG b/CHANGELOG index 4f4186409b..e4c029c592 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +* Update early date base on internal incoming requests + Version 6.2.0 - 2021-11-01 * Bug fixes (see mercurial logs for details) diff --git a/stock.py b/stock.py index 59edaf30f7..f88b349524 100644 --- a/stock.py +++ b/stock.py @@ -1,5 +1,6 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. +from collections import defaultdict from itertools import groupby from trytond.model import ModelSQL, ModelView, Model, Workflow, fields @@ -288,6 +289,56 @@ class QuantityEarlyPlan(Workflow, ModelSQL, ModelView): to_delete.append(plan) cls.delete(to_delete) + # Update early date based on internal incoming requests + for warehouse in warehouses: + product_plans = cls.search([ + ('company', '=', company.id), + ('state', '=', 'open'), + ('origin', 'like', 'stock.move,%'), + ], + order=[('early_date', 'ASC NULLS LAST')]) + + in_plans = cls.search([ + ('company', '=', company.id), + ('state', 'in', ['open', 'processing']), + cls._incoming_domain(), + ]) + product2in = defaultdict(lambda: defaultdict(list)) + for plan in in_plans: + products = defaultdict(int) + for product, quantity in plan._incoming_quantities(warehouse): + products[product] += quantity + for product, quantity in products.items(): + product2in[product][plan.planned_date].append( + (quantity, plan)) + + to_save = [] + products = set() + for product_plan in product_plans: + if product_plan.warehouse != warehouse: + continue + product = product_plan.origin.product + quantity = product_plan.origin.internal_quantity + plans = product2in[product][product_plan.early_date] + plans = cls._pick_incoming(quantity, plans) + if plans: + incoming_products = {p + for pl in plans + for p, q in pl._incoming_quantities(warehouse)} + if incoming_products & products: + cls.save(to_save) + del to_save[:] + products.clear() + + earlier_date = max(p.earlier_date for p in plans) + + if (not product_plan.early_date + or product_plan.early_date > earlier_date): + product_plan.early_date = earlier_date + to_save.append(product_plan) + products.add(product) + cls.save(to_save) + @classmethod def _get_earlier_date(cls, move, warehouse): pool = Pool() @@ -347,6 +398,28 @@ class QuantityEarlyPlan(Workflow, ModelSQL, ModelView): if move.shipment: yield move.shipment + @classmethod + def _incoming_domain(cls): + return ['OR', + ('origin.state', '=', 'request', 'stock.shipment.internal'), + ] + + def _incoming_quantities(self, warehouse): + pool = Pool() + ShipmentInternal = pool.get('stock.shipment.internal') + if isinstance(self.origin, ShipmentInternal): + shipment = self.origin + if (shipment.to_location.warehouse == warehouse + or shipment.from_location.warehouse != warehouse): + for move in shipment.outgoing_moves: + yield move.product, move.internal_quantity + + @classmethod + def _pick_incoming(cls, quantity, plans): + plans = [p for q, p in plans if q >= quantity] + plans.sort(key=lambda p: p.earlier_date) + return plans + class QuantityEarlyPlanProduction(metaclass=PoolMeta): __name__ = 'stock.quantity.early_plan' @@ -372,6 +445,22 @@ class QuantityEarlyPlanProduction(metaclass=PoolMeta): if move.production_output: yield move.production_output + @classmethod + def _incoming_domain(cls): + return super()._internal_domain() + [ + ('origin.state', '=', 'request', 'production'), + ] + + def _incoming_quantities(self, warehouse): + pool = Pool() + Production = pool.get('production') + yield from super()._incoming_quantities(warehouse) + if isinstance(self.origin, Production): + production = self.origin + if production.warehouse == warehouse: + for output in production.outputs: + yield output.product, output.internal_quantity + class Move(metaclass=PoolMeta): __name__ = 'stock.move' diff --git a/tests/scenario_stock_quantity_early_planning_incoming.rst b/tests/scenario_stock_quantity_early_planning_incoming.rst new file mode 100644 index 0000000000..b319815260 --- /dev/null +++ b/tests/scenario_stock_quantity_early_planning_incoming.rst @@ -0,0 +1,147 @@ +=============================================== +Stock Quantity Early Planning Incoming Scenario +=============================================== + +Imports:: + + >>> from decimal import Decimal + + >>> import datetime as dt + >>> from dateutil.relativedelta import relativedelta + + >>> from proteus import Model, Wizard + >>> from trytond.tests.tools import activate_modules + >>> from trytond.modules.company.tests.tools import ( + ... create_company, get_company) + + >>> today = dt.date.today() + >>> week1 = today + relativedelta(weeks=1) + >>> week2 = today + relativedelta(weeks=2) + >>> week3 = today + relativedelta(weeks=3) + +Activate modules:: + + >>> config = activate_modules('stock_quantity_early_planning') + + >>> Company = Model.get('company.company') + >>> Location = Model.get('stock.location') + >>> Move = Model.get('stock.move') + >>> Party = Model.get('party.party') + >>> Product = Model.get('product.product') + >>> ProductTemplate = Model.get('product.template') + >>> ProductUom = Model.get('product.uom') + >>> QuantityEarlyPlan = Model.get('stock.quantity.early_plan') + >>> ShipmentOut = Model.get('stock.shipment.out') + >>> ShipmentInternal = Model.get('stock.shipment.internal') + +Create company:: + + >>> _ = create_company() + >>> company = get_company() + +Create parties:: + + >>> customer = Party(name="Customer") + >>> customer.save() + +Create product:: + + >>> unit, = ProductUom.find([('name', '=', 'Unit')]) + >>> template = ProductTemplate() + >>> template.name = "Product" + >>> template.default_uom = unit + >>> template.type = 'goods' + >>> template.list_price = Decimal('20') + >>> product, = template.products + >>> product.cost_price = Decimal('5') + >>> template.save() + >>> product, = template.products + +Get stock locations:: + + >>> supplier_loc, = Location.find([('code', '=', 'SUP')]) + >>> customer_loc, = Location.find([('code', '=', 'CUS')]) + +Duplicate warehouse:: + + >>> warehouse, = Location.find([('type', '=', 'warehouse')]) + >>> warehouse2, = warehouse.duplicate() + +Plan receiving some products in 1 week in second warehouse:: + + >>> def fill(quantity, product, date): + ... move = Move() + ... move.product = product + ... move.quantity = quantity + ... move.from_location = supplier_loc + ... move.to_location = warehouse2.input_location + ... move.unit_price = product.cost_price + ... move.planned_date = date + ... move.save() + + >>> fill(5, product, today) + >>> fill(15, product, week1) + +Request to ship to first warehouse in 2 and 3 weeks:: + + >>> shipment_int1 = ShipmentInternal() + >>> shipment_int1.planned_date = week2 + >>> shipment_int1.from_location = warehouse2.storage_location + >>> shipment_int1.to_location = warehouse.storage_location + >>> move = shipment_int1.moves.new() + >>> move.from_location = warehouse2.storage_location + >>> move.to_location = warehouse.storage_location + >>> move.product = product + >>> move.quantity = 10 + >>> shipment_int1.save() + >>> ShipmentInternal.write( + ... [shipment_int1.id], {'state': 'request'}, shipment_int1._context) + + >>> shipment_int2 = ShipmentInternal() + >>> shipment_int2.planned_date = week2 + >>> shipment_int2.from_location = warehouse2.storage_location + >>> shipment_int2.to_location = warehouse.storage_location + >>> move = shipment_int2.moves.new() + >>> move.from_location = warehouse2.storage_location + >>> move.to_location = warehouse.storage_location + >>> move.product = product + >>> move.quantity = 5 + >>> shipment_int2.save() + >>> ShipmentInternal.write( + ... [shipment_int2.id], {'state': 'request'}, shipment_int2._context) + +Plan to ship in 3 weeks:: + + >>> shipment_out = ShipmentOut(warehouse=warehouse) + >>> shipment_out.planned_date = week3 + >>> shipment_out.customer = customer + >>> move = shipment_out.outgoing_moves.new() + >>> move.product = product + >>> move.quantity = 7 + >>> move.from_location = warehouse.output_location + >>> move.to_location = customer_loc + >>> move.unit_price = product.list_price + >>> shipment_out.save() + >>> shipment_out.click('wait') + +Generate early planning:: + + >>> generate_planning = Wizard('stock.quantity.early_plan.generate') + >>> generate_planning.execute('generate') + +Check early planning:: + + >>> plan, = QuantityEarlyPlan.find( + ... [('origin', '=', str(shipment_int1))]) + >>> plan.earlier_date == week1 + True + + >>> plan, = QuantityEarlyPlan.find( + ... [('origin', '=', str(shipment_int2))]) + >>> plan.earlier_date == today + True + + >>> plan, = QuantityEarlyPlan.find( + ... [('origin', '=', str(shipment_out))]) + >>> plan.earlier_date == week1 + True diff --git a/tests/test_stock_quantity_early_planning.py b/tests/test_stock_quantity_early_planning.py index eba657eb7e..29c97dd970 100644 --- a/tests/test_stock_quantity_early_planning.py +++ b/tests/test_stock_quantity_early_planning.py @@ -28,4 +28,9 @@ def suite(): tearDown=doctest_teardown, encoding='utf-8', checker=doctest_checker, optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) + suite.addTests(doctest.DocFileSuite( + 'scenario_stock_quantity_early_planning_incoming.rst', + tearDown=doctest_teardown, encoding='utf-8', + checker=doctest_checker, + optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) return suite