# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2008 Nexedi SA and Contributors. All Rights Reserved. # Ćukasz Nowak <lukasz.nowak@ventis.com.pl> # # WARNING: This program as such is intended to be used by professional # programmers who take the whole responsibility of assessing all potential # consequences resulting from its eventual inadequacies and bugs # End users who are looking for a ready-to-use solution with commercial # guarantees and support are strongly adviced to contract a Free Software # Service Company # # This program is Free Software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ############################################################################## import unittest 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, InventoryAPITestCase): run_all_test = 1 order_builder_portal_type = 'Order Builder' order_module = 'purchase_order_module' order_portal_type = 'Purchase Order' order_line_portal_type = 'Purchase Order Line' order_cell_portal_type = 'Purchase Order Cell' packing_list_portal_type = 'Internal Packing List' packing_list_line_portal_type = 'Internal Packing List Line' packing_list_cell_portal_type = 'Internal Packing List Cell' # hardcoded values order_builder_hardcoded_time_diff = 10.0 # defaults decrease_quantity = 1.0 max_delay = 0.0 min_flow = 0.0 def afterSetUp(self): """ Make sure to not use special apparel setting from 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, "%r != %r" % (first_date, second_date)) def stepSetMaxDelayOnResource(self, sequence): """ Sets max_delay on resource """ resource = sequence.get('resource') resource.edit(purchase_supply_line_max_delay=self.max_delay) def stepSetMinFlowOnResource(self, sequence): """ Sets min_flow on resource """ resource = sequence.get('resource') 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 = self.order_builder if sequence is not None: organisation = sequence.get('organisation') else: organisation = None order_builder.edit( delivery_module = self.order_module, delivery_portal_type = self.order_portal_type, delivery_line_portal_type = self.order_line_portal_type, delivery_cell_portal_type = self.order_cell_portal_type, destination_value = organisation, resource_portal_type = self.resource_portal_type, simulation_select_method_id='generateMovementListForStockOptimisation', ) order_builder.newContent( portal_type = 'Category Movement Group', collect_order_group='delivery', tested_property=['source', 'destination', 'source_section', 'destination_section'], int_index=1 ) order_builder.newContent( portal_type = 'Property Movement Group', collect_order_group='delivery', tested_property=['start_date', 'stop_date'], int_index=2 ) order_builder.newContent( portal_type = 'Category Movement Group', collect_order_group='line', tested_property=['resource'], int_index=1 ) order_builder.newContent( portal_type = 'Base Variant Movement Group', collect_order_group='line', int_index=2 ) order_builder.newContent( portal_type = 'Variant Movement Group', collect_order_group='cell', int_index=1 ) def stepCheckGeneratedDocumentListVariated(self, sequence): """ Checks documents generated by Order Builders with its properties for variated resource """ organisation = sequence.get('organisation') resource = sequence.get('resource') # XXX: add support for more generated documents order, = sequence.get('generated_document_list') self.assertEqual(order.getDestinationValue(), organisation) self.assertDateAlmostEquals(order.getStartDate(), self.wanted_start_date) self.assertDateAlmostEquals(order.getStopDate(), self.wanted_stop_date) # XXX: ... and for more lines/cells too order_line, = order.contentValues(portal_type=self.order_line_portal_type) self.assertEqual(order_line.getResourceValue(), resource) self.assertEqual(order_line.getTotalQuantity(), sum(self.wanted_quantity_matrix.itervalues())) quantity_matrix = {} for cell in order_line.contentValues(portal_type=self.order_cell_portal_type): key = cell.getProperty('membership_criterion_category') self.assertFalse(key in quantity_matrix) quantity_matrix[key] = cell.getQuantity() self.assertEqual(quantity_matrix, self.wanted_quantity_matrix) def stepCheckGeneratedDocumentList(self, sequence): """ Checks documents generated by Order Builders with its properties """ organisation = sequence.get('organisation') resource = sequence.get('resource') # XXX: add support for more generated documents order, = sequence.get('generated_document_list') self.assertEqual(order.getDestinationValue(), organisation) self.assertDateAlmostEquals(self.wanted_start_date, order.getStartDate()) self.assertDateAlmostEquals(self.wanted_stop_date, order.getStopDate()) # XXX: ... and for more lines/cells too order_line, = order.contentValues(portal_type=self.order_line_portal_type) self.assertEqual(order_line.getResourceValue(), resource) self.assertEqual(order_line.getTotalQuantity(), self.wanted_quantity) def stepBuildOrderBuilder(self, sequence): """ Invokes build method for Order Builder """ order_builder = sequence.get('order_builder') generated_document_list = order_builder.build() sequence.set('generated_document_list', generated_document_list) 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): """ Creates movement with variation from organisation to None. Using Internal Packing List, confirms it. Note: Maybe use InventoryAPITestCase::_makeMovement instead of IPL ? """ organisation = sequence.get('organisation') resource = sequence.get('resource') packing_list_module = self.portal.getDefaultModule( portal_type = self.packing_list_portal_type ) packing_list = packing_list_module.newContent( portal_type = self.packing_list_portal_type, source_value = organisation, start_date = self.datetime+10, specialise = self.business_process, ) packing_list_line = packing_list.newContent( portal_type = self.packing_list_line_portal_type, resource_value = resource, quantity = self.decrease_quantity, ) self.decrease_quantity_matrix = { 'variation/%s/blue' % resource.getRelativeUrl() : 1.0, 'variation/%s/green' % resource.getRelativeUrl() : 2.0, } self.wanted_quantity_matrix = self.decrease_quantity_matrix.copy() packing_list_line.setVariationCategoryList( self.decrease_quantity_matrix.keys(), ) self.tic() base_id = 'movement' cell_key_list = list(packing_list_line.getCellKeyList(base_id=base_id)) cell_key_list.sort() for cell_key in cell_key_list: cell = packing_list_line.newCell(base_id=base_id, portal_type=self.packing_list_cell_portal_type, *cell_key) cell.edit(mapped_value_property_list=['price','quantity'], quantity=self.decrease_quantity_matrix[cell_key[0]], predicate_category_list=cell_key, variation_category_list=cell_key) packing_list.confirm() def stepDecreaseOrganisationResourceQuantity(self, sequence): """ Creates movement from organisation to None. Using Internal Packing List, confirms it. Note: Maybe use InventoryAPITestCase::_makeMovement instead of IPL ? """ organisation = sequence.get('organisation') resource = sequence.get('resource') packing_list_module = self.portal.getDefaultModule( portal_type = self.packing_list_portal_type ) packing_list = packing_list_module.newContent( portal_type = self.packing_list_portal_type, source_value = organisation, start_date = self.datetime+10, specialise = self.business_process, ) packing_list.newContent( portal_type = self.packing_list_line_portal_type, resource_value = resource, quantity = self.decrease_quantity, ) packing_list.confirm() def stepCreateVariatedResource(self, sequence=None, sequence_list=None, \ **kw): """ Create a resource with variation """ portal = self.getPortal() resource_module = portal.getDefaultModule(self.resource_portal_type) resource = resource_module.newContent(portal_type=self.resource_portal_type) resource.edit( title = "VariatedResource%s" % resource.getId(), variation_base_category_list = ['variation'], ) for color in ['blue', 'green']: resource.newContent(portal_type='Product Individual Variation', id=color, title=color) sequence.edit(resource=resource) class TestOrderBuilder(TestOrderBuilderMixin, ERP5TypeTestCase): """ Test Order Builder functionality """ run_all_test = 1 resource_portal_type = "Product" common_sequence_string = """ CreateOrganisation CreateNotVariatedResource SetMaxDelayOnResource SetMinFlowOnResource Tic DecreaseOrganisationResourceQuantity Tic CreateOrderBuilder FillOrderBuilder Tic BuildOrderBuilder Tic CheckGeneratedDocumentList """ def getTitle(self): return "Order Builder" def test_01_simpleOrderBuilder(self, quiet=0, run=run_all_test): """ Test simple Order Builder """ if not run: return self.wanted_quantity = 1.0 self.wanted_start_date = DateTime( str(self.datetime + self.order_builder_hardcoded_time_diff)) self.wanted_stop_date = self.wanted_start_date sequence_list = SequenceList() sequence_list.addSequenceString(self.common_sequence_string) sequence_list.play(self) def test_01a_simpleOrderBuilderVariatedResource(self, quiet=0, run=run_all_test): """ Test simple Order Builder for Variated Resource """ if not run: return sequence_string = """ CreateOrganisation CreateVariatedResource SetMaxDelayOnResource SetMinFlowOnResource Tic DecreaseOrganisationResourceQuantityVariated Tic CreateOrderBuilder FillOrderBuilder Tic BuildOrderBuilder Tic CheckGeneratedDocumentListVariated """ self.wanted_quantity = 1.0 self.wanted_start_date = DateTime( str(self.datetime + self.order_builder_hardcoded_time_diff)) self.wanted_stop_date = self.wanted_start_date sequence_list = SequenceList() sequence_list.addSequenceString(sequence_string) sequence_list.play(self) def test_02_maxDelayResourceOrderBuilder(self, quiet=0, run=run_all_test): """ Test max_delay impact on generated order start date """ if not run: return self.max_delay = 4.0 self.wanted_quantity = 1.0 self.wanted_start_date = DateTime( str(self.datetime - self.max_delay + self.order_builder_hardcoded_time_diff)) self.wanted_stop_date = DateTime( str(self.datetime + self.order_builder_hardcoded_time_diff)) sequence_list = SequenceList() sequence_list.addSequenceString(self.common_sequence_string) sequence_list.play(self) def test_03_minFlowResourceOrderBuilder(self, quiet=0, run=run_all_test): """ Test min_flow impact on generated order line quantity """ if not run: return self.wanted_quantity = 1.0 self.wanted_start_date = DateTime( str(self.datetime + self.order_builder_hardcoded_time_diff)) self.wanted_stop_date = self.wanted_start_date sequence_list = SequenceList() sequence_list.addSequenceString(self.common_sequence_string) # case when min_flow > decreased_quantity self.min_flow = 15.0 self.wanted_quantity = self.min_flow 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)) return suite