diff --git a/product/ERP5/mixin/builder.py b/product/ERP5/mixin/builder.py index c70b26a052d829b6dd4195d2ccfaed09b85fa18f..99d4903c2cc277e7556151703b00887f5d373f48 100644 --- a/product/ERP5/mixin/builder.py +++ b/product/ERP5/mixin/builder.py @@ -167,8 +167,9 @@ class BuilderMixin(XMLObject, Amount, Predicate): delivery_module = getattr(self.getPortalObject(), self.getDeliveryModule()) getattr(delivery_module, delivery_module_before_building_script_id)() - def generateMovementListForStockOptimisation(self, **kw): + def generateMovementListForStockOptimisation(self, group_by_node=1, **kw): from Products.ERP5Type.Document import newTempMovement + now = DateTime() movement_list = [] for attribute, method in [('node_uid', 'getDestinationUid'), ('section_uid', 'getDestinationSectionUid')]: @@ -181,7 +182,7 @@ class BuilderMixin(XMLObject, Amount, Predicate): sql_list = self.portal_simulation.getFutureInventoryList( group_by_variation=1, group_by_resource=1, - group_by_node=1, + group_by_node=group_by_node, group_by_section=0, **kw) # min_flow and max_delay are stored on a supply line. By default @@ -203,7 +204,7 @@ class BuilderMixin(XMLObject, Amount, Predicate): movement = newTempMovement(self.getPortalObject(), "temp") dumb_movement = inventory_item.getObject() resource_portal_type = resource.getPortalType() - assert resource_portal_type in (resource_portal_type_list), \ + assert resource_portal_type in resource_portal_type_list, \ "Builder %r does not support resource of type : %r" % ( self.getRelativeUrl(), resource_portal_type) movement.edit( @@ -232,17 +233,18 @@ class BuilderMixin(XMLObject, Amount, Predicate): stop_date = resource.getNextAlertInventoryDate( reference_quantity=min_stock, variation_text=inventory_item.variation_text, - from_date=DateTime(), + from_date=now, + group_by_node=group_by_node, **kw) - if stop_date != None: - movement = newMovement(inventory_item, resource) - max_delay = resource.getMaxDelay(0) - movement.edit( - start_date=stop_date-max_delay, - stop_date=stop_date, - quantity=max(min_flow, -inventory_item.inventory), - ) - movement_list.append(movement) + if stop_date is None: + stop_date = now + movement = newMovement(inventory_item, resource) + movement.edit( + start_date=stop_date-max_delay, + stop_date=stop_date, + quantity=max(min_flow, -inventory_item.inventory), + ) + movement_list.append(movement) # We could need to cancel automated stock optimization if for some reasons # previous optimisations are obsolete elif round(inventory_item.inventory, 5) > min_stock: @@ -253,6 +255,7 @@ class BuilderMixin(XMLObject, Amount, Predicate): variation_text=inventory_item.variation_text, simulation_state="auto_planned", sort_on=[("date", "descending")], + group_by_node=group_by_node ) for optimized_inventory in optimized_inventory_list: movement = newMovement(inventory_item, resource) diff --git a/product/ERP5/tests/testOrder.py b/product/ERP5/tests/testOrder.py index fc7f5b16f0382a33dc94a3a4536f9c423188dfbd..018e70f013cc4b5768a38467dd358fbf662a8b96 100644 --- a/product/ERP5/tests/testOrder.py +++ b/product/ERP5/tests/testOrder.py @@ -68,7 +68,7 @@ class TestOrderMixin(SubcontentReindexingWrapper): 'erp5_simulation', 'erp5_trade', 'erp5_apparel', 'erp5_project', 'erp5_configurator_standard_solver', 'erp5_configurator_standard_trade_template', - 'erp5_simulation_test', 'erp5_administration') + 'erp5_simulation_test', 'erp5_administration', 'erp5_dummy_movement') def setUpPreferences(self): #create apparel variation preferences diff --git a/product/ERP5/tests/testOrderBuilder.py b/product/ERP5/tests/testOrderBuilder.py index 66780709a50f5fa5dbd8e4fdde48ebf5a280c4b1..3690a9f8a7deb4cf728297644805e269e5e645b3 100644 --- a/product/ERP5/tests/testOrderBuilder.py +++ b/product/ERP5/tests/testOrderBuilder.py @@ -33,8 +33,10 @@ from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase from DateTime import DateTime from Products.ERP5Type.tests.Sequence import SequenceList from Products.ERP5.tests.testOrder import TestOrderMixin +from Products.ERP5.tests.testInventoryAPI import InventoryAPITestCase +from Products.ERP5Type.tests.utils import createZODBPythonScript -class TestOrderBuilderMixin(TestOrderMixin): +class TestOrderBuilderMixin(TestOrderMixin, InventoryAPITestCase): run_all_test = 1 @@ -63,6 +65,10 @@ class TestOrderBuilderMixin(TestOrderMixin): """ self.createCategories() self.validateRules() + InventoryAPITestCase.afterSetUp(self) + self.node_1 = self.portal.organisation_module.newContent(title="Node 1") + self.node_2 = self.portal.organisation_module.newContent(title="Node 2") + self.pinDateTime(None) def assertDateAlmostEquals(self, first_date, second_date): self.assertTrue(abs(first_date - second_date) < 1.0/86400, @@ -73,7 +79,7 @@ class TestOrderBuilderMixin(TestOrderMixin): Sets max_delay on resource """ resource = sequence.get('resource') - resource.edit(max_delay=self.max_delay) + resource.edit(purchase_supply_line_max_delay=self.max_delay) def stepSetMinFlowOnResource(self, sequence): """ @@ -83,12 +89,17 @@ class TestOrderBuilderMixin(TestOrderMixin): resource.edit(purchase_supply_line_min_flow=self.min_flow) def stepFillOrderBuilder(self, sequence): + self.fillOrderBuilder(sequence=sequence) + + def fillOrderBuilder(self, sequence=None): """ Fills Order Builder with proper quantites """ - order_builder = sequence.get('order_builder') - organisation = sequence.get('organisation') - resource = sequence.get('resource') + order_builder = self.order_builder + if sequence is not None: + organisation = sequence.get('organisation') + else: + organisation = None order_builder.edit( delivery_module = self.order_module, @@ -183,12 +194,17 @@ class TestOrderBuilderMixin(TestOrderMixin): generated_document_list = order_builder.build() sequence.set('generated_document_list', generated_document_list) - def stepCreateOrderBuilder(self, sequence): + def createOrderBuilder(self): """ Creates empty Order Builder """ order_builder = self.portal.portal_orders.newContent( portal_type=self.order_builder_portal_type) + self.order_builder = order_builder + return order_builder + + def stepCreateOrderBuilder(self, sequence): + order_builder = self.createOrderBuilder() sequence.set('order_builder', order_builder) def stepDecreaseOrganisationResourceQuantityVariated(self, sequence): @@ -409,6 +425,73 @@ class TestOrderBuilder(TestOrderBuilderMixin, ERP5TypeTestCase): sequence_list.play(self) + def createSelectMethodForBuilder(self): + portal = self.getPortal() + + def checkOrderBuilderStockOptimisationResult(self, expected_result, **kw): + result_list = [(x.getResource(), x.getQuantity(), + x.getStartDate().strftime("%Y/%m/%d"), + x.getStopDate().strftime("%Y/%m/%d")) for x in \ + self.order_builder.generateMovementListForStockOptimisation(**kw)] + result_list.sort() + expected_result.sort() + self.assertEqual(expected_result, result_list) + + def test_04_generateMovementListWithDateInThePast(self): + """ + If we can not find a future date for stock optimisation, make sure to + take current date as default value (before if no date was found, no + result was returned, introducing risk to forget ordering something, this + could be big issue in real life) + """ + node_1 = self.node_1 + fixed_date = DateTime('2016/08/30') + self.pinDateTime(fixed_date) + self.createOrderBuilder() + self.fillOrderBuilder() + node_1_uid = node_1.getUid() + self.checkOrderBuilderStockOptimisationResult([], node_uid=node_1.getUid()) + self._makeMovement(quantity=-3, destination_value=node_1, simulation_state='confirmed') + resource_url = self.resource.getRelativeUrl() + self.checkOrderBuilderStockOptimisationResult( + [(resource_url, 3.0, '2016/08/30', '2016/08/30')], node_uid=node_1.getUid()) + + def test_05_generateMovementListForStockOptimisationForSeveralNodes(self): + """ + It's common to have a warehouse composed of subparts, each subpart could have + it's own subpart, etc. So we have to look at stock optimisation for the whole + warehouse, since every resource might be stored in several distinct sub parts. + Make sure that stock optimisation works fine in such case. + """ + node_1 = self.node_1 + node_2 = self.node_2 + resource = self.resource + self.createOrderBuilder() + self.fillOrderBuilder() + fixed_date = DateTime('2016/08/10') + self.pinDateTime(fixed_date) + resource_url = self.resource.getRelativeUrl() + node_uid_list = [node_1.getUid(), self.node_2.getUid()] + def checkStockOptimisationForTwoNodes(expected_result): + self.checkOrderBuilderStockOptimisationResult(expected_result, node_uid=node_uid_list, + group_by_node=0) + checkStockOptimisationForTwoNodes([]) + self._makeMovement(quantity=-3, destination_value=node_1, simulation_state='confirmed', + start_date=DateTime('2016/08/20')) + checkStockOptimisationForTwoNodes([(resource_url, 3.0, '2016/08/20', '2016/08/20')]) + self._makeMovement(quantity=-2, destination_value=node_1, simulation_state='confirmed', + start_date=DateTime('2016/08/18')) + checkStockOptimisationForTwoNodes([(resource_url, 5.0, '2016/08/18', '2016/08/18')]) + self._makeMovement(quantity=-7, destination_value=node_2, simulation_state='confirmed', + start_date=DateTime('2016/08/19')) + checkStockOptimisationForTwoNodes([(resource_url, 12.0, '2016/08/18', '2016/08/18')]) + self._makeMovement(quantity=11, destination_value=node_2, simulation_state='confirmed', + start_date=DateTime('2016/08/16')) + checkStockOptimisationForTwoNodes([(resource_url, 1.0, '2016/08/20', '2016/08/20')]) + self._makeMovement(quantity=7, destination_value=node_1, simulation_state='confirmed', + start_date=DateTime('2016/08/15')) + checkStockOptimisationForTwoNodes([]) + def test_suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestOrderBuilder))