From 9e9d4fb599d74a8895d6d1c6979fbe5a41bcadd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Nowak?= <luke@nexedi.com> Date: Tue, 7 Jul 2009 12:51:00 +0000 Subject: [PATCH] - BPM enabled rules, evaluation only git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@27992 20353a03-c40f-0410-a6d1-a30d3c3de9de --- product/ERP5/Document/BPMDeliveryRule.py | 161 +++++++++++ .../Document/BPMInvoiceTransactionRule.py | 262 ++++++++++++++++++ product/ERP5/Document/BPMInvoicingRule.py | 138 +++++++++ product/ERP5/Document/BPMOrderRule.py | 170 ++++++++++++ product/ERP5/Document/BPMRule.py | 244 ++++++++++++++++ 5 files changed, 975 insertions(+) create mode 100644 product/ERP5/Document/BPMDeliveryRule.py create mode 100644 product/ERP5/Document/BPMInvoiceTransactionRule.py create mode 100644 product/ERP5/Document/BPMInvoicingRule.py create mode 100644 product/ERP5/Document/BPMOrderRule.py create mode 100644 product/ERP5/Document/BPMRule.py diff --git a/product/ERP5/Document/BPMDeliveryRule.py b/product/ERP5/Document/BPMDeliveryRule.py new file mode 100644 index 0000000000..c6628635b1 --- /dev/null +++ b/product/ERP5/Document/BPMDeliveryRule.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved. +# Jean-Paul Smets-Solanes <jp@nexedi.com> +# Romain Courteaud <romain@nexedi.com> +# Åukasz Nowak <luke@nexedi.com> +# +# 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.BPMRule import BPMRule + +class BPMDeliveryRule(BPMRule): + """ + DISCLAIMER: Refer to BPMRule docstring disclaimer. + + This is BPM enabled Delivery Rule. + """ + + # CMF Type Definition + meta_type = 'ERP5 BPM Delivery Rule' + portal_type = 'BPM Delivery Rule' + + # Declarative security + security = ClassSecurityInfo() + security.declareObjectProtected(Permissions.AccessContentsInformation) + + # Default Properties + property_sheets = ( PropertySheet.Base + , PropertySheet.XMLObject + , PropertySheet.CategoryCore + , PropertySheet.DublinCore + , PropertySheet.Task + , PropertySheet.AppliedRule + ) + + # Simulation workflow + security.declareProtected(Permissions.ModifyPortalContent, 'expand') + def expand(self, applied_rule, delivery_movement_type_list=None, **kw): + """ + Expands the additional Delivery movements to a new simulation tree. + Expand is only allowed to create or modify simulation movements for + delivery lines which are not already linked to another simulation + movement. + + If the movement is not in current state, has no delivered child, and not + in delivery movements, it can be deleted. + Else if the movement is not in current state, it can be modified. + Else, it cannot be modified. + """ + existing_movement_list = [] + immutable_movement_list = [] + delivery = applied_rule.getDefaultCausalityValue() + if delivery_movement_type_list is None: + delivery_movement_type_list = self.getPortalDeliveryMovementTypeList() + if delivery is not None: + delivery_movement_list = delivery.getMovementList( + portal_type=delivery_movement_type_list) + # Check existing movements + for movement in applied_rule.contentValues( + portal_type=self.movement_type): + if movement.getLastExpandSimulationState() not in \ + self.getPortalCurrentInventoryStateList(): + # XXX: This condition is quick and dirty hack - knowing if + # Simulation Movement is frozen shall not be ever hardcoded, + # this is BPM configuration + movement_delivery = movement.getDeliveryValue() + if not movement._isTreeDelivered(ignore_first=1) and \ + movement_delivery not in delivery_movement_list: + applied_rule._delObject(movement.getId()) + else: + existing_movement_list.append(movement) + else: + existing_movement_list.append(movement) + immutable_movement_list.append(movement) + + # Create or modify movements + for deliv_mvt in delivery_movement_list: + sim_mvt = deliv_mvt.getDeliveryRelatedValue() + if sim_mvt is None: + # create a new deliv_mvt + if deliv_mvt.getParentUid() == deliv_mvt.getExplanationUid(): + # We are on a line + new_id = deliv_mvt.getId() + else: + # We are on a cell + new_id = "%s_%s" % (deliv_mvt.getParentId(), deliv_mvt.getId()) + # Generate the simulation deliv_mvt + property_dict = self.self._getExpandablePropertyDict(applied_rule, + deliv_mvt) + new_sim_mvt = applied_rule.newContent( + portal_type=self.movement_type, + id=new_id, + order_value=deliv_mvt, + order_ratio=1, + delivery_value=deliv_mvt, + delivery_ratio=1, + deliverable=1, + + **property_dict + ) + elif sim_mvt in existing_movement_list: + if sim_mvt not in immutable_movement_list: + # modification allowed + # XXX Hardcoded value + sim_mvt.edit( + delivery_value=deliv_mvt, + delivery_ratio=1, + deliverable=1, + force_update=1, + **property_dict + ) + else: + # modification disallowed, must compensate + raise NotImplementedError('BPM *have* to support') + + # Now we can set the last expand simulation state to the current state + applied_rule.setLastExpandSimulationState(delivery.getSimulationState()) + # Pass to base class + BPMRule.expand(self, applied_rule, **kw) + + security.declareProtected(Permissions.AccessContentsInformation, 'isStable') + def isStable(self, applied_rule): + """ + Checks that the applied_rule is stable + """ + return 0 + + # Deliverability / orderability + def isOrderable(self, movement): + return 1 + + def isDeliverable(self, movement): + if movement.getSimulationState() in movement \ + .getPortalDraftOrderStateList(): + return 0 + return 1 + diff --git a/product/ERP5/Document/BPMInvoiceTransactionRule.py b/product/ERP5/Document/BPMInvoiceTransactionRule.py new file mode 100644 index 0000000000..6cdbf8ac6e --- /dev/null +++ b/product/ERP5/Document/BPMInvoiceTransactionRule.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved. +# Jean-Paul Smets-Solanes <jp@nexedi.com> +# Åukasz Nowak <luke@nexedi.com> +# +# 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.BPMRule import BPMRule +from Products.ERP5.Document.PredicateMatrix import PredicateMatrix + +class BPMInvoiceTransactionRule(BPMRule, PredicateMatrix): + """ + DISCLAIMER: Refer to BPMRule docstring disclaimer. + + This is BPM enabled Invoice Transaction Rule. + """ + + # CMF Type Definition + meta_type = 'ERP5 BPM Invoice Transaction Rule' + portal_type = 'BPM Invoice Transaction Rule' + add_permission = Permissions.AddPortalContent + isPortalContent = 1 + isRADContent = 1 + + # Declarative security + security = ClassSecurityInfo() + security.declareObjectProtected(Permissions.AccessContentsInformation) + + # Default Properties + property_sheets = ( PropertySheet.Base + , PropertySheet.XMLObject + , PropertySheet.CategoryCore + , PropertySheet.DublinCore + , PropertySheet.Task + , PropertySheet.AppliedRule + ) + +#### Helper method for expand + def _generatePrevisionList(self, applied_rule, **kw): + """ + Generate a list of movements, that should be children of this rule, + based on its context (parent movement, delivery, configuration ...) + + These previsions are actually returned as dictionaries. + """ + prevision_list = [] + context_movement = applied_rule.getParentValue() + + business_process = applied_rule.getBusinessProcessValue() + + movement_and_path_list = [] + for business_path in business_process.getPathValueList( + self.getProperty('trade_phase_list'), + context_movement): + movement_and_path_list.append((context_movement, business_path)) + + if len(movement_and_path_list) > 1: + raise NotImplementedError + + # Find a matching cell + cell = self._getMatchingCell(context_movement) + + if cell is not None : # else, we do nothing + for accounting_rule_cell_line in cell.objectValues() : + # get the resource (in that order): + # * resource from the invoice (using deliveryValue) + # * price_currency from the invoice + # * price_currency from the parents simulation movement's + # deliveryValue + # * price_currency from the top level simulation movement's + # orderValue + resource = None + invoice_line = context_movement.getDeliveryValue() + if invoice_line is not None : + invoice = invoice_line.getExplanationValue() + resource = invoice.getProperty('resource', + invoice.getProperty('price_currency', None)) + if resource is None : + # search the resource on parents simulation movement's deliveries + simulation_movement = applied_rule.getParentValue() + portal_simulation = self.getPortalObject().portal_simulation + while resource is None and \ + simulation_movement != portal_simulation : + delivery = simulation_movement.getDeliveryValue() + if delivery is not None: + resource = delivery.getProperty('price_currency', None) + if (resource is None) and \ + (simulation_movement.getParentValue().getParentValue() \ + == portal_simulation) : + # we are on the first simulation movement, we'll try + # to get the resource from it's order price currency. + order = simulation_movement.getOrderValue() + if order is not None: + resource = order.getProperty('price_currency', None) + simulation_movement = simulation_movement\ + .getParentValue().getParentValue() + if resource is None : + # last resort : get the resource from the rule + resource = accounting_rule_cell_line.getResource() \ + or cell.getResource() + # XXX Harcoded list + prevision_line = { + 'source': accounting_rule_cell_line.getSource(), + 'source_section': context_movement.getSourceSection(), + 'source_decision': context_movement.getSourceDecision(), + 'source_administration': context_movement \ + .getSourceAdministration(), + 'source_project': context_movement.getSourceProject(), + 'source_function': context_movement.getSourceFunction(), + 'source_payment': context_movement.getSourcePayment(), + 'destination': accounting_rule_cell_line.getDestination(), + 'destination_section': context_movement.getDestinationSection(), + 'destination_decision': context_movement.getDestinationDecision(), + 'destination_administration': context_movement \ + .getDestinationAdministration(), + 'destination_project': context_movement.getDestinationProject(), + 'destination_function': context_movement.getDestinationFunction(), + 'destination_payment': context_movement.getDestinationPayment(), + 'start_date': context_movement.getStartDate(), + 'stop_date': context_movement.getStopDate(), + 'resource': resource, + 'quantity': (context_movement.getCorrectedQuantity() * + context_movement.getPrice(0.0)) * + accounting_rule_cell_line.getQuantity(), + 'price': 1, + 'force_update': 1, + 'causality_value': business_path, + } + + if accounting_rule_cell_line.hasProperty( + 'generate_prevision_script_id'): + generate_prevision_script_id = \ + accounting_rule_cell_line.getGeneratePrevisionScriptId() + prevision_line.update(getattr(context_movement, + generate_prevision_script_id)(prevision_line)) + prevision_list.append(prevision_line) + return prevision_list + + security.declareProtected(Permissions.ModifyPortalContent, 'expand') + def expand(self, applied_rule, force=0, **kw): + """ + Expands the rule: + - generate a list of previsions + - compare the prevision with existing children + - get the list of existing movements (immutable, mutable, deletable) + - compute the difference between prevision and existing (add, + modify, remove) + - add/modify/remove child movements to match prevision + """ + add_list, modify_dict, \ + delete_list = self._getCompensatedMovementList(applied_rule, + matching_property_list=['resource', 'source', + 'destination','destination_total_asset_price', + 'source_total_asset_price'],**kw) + + if len(add_list) or len(modify_dict): + pass#import pdb; pdb.set_trace() + + for movement_id in delete_list: + applied_rule._delObject(movement_id) + + for movement, prop_dict in modify_dict.items(): + applied_rule[movement].edit(**prop_dict) + + for movement_dict in add_list: + if 'id' in movement_dict.keys(): + mvmt_id = applied_rule._get_id(movement_dict.pop('id')) + new_mvmt = applied_rule.newContent(id=mvmt_id, + portal_type=self.movement_type) + else: + new_mvmt = applied_rule.newContent(portal_type=self.movement_type) + new_mvmt.edit(**movement_dict) + #set asset_price on movement when resource is different from price + #currency of the source/destination section + currency = new_mvmt.getResourceValue() + if currency is not None: + currency_url = currency.getRelativeUrl() + dest_section = new_mvmt.getDestinationSectionValue() + if dest_section is not None: + dest_currency_url = dest_section.getProperty('price_currency', None) + else: + dest_currency_url = None + if dest_currency_url is not None \ + and currency_url != dest_currency_url: + precision = dest_section.getPriceCurrencyValue() \ + .getQuantityPrecision() + dest_exchange_ratio = currency.getPrice(context=new_mvmt.asContext( + categories=['price_currency/%s' % dest_currency_url, + 'resource/%s' % currency_url], + start_date=new_mvmt.getStartDate())) + if dest_exchange_ratio is not None: + new_mvmt.edit(destination_total_asset_price=round( + (dest_exchange_ratio* + applied_rule.getParentValue().getTotalPrice()),precision)) + + source_section = new_mvmt.getSourceSectionValue() + if source_section is not None: + source_currency_url = source_section.getProperty( + 'price_currency', None) + else: + source_currency_url = None + if source_currency_url is not None \ + and currency_url != source_currency_url: + precision = source_section.getPriceCurrencyValue() \ + .getQuantityPrecision() + source_exchange_ratio = currency.getPrice(context=new_mvmt\ + .asContext( + categories=['price_currency/%s' % source_currency_url, + 'resource/%s' % currency_url], + start_date=new_mvmt.getStartDate())) + if source_exchange_ratio is not None: + new_mvmt.setSourceTotalAssetPrice(round( + source_exchange_ratio*applied_rule.getParentValue().getTotalPrice(), + precision)) + + # Pass to base class + BPMRule.expand(self, applied_rule, force=force, **kw) + + # Matrix related + security.declareProtected( Permissions.ModifyPortalContent, + 'newCellContent' ) + def newCellContent(self, id, portal_type='Accounting Rule Cell', **kw): + """ + Creates a new Cell. + """ + self.invokeFactory(type_name=portal_type, id=id) + new_cell = self.get(id) + return new_cell + + # Deliverability / orderability + def isOrderable(self, m): + return 1 + + def isDeliverable(self, m): + if m.getSimulationState() in self.getPortalDraftOrderStateList(): + return 0 + return 1 diff --git a/product/ERP5/Document/BPMInvoicingRule.py b/product/ERP5/Document/BPMInvoicingRule.py new file mode 100644 index 0000000000..305f97b739 --- /dev/null +++ b/product/ERP5/Document/BPMInvoicingRule.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved. +# Sebastien Robin <seb@nexedi.com> +# Romain Courteaud <romain@nexedi.com> +# Åukasz Nowak <luke@nexedi.com> +# +# 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., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, +# USA. +# +############################################################################## + +from AccessControl import ClassSecurityInfo +from Products.ERP5Type import Permissions, PropertySheet +from Products.ERP5.Document.BPMRule import BPMRule + +class BPMInvoicingRule(BPMRule): + """ + DISCLAIMER: Refer to BPMRule docstring disclaimer. + + This is BPM enabled Invoicing Rule + """ + + # CMF Type Definition + meta_type = 'ERP5 BPM Invoicing Rule' + portal_type = 'BPM Invoicing Rule' + add_permission = Permissions.AddPortalContent + isPortalContent = 1 + isRADContent = 1 + + # Declarative security + security = ClassSecurityInfo() + security.declareObjectProtected(Permissions.AccessContentsInformation) + + # Default Properties + property_sheets = ( PropertySheet.Base + , PropertySheet.XMLObject + , PropertySheet.CategoryCore + , PropertySheet.DublinCore + , PropertySheet.Task + , PropertySheet.AppliedRule + ) + + security.declareProtected(Permissions.AccessContentsInformation, + 'isAccountable') + def isAccountable(self, movement): + """ + Tells whether generated movement needs to be accounted or not. + + Invoice movement are never accountable, so simulation movement for + invoice movements should not be accountable either. + """ + return 0 + +#### Helper method for expand + def _generatePrevisionList(self, applied_rule, **kw): + """ + Generate a list of movements, that should be children of this rule, + based on its context (parent movement, delivery, configuration ...) + + These previsions are returned as dictionaries. + """ + # XXX Isn't it better to share the code with expand method + context_movement = applied_rule.getParentValue() + business_process = applied_rule.getBusinessProcessValue() + + movement_and_path_list = [] + for business_path in business_process.getPathValueList( + self.getProperty('trade_phase_list'), + context_movement): + movement_and_path_list.append((context_movement, business_path)) + + if len(movement_and_path_list) > 1: + raise NotImplementedError + + for movement, business_path in movement_and_path_list: + property_dict = self._getExpandablePropertyDict( + applied_rule, movement, business_path) + property_dict['deliverable'] = 1 + return [property_dict] + + security.declareProtected(Permissions.ModifyPortalContent, 'expand') + def expand(self, applied_rule, force=0, **kw): + """ + Expands the rule: + - generate a list of previsions + - compare the prevision with existing children + - get the list of existing movements (immutable, mutable, deletable) + - compute the difference between prevision and existing (add, + modify, remove) + - add/modify/remove child movements to match prevision + """ + parent_movement = applied_rule.getParentValue() + if parent_movement is not None: + if not parent_movement.isFrozen(): + add_list, modify_dict, \ + delete_list = self._getCompensatedMovementList(applied_rule, **kw) + for movement_id in delete_list: + applied_rule._delObject(movement_id) + + for movement, prop_dict in modify_dict.items(): + applied_rule[movement].edit(**prop_dict) + + for movement_dict in add_list: + if 'id' in movement_dict.keys(): + mvmt_id = applied_rule._get_id(movement_dict.pop('id')) + new_mvmt = applied_rule.newContent(id=mvmt_id, + portal_type=self.movement_type) + else: + new_mvmt = applied_rule.newContent(portal_type=self.movement_type) + new_mvmt.edit(**movement_dict) + + # Pass to base class + BPMRule.expand(self, applied_rule, force=force, **kw) + + def isDeliverable(self, movement): + return movement.getResource() is not None + diff --git a/product/ERP5/Document/BPMOrderRule.py b/product/ERP5/Document/BPMOrderRule.py new file mode 100644 index 0000000000..733855600e --- /dev/null +++ b/product/ERP5/Document/BPMOrderRule.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved. +# Jean-Paul Smets-Solanes <jp@nexedi.com> +# Romain Courteaud <romain@nexedi.com> +# Åukasz Nowak <luke@nexedi.com> +# +# 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.BPMRule import BPMRule +from Products.ERP5.Document.BPMDeliveryRule import BPMDeliveryRule +from zLOG import LOG, WARNING + +class BPMOrderRule(BPMDeliveryRule): + """ + DISCLAIMER: Refer to BPMRule docstring disclaimer. + + This is BPM enabled Order Rule. + """ + # CMF Type Definition + meta_type = 'ERP5 BPM Order Rule' + portal_type = 'BPM Order Rule' + + # Declarative security + security = ClassSecurityInfo() + security.declareObjectProtected(Permissions.AccessContentsInformation) + + # Default Properties + property_sheets = ( PropertySheet.Base + , PropertySheet.XMLObject + , PropertySheet.CategoryCore + , PropertySheet.DublinCore + , PropertySheet.Task + , PropertySheet.AppliedRule + ) + + # 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. + """ + + existing_movement_list = [] + immutable_movement_list = [] + order = applied_rule.getDefaultCausalityValue() + business_process = applied_rule.getBusinessProcessValue() + if order is not None: + order_movement_list = order.getMovementList( + portal_type=order.getPortalOrderMovementTypeList()) + # check existing movements + for simulation_movement in applied_rule.contentValues( + portal_type=self.movement_type): + if (not simulation_movement.getLastExpandSimulationState() in + order.getPortalReservedInventoryStateList() and + not simulation_movement.getLastExpandSimulationState() in + order.getPortalCurrentInventoryStateList()) and \ + not simulation_movement._isTreeDelivered(): + + movement_order = simulation_movement.getOrderValue() + if movement_order in order_movement_list: + existing_movement_list.append(simulation_movement) + else: + applied_rule._delObject(simulation_movement.getId()) + else: + existing_movement_list.append(simulation_movement) + immutable_movement_list.append(simulation_movement) + + # this dict simulates getOrderRelatedValue, but it will not work if an + # order was generated from multiple applied rules + order_movement_dict = {} + for s_m in applied_rule.objectValues(): + order_movement = s_m.getOrderValue() + if order_movement is not None: + order_movement_dict[order_movement.getPath()] = s_m + + # Create or modify movements + for order_movement in order_movement_list: + related_order = order_movement_dict.get(order_movement.getPath(), + None) + if related_order is None: + related_order = order_movement.getOrderRelatedValue() + + movement_and_path_list = [] + for business_path in business_process.getPathValueList( + self.getProperty('trade_phase_list'), + order_movement): + movement_and_path_list.append((order_movement, business_path)) + + if len(movement_and_path_list) > 1: + raise NotImplementedError + + for movement, business_path in movement_and_path_list: + property_dict = self._getExpandablePropertyDict( + applied_rule, movement, + business_path) + property_dict.update(order_value=order_movement) + if related_order is None: + # Generate a simulation movement + # Do not try to create meaningfull IDs, as order movement can be + # hierarchical + applied_rule.newContent( + portal_type=self.movement_type, + order_ratio=1, + delivery_ratio=1, + deliverable=1, + **property_dict) + elif related_order in existing_movement_list: + if related_order not in immutable_movement_list: + # modification allowed + related_order.edit( + **property_dict) + else: + # modification disallowed, must compensate + raise NotImplementedError('BPM *have* to support compensation') + + # Now we can set the last expand simulation state to the current + # state + applied_rule.setLastExpandSimulationState( + order.getSimulationState()) + # Pass to base class + BPMRule.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 BPMRule.isDivergent(self, movement) diff --git a/product/ERP5/Document/BPMRule.py b/product/ERP5/Document/BPMRule.py new file mode 100644 index 0000000000..f55cd8729a --- /dev/null +++ b/product/ERP5/Document/BPMRule.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved. +# Jean-Paul Smets-Solanes <jp@nexedi.com> +# Åukasz Nowak <luke@nexedi.com> +# +# 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 zope.interface +from AccessControl import ClassSecurityInfo +from Products.ERP5Type import Permissions, PropertySheet, interfaces +from Products.ERP5.Document.Rule import Rule + +class BPMRule(Rule): + """ + DISCLAIMER: Do not use this in any production system. + This is only proof of concept and evaluation of new system + design. Implementation and API can change without any + further warning. + + *DO NOT USE IN PRODUCTION SYSTEM* + + This is BPM enabled Rule system Base class. + """ + + # CMF Type Definition + meta_type = 'ERP5 BPM Rule' + portal_type = 'BPM Rule' + add_permission = Permissions.AddPortalContent + isPortalContent = 1 + isRADContent = 1 + isPredicate = 1 + + # Declarative security + security = ClassSecurityInfo() + security.declareObjectProtected(Permissions.AccessContentsInformation) + + zope.interface.implements( interfaces.IPredicate, + interfaces.IRule ) + + # Default Properties + property_sheets = ( PropertySheet.Base + , PropertySheet.XMLObject + , PropertySheet.CategoryCore + , PropertySheet.DublinCore + , PropertySheet.Task + , PropertySheet.Predicate + , PropertySheet.Reference + , PropertySheet.Version + ) + +#### Helpers + def _getCurrentMovementList(self, applied_rule, **kw): + """ + Returns the list of current children of the applied rule, sorted in 3 + groups : immutables/mutables/deletable + + * immutable is frozen + * mutable is not frozen, but delivered + * deletable is not frozen and not delivered + + Delivered means movement is delivered or any of its children is delivered. + """ + immutable_movement_list = [] + mutable_movement_list = [] + deletable_movement_list = [] + + for movement in applied_rule.contentValues( + portal_type=self.movement_type): + if movement.isFrozen(): + immutable_movement_list.append(movement) + else: + if movement._isTreeDelivered(): + mutable_movement_list.append(movement) + else: + deletable_movement_list.append(movement) + + return (immutable_movement_list, mutable_movement_list, + deletable_movement_list) + + def _getCompensatedMovementList(self, applied_rule, + matching_property_list=( + 'resource', + 'variation_category_list', + 'variation_property_dict',), **kw): + """ + Compute the difference between prevision and existing movements + + immutable movements need compensation, mutables needs to be modified + + XXX For now, this implementation is too simple. It could be improved by + using MovementGroups + """ + add_list = [] # list of movements to be added + modify_dict = {} # dict of movements to be modified + delete_list = [] # list of movements to be deleted + + prevision_list = self._generatePrevisionList(applied_rule, **kw) + immutable_movement_list, mutable_movement_list, \ + deletable_movement_list = self._getCurrentMovementList(applied_rule, + **kw) + movement_list = immutable_movement_list + mutable_movement_list \ + + deletable_movement_list + non_matched_list = movement_list[:] # list of remaining movements + + for prevision in prevision_list: + p_matched_list = [] + for movement in non_matched_list: + for prop in matching_property_list: + if prevision.get(prop) != movement.getProperty(prop): + break + else: + p_matched_list.append(movement) + + # Movements exist, we'll try to make them match the prevision + if p_matched_list != []: + # Check the quantity + m_quantity = 0.0 + for movement in p_matched_list: + m_quantity += movement.getQuantity()#getCorrectedQuantity() + if m_quantity != prevision.get('quantity'): + q_diff = prevision.get('quantity') - m_quantity + # try to find a movement that can be edited + for movement in p_matched_list: + if movement in (mutable_movement_list \ + + deletable_movement_list): + # mark as requiring modification + prop_dict = modify_dict.setdefault(movement.getId(), {}) + #prop_dict['quantity'] = movement.getCorrectedQuantity() + \ + prop_dict['quantity'] = movement.getQuantity() + \ + q_diff + break + # no modifiable movement was found, need to create one + else: + prevision['quantity'] = q_diff + add_list.append(prevision) + + # Check the date + for movement in p_matched_list: + if movement in (mutable_movement_list \ + + deletable_movement_list): + prop_dict = modify_dict.setdefault(movement.getId(), {}) + for prop in ('start_date', 'stop_date'): + #XXX should be >= 15 + if prevision.get(prop) != movement.getProperty(prop): + prop_dict[prop] = prevision.get(prop) + break + + for k, v in prevision.items(): + if k not in ('quantity', 'start_date', 'stop_date') and \ + v != movement.getProperty(k): + prop_dict.setdefault(k, v) + + # update movement lists + for movement in p_matched_list: + non_matched_list.remove(movement) + + # No movement matched, we need to create one + else: + add_list.append(prevision) + + # delete non matched movements + for movement in non_matched_list: + if movement in deletable_movement_list: + # delete movement + delete_list.append(movement.getId()) + elif movement in mutable_movement_list: + # set movement quantity to 0 to make it "void" + prop_dict = modify_dict.setdefault(movement.getId(), {}) + prop_dict['quantity'] = 0.0 + else: + # movement not modifiable, we can decide to create a compensation + # with negative quantity + raise NotImplementedError( + "Can not create a compensation movement for %s" % \ + movement.getRelativeUrl()) + return (add_list, modify_dict, delete_list) + + security.declareProtected(Permissions.AccessContentsInformation, + '_getExpandablePropertyDict') + def _getExpandablePropertyDict(self, applied_rule, movement, business_path, + **kw): + """ + Return a Dictionary with the Properties used to edit + the simulation movement. A supply path can be passed to assign more + information + """ + property_dict = {} + + default_property_list = self.getExpandablePropertyList() + for prop in default_property_list: + property_dict[prop] = movement.getProperty(prop) + + # Arrow + for base_category in \ + business_path.getSourceBaseCategoryList() +\ + business_path.getDestinationBaseCategoryList(): + property_dict[base_category] = business_path\ + .getDefaultAcquiredCategoryMembership(base_category, + context=movement) + # Amount + if business_path.getQuantity(): + property_dict['quantity'] = business_path.getQuantity() + elif business_path.getEfficiency(): + property_dict['quantity'] = movement.getQuantity() *\ + business_path.getEfficiency() + else: + property_dict['quantity'] = movement.getQuantity() + + if movement.getStartDate() == movement.getStopDate(): + property_dict['start_date'] = business_path.getExpectedStartDate( + movement) + property_dict['stop_date'] = business_path.getExpectedStopDate(movement) + else: # XXX shall not be used, but business_path.getExpectedStart/StopDate + # do not works on second path... + property_dict['start_date'] = movement.getStartDate() + property_dict['stop_date'] = movement.getStopDate() + + # save a relation to supply path for builders XXX which category + property_dict['causality_value'] = business_path + + return property_dict -- 2.30.9