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):
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)
......
......@@ -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
......
......@@ -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))
......
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