Commit b1798654 authored by Sebastien Robin's avatar Sebastien Robin

order builder: improve stock optimisations to handle several nodes

This is very helpful when a warehouse is splitted into multiple
stock points.

Also avoid returning no stock optimisations if no date could be
found in future.
parent d032411c
...@@ -167,8 +167,9 @@ class BuilderMixin(XMLObject, Amount, Predicate): ...@@ -167,8 +167,9 @@ class BuilderMixin(XMLObject, Amount, Predicate):
delivery_module = getattr(self.getPortalObject(), self.getDeliveryModule()) delivery_module = getattr(self.getPortalObject(), self.getDeliveryModule())
getattr(delivery_module, delivery_module_before_building_script_id)() 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 from Products.ERP5Type.Document import newTempMovement
now = DateTime()
movement_list = [] movement_list = []
for attribute, method in [('node_uid', 'getDestinationUid'), for attribute, method in [('node_uid', 'getDestinationUid'),
('section_uid', 'getDestinationSectionUid')]: ('section_uid', 'getDestinationSectionUid')]:
...@@ -181,7 +182,7 @@ class BuilderMixin(XMLObject, Amount, Predicate): ...@@ -181,7 +182,7 @@ class BuilderMixin(XMLObject, Amount, Predicate):
sql_list = self.portal_simulation.getFutureInventoryList( sql_list = self.portal_simulation.getFutureInventoryList(
group_by_variation=1, group_by_variation=1,
group_by_resource=1, group_by_resource=1,
group_by_node=1, group_by_node=group_by_node,
group_by_section=0, group_by_section=0,
**kw) **kw)
# min_flow and max_delay are stored on a supply line. By default # min_flow and max_delay are stored on a supply line. By default
...@@ -203,7 +204,7 @@ class BuilderMixin(XMLObject, Amount, Predicate): ...@@ -203,7 +204,7 @@ class BuilderMixin(XMLObject, Amount, Predicate):
movement = newTempMovement(self.getPortalObject(), "temp") movement = newTempMovement(self.getPortalObject(), "temp")
dumb_movement = inventory_item.getObject() dumb_movement = inventory_item.getObject()
resource_portal_type = resource.getPortalType() 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" % ( "Builder %r does not support resource of type : %r" % (
self.getRelativeUrl(), resource_portal_type) self.getRelativeUrl(), resource_portal_type)
movement.edit( movement.edit(
...@@ -232,17 +233,18 @@ class BuilderMixin(XMLObject, Amount, Predicate): ...@@ -232,17 +233,18 @@ class BuilderMixin(XMLObject, Amount, Predicate):
stop_date = resource.getNextAlertInventoryDate( stop_date = resource.getNextAlertInventoryDate(
reference_quantity=min_stock, reference_quantity=min_stock,
variation_text=inventory_item.variation_text, variation_text=inventory_item.variation_text,
from_date=DateTime(), from_date=now,
group_by_node=group_by_node,
**kw) **kw)
if stop_date != None: if stop_date is None:
movement = newMovement(inventory_item, resource) stop_date = now
max_delay = resource.getMaxDelay(0) movement = newMovement(inventory_item, resource)
movement.edit( movement.edit(
start_date=stop_date-max_delay, start_date=stop_date-max_delay,
stop_date=stop_date, stop_date=stop_date,
quantity=max(min_flow, -inventory_item.inventory), quantity=max(min_flow, -inventory_item.inventory),
) )
movement_list.append(movement) movement_list.append(movement)
# We could need to cancel automated stock optimization if for some reasons # We could need to cancel automated stock optimization if for some reasons
# previous optimisations are obsolete # previous optimisations are obsolete
elif round(inventory_item.inventory, 5) > min_stock: elif round(inventory_item.inventory, 5) > min_stock:
...@@ -253,6 +255,7 @@ class BuilderMixin(XMLObject, Amount, Predicate): ...@@ -253,6 +255,7 @@ class BuilderMixin(XMLObject, Amount, Predicate):
variation_text=inventory_item.variation_text, variation_text=inventory_item.variation_text,
simulation_state="auto_planned", simulation_state="auto_planned",
sort_on=[("date", "descending")], sort_on=[("date", "descending")],
group_by_node=group_by_node
) )
for optimized_inventory in optimized_inventory_list: for optimized_inventory in optimized_inventory_list:
movement = newMovement(inventory_item, resource) movement = newMovement(inventory_item, resource)
......
...@@ -68,7 +68,7 @@ class TestOrderMixin(SubcontentReindexingWrapper): ...@@ -68,7 +68,7 @@ class TestOrderMixin(SubcontentReindexingWrapper):
'erp5_simulation', 'erp5_trade', 'erp5_apparel', 'erp5_project', 'erp5_simulation', 'erp5_trade', 'erp5_apparel', 'erp5_project',
'erp5_configurator_standard_solver', 'erp5_configurator_standard_solver',
'erp5_configurator_standard_trade_template', 'erp5_configurator_standard_trade_template',
'erp5_simulation_test', 'erp5_administration') 'erp5_simulation_test', 'erp5_administration', 'erp5_dummy_movement')
def setUpPreferences(self): def setUpPreferences(self):
#create apparel variation preferences #create apparel variation preferences
......
...@@ -33,8 +33,10 @@ from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase ...@@ -33,8 +33,10 @@ from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from DateTime import DateTime from DateTime import DateTime
from Products.ERP5Type.tests.Sequence import SequenceList from Products.ERP5Type.tests.Sequence import SequenceList
from Products.ERP5.tests.testOrder import TestOrderMixin 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 run_all_test = 1
...@@ -63,6 +65,10 @@ class TestOrderBuilderMixin(TestOrderMixin): ...@@ -63,6 +65,10 @@ class TestOrderBuilderMixin(TestOrderMixin):
""" """
self.createCategories() self.createCategories()
self.validateRules() 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): def assertDateAlmostEquals(self, first_date, second_date):
self.assertTrue(abs(first_date - second_date) < 1.0/86400, self.assertTrue(abs(first_date - second_date) < 1.0/86400,
...@@ -73,7 +79,7 @@ class TestOrderBuilderMixin(TestOrderMixin): ...@@ -73,7 +79,7 @@ class TestOrderBuilderMixin(TestOrderMixin):
Sets max_delay on resource Sets max_delay on resource
""" """
resource = sequence.get('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): def stepSetMinFlowOnResource(self, sequence):
""" """
...@@ -83,12 +89,17 @@ class TestOrderBuilderMixin(TestOrderMixin): ...@@ -83,12 +89,17 @@ class TestOrderBuilderMixin(TestOrderMixin):
resource.edit(purchase_supply_line_min_flow=self.min_flow) resource.edit(purchase_supply_line_min_flow=self.min_flow)
def stepFillOrderBuilder(self, sequence): def stepFillOrderBuilder(self, sequence):
self.fillOrderBuilder(sequence=sequence)
def fillOrderBuilder(self, sequence=None):
""" """
Fills Order Builder with proper quantites Fills Order Builder with proper quantites
""" """
order_builder = sequence.get('order_builder') order_builder = self.order_builder
organisation = sequence.get('organisation') if sequence is not None:
resource = sequence.get('resource') organisation = sequence.get('organisation')
else:
organisation = None
order_builder.edit( order_builder.edit(
delivery_module = self.order_module, delivery_module = self.order_module,
...@@ -183,12 +194,17 @@ class TestOrderBuilderMixin(TestOrderMixin): ...@@ -183,12 +194,17 @@ class TestOrderBuilderMixin(TestOrderMixin):
generated_document_list = order_builder.build() generated_document_list = order_builder.build()
sequence.set('generated_document_list', generated_document_list) sequence.set('generated_document_list', generated_document_list)
def stepCreateOrderBuilder(self, sequence): def createOrderBuilder(self):
""" """
Creates empty Order Builder Creates empty Order Builder
""" """
order_builder = self.portal.portal_orders.newContent( order_builder = self.portal.portal_orders.newContent(
portal_type=self.order_builder_portal_type) 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) sequence.set('order_builder', order_builder)
def stepDecreaseOrganisationResourceQuantityVariated(self, sequence): def stepDecreaseOrganisationResourceQuantityVariated(self, sequence):
...@@ -409,6 +425,73 @@ class TestOrderBuilder(TestOrderBuilderMixin, ERP5TypeTestCase): ...@@ -409,6 +425,73 @@ class TestOrderBuilder(TestOrderBuilderMixin, ERP5TypeTestCase):
sequence_list.play(self) 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(): def test_suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestOrderBuilder)) suite.addTest(unittest.makeSuite(TestOrderBuilder))
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment