From 040a45d3b2e162b304a549ef2db3d7534c738d3e Mon Sep 17 00:00:00 2001 From: Yusei Tahara <yusei@nexedi.com> Date: Mon, 9 Feb 2009 09:35:58 +0000 Subject: [PATCH] Added open order functionality. git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@25495 20353a03-c40f-0410-a6d1-a30d3c3de9de --- product/ERP5/Document/OpenOrderRule.py | 187 ++++++++ product/ERP5/Document/PeriodicityLine.py | 106 +++++ product/ERP5/PropertySheet/OpenOrder.py | 39 ++ product/ERP5/PropertySheet/PeriodicityTerm.py | 46 ++ product/ERP5/tests/testOpenOrder.py | 441 ++++++++++++++++++ 5 files changed, 819 insertions(+) create mode 100644 product/ERP5/Document/OpenOrderRule.py create mode 100644 product/ERP5/Document/PeriodicityLine.py create mode 100644 product/ERP5/PropertySheet/OpenOrder.py create mode 100644 product/ERP5/PropertySheet/PeriodicityTerm.py create mode 100644 product/ERP5/tests/testOpenOrder.py diff --git a/product/ERP5/Document/OpenOrderRule.py b/product/ERP5/Document/OpenOrderRule.py new file mode 100644 index 0000000000..19c04eea2e --- /dev/null +++ b/product/ERP5/Document/OpenOrderRule.py @@ -0,0 +1,187 @@ +############################################################################## +# +# Copyright (c) 2009 Nexedi KK, Nexedi SA and Contributors. All Rights Reserved. +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability 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 +# garantees 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. +# +############################################################################## + +from AccessControl import ClassSecurityInfo +from Products.ERP5Type import Permissions, PropertySheet, Interface +from Products.ERP5.Document.Rule import Rule +from Products.ERP5.Document.DeliveryRule import DeliveryRule +from zLOG import LOG, WARNING +from DateTime import DateTime + + +class OpenOrderRule(DeliveryRule): + """ + Order Rule object make sure an Order in the simulation + is consistent with the real order + + WARNING: what to do with movement split ? + """ + # CMF Type Definition + meta_type = 'ERP5 Open Order Rule' + portal_type = 'Open Order Rule' + + # Declarative security + security = ClassSecurityInfo() + security.declareObjectProtected(Permissions.AccessContentsInformation) + + __implements__ = ( Interface.Predicate, + Interface.Rule ) + + # Default Properties + property_sheets = ( PropertySheet.Base + , PropertySheet.XMLObject + , PropertySheet.CategoryCore + , PropertySheet.DublinCore + , PropertySheet.Task + ) + + # Simulation workflow + security.declareProtected(Permissions.ModifyPortalContent, 'expand') + def expand(self, applied_rule, force=0, **kw): + """ + Expands the Order to a new simulation tree. + expand is only allowed to modify a simulation movement if it doesn't + have a delivery relation yet. + + If the movement is in ordered or planned state, has no delivered + child, and is not in order, it can be deleted. + Else, if the movement is in ordered or planned state, has no + delivered child, and is in order, it can be modified. + Else, it cannot be modified. + """ + movement_type = 'Simulation Movement' + order = applied_rule.getDefaultCausalityValue() + if order is not None: + order_movement_list = order.getMovementList( + portal_type=order.getPortalOrderMovementTypeList()) + + for order_movement in order_movement_list: + last_simulation_movement = self._getLastSimulationMovementValue(applied_rule, order_movement) + if last_simulation_movement is not None: + schedule_start_date = last_simulation_movement.getStartDate() + schedule_list = self._getOrderDateScheduleTupleList(order_movement, schedule_start_date, **kw) + else: + # Because order's start_date might be matched with the periodicity. + order_start_date = order.getStartDate() + schedule_start_date = order_start_date-1 + schedule_list = [date_pair + for date_pair in self._getOrderDateScheduleTupleList(order_movement, schedule_start_date, **kw) + if date_pair[0]>=order_start_date] + + for start_date, stop_date in schedule_list: + property_dict = {'start_date':start_date, 'stop_date':stop_date} + property_dict = self._getExpandablePropertyDict(order_movement, + property_dict) + simulation_movement = applied_rule.newContent( + portal_type=movement_type, + order_value=order_movement, + order_ratio=1, + delivery_ratio=1, + deliverable=1, + **property_dict + ) + + # Mark that expand finished. + applied_rule.setLastExpandSimulationState(order.getSimulationState()) + # Pass to base class + Rule.expand(self, applied_rule, force=force, **kw) + + security.declareProtected(Permissions.AccessContentsInformation, 'isStable') + def isStable(self, applied_rule): + """ + Checks that the applied_rule is stable + """ + LOG('OrderRule.isStable', WARNING, 'Not Implemented') + return 1 + + security.declareProtected(Permissions.AccessContentsInformation, + 'isDivergent') + def isDivergent(self, movement): + """ + Checks that the movement is divergent + """ + return Rule.isDivergent(self, movement) + + def _getExpandablePropertyDict(self, order_movement, property_dict=None): + property_list = ( + 'title', + 'reference', + 'description', + 'int_index', + 'source', + 'source_section', + 'source_function', + 'source_trade_list', + 'destination', + 'destination_section', + 'destination_function', + 'resource', + 'variation_category_list', + 'variation_property_dict', + 'base_contribution_list', + 'aggregate_list', + 'price', + 'price_currency', + 'quantity', + 'quantity_unit', + ) + if property_dict is None: + property_dict = {} + for property_name in property_list: + if not property_name in property_dict: + property_dict[property_name] = order_movement.getProperty(property_name) + return property_dict + + def _getLastSimulationMovementValue(self, applied_rule, order_movement): + result = applied_rule.searchFolder(order_uid=order_movement.getUid(), + sort_on=[('movement.start_date','DESC')]) + if len(result)>0: + return result[0].getObject() + else: + return None + + def _getOrderDateScheduleTupleList(self, order_movement, schedule_start_date, + calculation_base_date=None, **kw): + if calculation_base_date is None: + # This is NOW + calculation_base_date = DateTime() + + getPeriodicityLineValueList = order_movement._getTypeBasedMethod('getPeriodicityLineValueList') + if getPeriodicityLineValueList is None: + raise RuntimeError, "Cannot find getPeriodicityLineValueList script" + schedule_stop_date = (calculation_base_date+ + order_movement.getForecastingTermDays()) + if schedule_stop_date > order_movement.getStopDate(): + schedule_stop_date = order_movement.getStopDate() + + periodicity_line_list = getPeriodicityLineValueList(schedule_start_date, + schedule_stop_date) + result = [] + for periodicity_line in periodicity_line_list: + result.extend(periodicity_line.getDatePeriodList(schedule_start_date, + schedule_stop_date)) + return result diff --git a/product/ERP5/Document/PeriodicityLine.py b/product/ERP5/Document/PeriodicityLine.py new file mode 100644 index 0000000000..2f13571f8e --- /dev/null +++ b/product/ERP5/Document/PeriodicityLine.py @@ -0,0 +1,106 @@ +############################################################################## +# +# Copyright (c) 2009 Nexedi KK, Nexedi SA and Contributors. All Rights Reserved. +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability 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 +# garantees 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. +# +############################################################################## + +from AccessControl import ClassSecurityInfo +from Products.ERP5Type import Permissions, PropertySheet +from Products.ERP5.Document.Path import Path +from Products.ERP5.Document.Alarm import PeriodicityMixin + + +class PeriodicityLineMixin(PeriodicityMixin): + """ + A class extends PeriodicityMixin to add term. + """ + + security = ClassSecurityInfo() + security.declareObjectProtected(Permissions.AccessContentsInformation) + + property_sheets = (PropertySheet.PeriodicityTerm,) + + security.declareProtected(Permissions.AccessContentsInformation, 'getPeriodicityTermStopDate') + def getPeriodicityTermStopDate(self, start_date, default=None): + """Return periodicity term's stop_date by calculating periodicity term + length with a start_date argument and other own properties. + """ + length_number = self.getPeriodicityTermLengthNumber() + time_scale = self.getPeriodicityTermTimeScale() + scope_type = self.getPeriodicityTermScopeType() + + if scope_type: + method = self._getTypeBasedMethod('calculateScopeTypeStopDate') + if method is None: + raise RuntimeError, 'Type based method calculateScopeTypeStopDate does not exist.' + else: + return method(scope_type, start_date) + elif time_scale: + if time_scale=='day': + day = length_number + return start_date+day + else: + raise RuntimeError, 'Unknown time scale: %s' % time_scale + else: + return None + +class PeriodicityLine(Path, PeriodicityLineMixin): + """ + A class defines how often an order is made. + """ + + meta_type = 'ERP5 Periodicity Line' + portal_type = 'Periodicity Line' + add_permission = Permissions.AddPortalContent + isPortalContent = 1 + isRADContent = 1 + isPredicate = 1 + + security = ClassSecurityInfo() + security.declareObjectProtected(Permissions.AccessContentsInformation) + + property_sheets = Path.property_sheets+(PropertySheet.Periodicity, + PropertySheet.PeriodicityTerm, + ) + + security.declareProtected(Permissions.AccessContentsInformation, 'getDatePeriodList') + def getDatePeriodList(self, from_date, to_date): + """ + Returns a list of a tuple of start_date and stop_date. + """ + effective_date = self.getEffectiveDate() + expiration_date = self.getExpirationDate() + result = [] + + if effective_date is not None and from_date < effective_date: + from_date = effective_date + if expiration_date is not None and to_date > expiration_date: + to_date = expiration_date + + next_start_date = self.getNextPeriodicalDate(from_date) + while next_start_date <= to_date: + result.append((next_start_date, + self.getPeriodicityTermStopDate(next_start_date))) + next_start_date = self.getNextPeriodicalDate(next_start_date) + return result diff --git a/product/ERP5/PropertySheet/OpenOrder.py b/product/ERP5/PropertySheet/OpenOrder.py new file mode 100644 index 0000000000..2df282ee00 --- /dev/null +++ b/product/ERP5/PropertySheet/OpenOrder.py @@ -0,0 +1,39 @@ +############################################################################## +# +# Copyright (c) 2009 Nexedi KK, Nexedi SA and Contributors. All Rights Reserved. +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability 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 +# garantees 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. +# +############################################################################## + +class OpenOrder: + """ + OpenOrder provides a way to order same items repeatedly. This property sheet + provides properties which are brought by the nature of repetition. + """ + + _properties = ( + { 'id' : 'forecasting_term_days', + 'description' : 'A number of days to be forecasted.', + 'type' : 'int', + 'mode' : 'w' }, + ) diff --git a/product/ERP5/PropertySheet/PeriodicityTerm.py b/product/ERP5/PropertySheet/PeriodicityTerm.py new file mode 100644 index 0000000000..e7e28ccbc9 --- /dev/null +++ b/product/ERP5/PropertySheet/PeriodicityTerm.py @@ -0,0 +1,46 @@ +############################################################################## +# +# Copyright (c) 2009 Nexedi KK, Nexedi SA and Contributors. All Rights Reserved. +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability 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 +# garantees 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. +# +############################################################################## + +class PeriodicityTerm: + """ + A property sheet extends Periodicity to add term. + """ + + _properties = ( + { 'id' : 'periodicity_term_scope_type', + 'description' : 'A scope type pattern which is often used in the real world. ex. from today to the end of month, the end of year.', + 'type' : 'string', + 'mode' : 'w' }, + { 'id' : 'periodicity_term_time_scale', + 'description' : 'A time scale of term. ex. day, month, year.', + 'type' : 'string', + 'mode' : 'w' }, + { 'id' : 'periodicity_term_length_number', + 'description' : 'A length number of term without time scale.', + 'type' : 'int', + 'mode' : 'w' }, + ) diff --git a/product/ERP5/tests/testOpenOrder.py b/product/ERP5/tests/testOpenOrder.py new file mode 100644 index 0000000000..582ae2e898 --- /dev/null +++ b/product/ERP5/tests/testOpenOrder.py @@ -0,0 +1,441 @@ +############################################################################## +# +# Copyright (c) 2009 Nexedi KK, Nexedi SA and Contributors. All Rights Reserved. +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability 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 +# garantees 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 transaction import get as get_transaction +from AccessControl.SecurityManagement import newSecurityManager +from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase +from DateTime import DateTime + + +class TestOpenOrder(ERP5TypeTestCase): + """ + Test Open Order + """ + + def getTitle(self): + return 'Test Open Order' + + def getBusinessTemplateList(self): + + return ('erp5_base', + 'erp5_pdm', + 'erp5_trade', + ) + + def afterSetUp(self): + if getattr(self.portal, '_run_after_setup', None) is not None: + return + + self.portal.portal_rules.default_open_order_rule.validate() + self.portal.portal_rules.default_order_rule.validate() + + self.portal.portal_categories.base_amount.newContent( + id='taxable', + portal_type='Category', + title='Taxable') + tax = self.portal.tax_module.newContent( + portal_type='Tax', + title='VAT', + base_contribution='base_amount/taxable') + client = self.portal.organisation_module.newContent( + id='client', + portal_type='Organisation', + title='Client') + vendor = self.portal.organisation_module.newContent( + id='vendor', + portal_type='Organisation', + title='Vendor') + internet_connection = self.portal.service_module.newContent( + id='internet_connection', + title='Internet Connection', + base_contribution='base_amount/taxable') + training = self.portal.service_module.newContent( + id='training', + title='Training', + base_contribution='base_amount/taxable') + bread = self.portal.product_module.newContent( + id='bread', + title='Bread', + base_contribution='base_amount/taxable') + water = self.portal.product_module.newContent( + id='water', + title='Water', + base_contribution='base_amount/taxable') + main_trade_condition = self.portal.sale_trade_condition_module.newContent( + id='main_trade_condition', + portal_type='Sale Trade Condition', + title='Vendor ---> Client', + source=vendor.getRelativeUrl(), + source_section=vendor.getRelativeUrl(), + destination=client.getRelativeUrl(), + destination_section=client.getRelativeUrl(), + ) + main_trade_condition.newContent(portal_type='Tax Model Line', + title='VAT', + base_application='base_amount/taxable', + efficiency=0.05) + main_trade_condition.newContent(portal_type='Sale Supply Line', + resource=internet_connection.getRelativeUrl(), + priced_quantity=1, + base_price=200) + main_trade_condition.newContent( + id='internet_connection_periodicity_line', + portal_type='Periodicity Line', + resource=internet_connection.getRelativeUrl(), + periodicity_term_scope_type='until_the_end_of_month', + periodicity_minute=0, + periodicity_hour=0, + periodicity_month_day=1) + main_trade_condition.newContent(portal_type='Sale Supply Line', + resource=training.getRelativeUrl(), + priced_quantity=4, + base_price=400) + main_trade_condition.newContent( + id='training_periodicity_line', + portal_type='Periodicity Line', + resource=training.getRelativeUrl(), + periodicity_term_time_scale='day', + periodicity_term_length_number=1, + periodicity_hour=10, + periodicity_week_day='Monday') + main_trade_condition.newContent(portal_type='Sale Supply Line', + resource=bread.getRelativeUrl(), + priced_quantity=1, + base_price=10) + main_trade_condition.newContent( + id='bread_periodicity_line', + portal_type='Periodicity Line', + resource=bread.getRelativeUrl(), + periodicity_term_time_scale='day', + periodicity_term_length_number=0, + periodicity_minute=0, + periodicity_hour_list=(6, 12), + periodicity_week_day_list=('Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday')) + main_trade_condition.newContent(portal_type='Sale Supply Line', + resource=water.getRelativeUrl(), + priced_quantity=1, + base_price=5) + main_trade_condition.newContent( + id='water_periodicity_line', + portal_type='Periodicity Line', + resource=water.getRelativeUrl(), + periodicity_term_scope_type='until_the_next_period', + periodicity_minute=0, + periodicity_hour_list=10, + periodicity_week_frequency=2, + periodicity_week_day = 'Monday') + + # Inherit trade conditions to make sure that it works. + useless_trade_condition = self.portal.sale_trade_condition_module.newContent( + portal_type='Sale Trade Condition') + + self.portal.sale_trade_condition_module.newContent( + id='trade_condition', + portal_type='Sale Trade Condition', + specialise_list=(useless_trade_condition.getRelativeUrl(), + main_trade_condition.getRelativeUrl()) + ) + + self.portal._run_after_setup = True + get_transaction().commit() + self.tic() + + def testPeriodicityDateList(self): + """ + Make sure that periodicity line can generate correct schedule. + """ + self.assertEqual(self.portal.sale_trade_condition_module.main_trade_condition.internet_connection_periodicity_line.getDatePeriodList( + DateTime(2008,1,15), DateTime(2008,12,1)), + [(DateTime(2008,2,1), DateTime(2008,2,29)), + (DateTime(2008,3,1), DateTime(2008,3,31)), + (DateTime(2008,4,1), DateTime(2008,4,30)), + (DateTime(2008,5,1), DateTime(2008,5,31)), + (DateTime(2008,6,1), DateTime(2008,6,30)), + (DateTime(2008,7,1), DateTime(2008,7,31)), + (DateTime(2008,8,1), DateTime(2008,8,31)), + (DateTime(2008,9,1), DateTime(2008,9,30)), + (DateTime(2008,10,1), DateTime(2008,10,31)), + (DateTime(2008,11,1), DateTime(2008,11,30)), + (DateTime(2008,12,1), DateTime(2008,12,31)), + ]) + + self.assertEqual(self.portal.sale_trade_condition_module.main_trade_condition.bread_periodicity_line.getDatePeriodList( + DateTime(2008,2,26), DateTime(2008,3,5)), + [(DateTime(2008,2,26,6,0), DateTime(2008,2,26,6,0)), + (DateTime(2008,2,26,12,0), DateTime(2008,2,26,12,0)), + (DateTime(2008,2,27,6,0), DateTime(2008,2,27,6,0)), + (DateTime(2008,2,27,12,0), DateTime(2008,2,27,12,0)), + (DateTime(2008,2,28,6,0), DateTime(2008,2,28,6,0)), + (DateTime(2008,2,28,12,0), DateTime(2008,2,28,12,0)), + (DateTime(2008,2,29,6,0), DateTime(2008,2,29,6,0)), + (DateTime(2008,2,29,12,0), DateTime(2008,2,29,12,0)), + (DateTime(2008,3,1,6,0), DateTime(2008,3,1,6,0)), + (DateTime(2008,3,1,12,0), DateTime(2008,3,1,12,0)), + (DateTime(2008,3,3,6,0), DateTime(2008,3,3,6,0)), + (DateTime(2008,3,3,12,0), DateTime(2008,3,3,12,0)), + (DateTime(2008,3,4,6,0), DateTime(2008,3,4,6,0)), + (DateTime(2008,3,4,12,0), DateTime(2008,3,4,12,0)), + ]) + + self.assertEqual(self.portal.sale_trade_condition_module.main_trade_condition.water_periodicity_line.getDatePeriodList( + DateTime(2008,2,16), DateTime(2008,4,15)), + [(DateTime(2008,2,18,10,0), DateTime(2008,3,3,10,0)), + (DateTime(2008,3,3,10,0), DateTime(2008,3,17,10,0)), + (DateTime(2008,3,17,10,0), DateTime(2008,3,31,10,0)), + (DateTime(2008,3,31,10,0), DateTime(2008,4,14,10,0)), + (DateTime(2008,4,14,10,0), DateTime(2008,4,28,10,0)), + ]) + self.assertEqual(self.portal.sale_trade_condition_module.main_trade_condition.training_periodicity_line.getDatePeriodList( + DateTime(2008,2,16), DateTime(2008,3,6)), + [(DateTime(2008,2,18,10,0), DateTime(2008,2,19,10,0)), + (DateTime(2008,2,25,10,0), DateTime(2008,2,26,10,0)), + (DateTime(2008,3,3,10,0), DateTime(2008,3,4,10,0)), + ]) + + def testOpenOrderRule(self): + """ + Make sure that Open Order Rule can generate simulation movements by + following trade conditon's periodicity setting and order's forecasting term. + """ + open_sale_order = self.portal.sale_order_module.newContent( + portal_type='Open Sale Order', + specialise=self.portal.sale_trade_condition_module.trade_condition.getRelativeUrl(), + start_date=DateTime(3000,2,9), + stop_date=DateTime(3000,8,1), + ) + + open_sale_order_line = open_sale_order.newContent( + portal_type='Open Sale Order Line', + resource=self.portal.service_module.training.getRelativeUrl(), + quantity=1) + + open_sale_order.Order_applyTradeCondition(open_sale_order.getSpecialiseValue()) + + get_transaction().commit() + self.tic() + + self.assertEqual(open_sale_order_line.getPrice(), 100) + self.assertEqual(open_sale_order.getTotalPrice(), 100) + self.assertEqual(open_sale_order.getTotalNetPrice(), 105) + + open_sale_order.setForecastingTermDays(5) + open_sale_order.order() + open_sale_order.start() + + get_transaction().commit() + self.tic() + + applied_rule = open_sale_order.getCausalityRelatedValue(portal_type='Applied Rule') + self.assertEqual(len(applied_rule.objectIds()), 0) + + self.portal.portal_rules.default_open_order_rule.expand( + applied_rule, + calculation_base_date=DateTime(3000,2,9)) + + get_transaction().commit() + self.tic() + + self.assertEqual(len(applied_rule.objectIds()), 1) + self.assertEqual(applied_rule['1'].getStartDate(), DateTime(3000,2,10,10,0)) + self.assertEqual(applied_rule['1'].getStopDate(), DateTime(3000,2,11,10,0)) + + open_sale_order.setForecastingTermDays(10) + self.portal.portal_rules.default_open_order_rule.expand( + applied_rule, + calculation_base_date=DateTime(3000,2,9)) + + get_transaction().commit() + self.tic() + + self.assertEqual(len(applied_rule.objectIds()), 2) + self.assertEqual(applied_rule['2'].getStartDate(), DateTime(3000,2,17,10,0)) + self.assertEqual(applied_rule['2'].getStopDate(), DateTime(3000,2,18,10,0)) + + self.portal.portal_rules.default_open_order_rule.expand( + applied_rule, + calculation_base_date=DateTime(3000,3,1)) + + get_transaction().commit() + self.tic() + + self.assertEqual(len(applied_rule.objectIds()), 5) + self.assertEqual([(movement.getStartDate(), movement.getStopDate()) + for movement in applied_rule.objectValues(sort_on='start_date')], + [(DateTime(3000,2,10,10,0), DateTime(3000,2,11,10,0)), + (DateTime(3000,2,17,10,0), DateTime(3000,2,18,10,0)), + (DateTime(3000,2,24,10,0), DateTime(3000,2,25,10,0)), + (DateTime(3000,3,3,10,0), DateTime(3000,3,4,10,0)), + (DateTime(3000,3,10,10,0), DateTime(3000,3,11,10,0)) + ]) + + self.portal.portal_rules.default_open_order_rule.expand( + applied_rule, + calculation_base_date=DateTime(3000,3,1)) + + get_transaction().commit() + self.tic() + + self.assertEqual(len(applied_rule.objectIds()), 5) + + self.portal.sale_trade_condition_module.main_trade_condition.setExpirationDate(DateTime(3000,3,22)) + self.portal.portal_rules.default_open_order_rule.expand( + applied_rule, + calculation_base_date=DateTime(3000,3,30)) + + get_transaction().commit() + self.tic() + + self.assertEqual(len(applied_rule.objectIds()), 6) + self.assertEqual([(movement.getStartDate(), movement.getStopDate()) + for movement in applied_rule.objectValues(sort_on='start_date')], + [(DateTime(3000,2,10,10,0), DateTime(3000,2,11,10,0)), + (DateTime(3000,2,17,10,0), DateTime(3000,2,18,10,0)), + (DateTime(3000,2,24,10,0), DateTime(3000,2,25,10,0)), + (DateTime(3000,3,3,10,0), DateTime(3000,3,4,10,0)), + (DateTime(3000,3,10,10,0), DateTime(3000,3,11,10,0)), + (DateTime(3000,3,17,10,0), DateTime(3000,3,18,10,0)), + ]) + + def testBuildingSaleOrder(self): + """ + Make sure that open sale order can create sale orders repeatedly + """ + open_sale_order = self.portal.sale_order_module.newContent( + portal_type='Open Sale Order', + specialise=self.portal.sale_trade_condition_module.trade_condition.getRelativeUrl(), + start_date=DateTime(3000,2,9), + stop_date=DateTime(3000,8,1), + forecasting_term_days=5 + ) + + # Remove other test's side effect. + self.portal.sale_trade_condition_module.main_trade_condition.setExpirationDate(None) + + get_transaction().commit() + self.tic() + + open_sale_order.newContent( + title='Piano Lesson', + portal_type='Open Sale Order Line', + resource=self.portal.service_module.training.getRelativeUrl(), + quantity=1) + + open_sale_order.newContent( + title='Internet Connection', + portal_type='Open Sale Order Line', + resource=self.portal.service_module.internet_connection.getRelativeUrl(), + quantity=1) + + open_sale_order.newContent( + title='Bread Delivery Serivce', + portal_type='Open Sale Order Line', + resource=self.portal.product_module.bread.getRelativeUrl(), + quantity=1) + + open_sale_order.newContent( + title='Mineral Water Delivery Service', + portal_type='Open Sale Order Line', + resource=self.portal.product_module.water.getRelativeUrl(), + quantity=1) + + open_sale_order.Order_applyTradeCondition(open_sale_order.getSpecialiseValue()) + + get_transaction().commit() + self.tic() + + open_sale_order.order() + open_sale_order.start() + + get_transaction().commit() + self.tic() + + applied_rule = open_sale_order.getCausalityRelatedValue(portal_type='Applied Rule') + self.assertEqual(len(applied_rule.objectIds()), 0) + + open_sale_order.autoOrderPeriodically(comment='Test', calculation_base_date=DateTime(3000,2,9)) + + get_transaction().commit() + self.tic() + + self.assertEqual(len(applied_rule.objectIds()), 9) + self.assertEqual(len(open_sale_order.getCausalityRelatedValueList(portal_type='Sale Order')), 9) + + # Do the same thing and nothing happens. + open_sale_order.autoOrderPeriodically(comment='Test', calculation_base_date=DateTime(3000,2,9)) + + get_transaction().commit() + self.tic() + + self.assertEqual(len(applied_rule.objectIds()), 9) + self.assertEqual(len(open_sale_order.getCausalityRelatedValueList(portal_type='Sale Order')), 9) + + # Next + open_sale_order.autoOrderPeriodically(comment='Test', calculation_base_date=DateTime(3000,2,14)) + + get_transaction().commit() + self.tic() + + self.assertEqual(len(applied_rule.objectIds()), 19) + self.assertEqual(len(open_sale_order.getCausalityRelatedValueList(portal_type='Sale Order')), 19) + + # Check sale orders + sale_order_list = [ + brain.getObject() + for brain in self.portal.portal_catalog(portal_type='Sale Order', + causality_uid=open_sale_order.getUid(), + sort_on='delivery.start_date')] + + # The first order is bread. + self.assertEqual( + len(sale_order_list[0].objectValues(portal_type='Sale Order Line')), + 1) + self.assertEqual( + sale_order_list[0].objectValues(portal_type='Sale Order Line')[0].getTitle(), + 'Bread Delivery Serivce') + self.assertEqual(sale_order_list[0].getTotalPrice(), 10) + self.assertEqual(sale_order_list[0].getTotalNetPrice(), 10.5) + + # The second order is piano lesson. + self.assertEqual( + len(sale_order_list[1].objectValues(portal_type='Sale Order Line')), + 1) + self.assertEqual( + sale_order_list[1].objectValues(portal_type='Sale Order Line')[0].getTitle(), + 'Piano Lesson') + self.assertEqual(sale_order_list[1].getTotalPrice(), 100) + self.assertEqual(sale_order_list[1].getTotalNetPrice(), 105) + + +def test_suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestOpenOrder)) + return suite -- 2.30.9