diff --git a/bt5/erp5_accounting/DocumentTemplateItem/portal_components/document.erp5.AccountingTransactionRootSimulationRule.py b/bt5/erp5_accounting/DocumentTemplateItem/portal_components/document.erp5.AccountingTransactionRootSimulationRule.py index 6d6b5f6e801c5433726c9967dab6cdfc040e9547..46d02b162332475847584001b58b17fa336018ac 100644 --- a/bt5/erp5_accounting/DocumentTemplateItem/portal_components/document.erp5.AccountingTransactionRootSimulationRule.py +++ b/bt5/erp5_accounting/DocumentTemplateItem/portal_components/document.erp5.AccountingTransactionRootSimulationRule.py @@ -28,8 +28,8 @@ from AccessControl import ClassSecurityInfo from Products.ERP5Type import Permissions, PropertySheet -from Products.ERP5.mixin.rule import RuleMixin -from Products.ERP5.mixin.movement_collection_updater import MovementCollectionUpdaterMixin +from erp5.component.mixin.RuleMixin import RuleMixin +from erp5.component.mixin.MovementCollectionUpdaterMixin import MovementCollectionUpdaterMixin class AccountingTransactionRootSimulationRule(RuleMixin, MovementCollectionUpdaterMixin): """ Accounting Transaction Root Simulation Rule is a root level rule for diff --git a/bt5/erp5_accounting/DocumentTemplateItem/portal_components/document.erp5.PaymentSimulationRule.py b/bt5/erp5_accounting/DocumentTemplateItem/portal_components/document.erp5.PaymentSimulationRule.py index f69cc8cace26cda4945279ff5df2f952d07e0f15..d6d92168e6b47ca05114a843795f5235955d78a2 100644 --- a/bt5/erp5_accounting/DocumentTemplateItem/portal_components/document.erp5.PaymentSimulationRule.py +++ b/bt5/erp5_accounting/DocumentTemplateItem/portal_components/document.erp5.PaymentSimulationRule.py @@ -28,11 +28,13 @@ import zope.interface from AccessControl import ClassSecurityInfo -from Products.ERP5Type import Permissions, PropertySheet, interfaces -from Products.ERP5.mixin.rule import RuleMixin +from Products.ERP5Type import Permissions, PropertySheet +from erp5.component.mixin.RuleMixin import RuleMixin from Products.ERP5.mixin.movement_generator import MovementGeneratorMixin -from Products.ERP5.mixin.movement_collection_updater import \ - MovementCollectionUpdaterMixin +from erp5.component.mixin.MovementCollectionUpdaterMixin import MovementCollectionUpdaterMixin +from erp5.component.interface.IRule import IRule +from erp5.component.interface.IDivergenceController import IDivergenceController +from erp5.component.interface.IMovementCollectionUpdater import IMovementCollectionUpdater class PaymentSimulationRule(RuleMixin, MovementCollectionUpdaterMixin): """ @@ -48,9 +50,9 @@ class PaymentSimulationRule(RuleMixin, MovementCollectionUpdaterMixin): security.declareObjectProtected(Permissions.AccessContentsInformation) # Declarative interfaces - zope.interface.implements(interfaces.IRule, - interfaces.IDivergenceController, - interfaces.IMovementCollectionUpdater,) + zope.interface.implements(IRule, + IDivergenceController, + IMovementCollectionUpdater,) # Default Properties property_sheets = ( diff --git a/bt5/erp5_base/MixinTemplateItem/portal_components/mixin.erp5.BuilderMixin.py b/bt5/erp5_base/MixinTemplateItem/portal_components/mixin.erp5.BuilderMixin.py index bbe807af93af554b1209358afa8ac3360e7057f4..90f21bc7d13f2ab7d2e84e6ce899d409999741fa 100644 --- a/bt5/erp5_base/MixinTemplateItem/portal_components/mixin.erp5.BuilderMixin.py +++ b/bt5/erp5_base/MixinTemplateItem/portal_components/mixin.erp5.BuilderMixin.py @@ -33,7 +33,7 @@ from Products.ERP5Type import Permissions, PropertySheet from Products.ERP5Type.XMLObject import XMLObject from Products.ERP5Type.Core.Predicate import Predicate from Products.ERP5.Document.Amount import Amount -from Products.ERP5.MovementGroup import MovementGroupNode +from erp5.component.module.MovementGroup import MovementGroupNode from Products.ERP5Type.TransactionalVariable import getTransactionalVariable from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod from Products.ERP5.ExplanationCache import _getExplanationCache diff --git a/bt5/erp5_commerce_loyalty_program/DocumentTemplateItem/portal_components/document.erp5.LoyaltyTransactionSimulationRule.py b/bt5/erp5_commerce_loyalty_program/DocumentTemplateItem/portal_components/document.erp5.LoyaltyTransactionSimulationRule.py index 54944b98b2662c02462b0e911868efa520c44cae..72e8179f1a4cac776afe55b816a844c3d6958f1e 100644 --- a/bt5/erp5_commerce_loyalty_program/DocumentTemplateItem/portal_components/document.erp5.LoyaltyTransactionSimulationRule.py +++ b/bt5/erp5_commerce_loyalty_program/DocumentTemplateItem/portal_components/document.erp5.LoyaltyTransactionSimulationRule.py @@ -29,12 +29,15 @@ import zope.interface from AccessControl import ClassSecurityInfo -from Products.ERP5Type import Permissions, PropertySheet, interfaces -from Products.ERP5.mixin.rule import RuleMixin +from Products.ERP5Type import Permissions, PropertySheet +from erp5.component.mixin.RuleMixin import RuleMixin from Products.ERP5.mixin.movement_generator import MovementGeneratorMixin -from Products.ERP5.mixin.movement_collection_updater import \ +from erp5.component.mixin.MovementCollectionUpdaterMixin import \ MovementCollectionUpdaterMixin from Acquisition import aq_base +from erp5.component.interface.IRule import IRule +from erp5.component.interface.IDivergenceController import IDivergenceController +from erp5.component.interface.IMovementCollectionUpdater import IMovementCollectionUpdater class LoyaltyTransactionSimulationRule(RuleMixin,MovementCollectionUpdaterMixin): """ """ @@ -47,9 +50,9 @@ class LoyaltyTransactionSimulationRule(RuleMixin,MovementCollectionUpdaterMixin) security.declareObjectProtected(Permissions.AccessContentsInformation) # Declarative interfaces - zope.interface.implements(interfaces.IRule, - interfaces.IDivergenceController, - interfaces.IMovementCollectionUpdater,) + zope.interface.implements(IRule, + IDivergenceController, + IMovementCollectionUpdater,) # Default Properties property_sheets = ( diff --git a/bt5/erp5_immobilisation/DocumentTemplateItem/portal_components/document.erp5.AmortisationRule.py b/bt5/erp5_immobilisation/DocumentTemplateItem/portal_components/document.erp5.AmortisationRule.py index 5feb67c616bfba7c38ce40a657f024350aefcbb3..d2f9bbb6c98fa03c016b2badcf4d28d87d9e9a3a 100644 --- a/bt5/erp5_immobilisation/DocumentTemplateItem/portal_components/document.erp5.AmortisationRule.py +++ b/bt5/erp5_immobilisation/DocumentTemplateItem/portal_components/document.erp5.AmortisationRule.py @@ -33,7 +33,7 @@ from string import capitalize from Products.ERP5Type.DateUtils import centis, getClosestDate, addToDate from Products.ERP5Type.DateUtils import getDecimalNumberOfYearsBetween from Products.ERP5Type import Permissions -from Products.ERP5.mixin.rule import RuleMixin +from erp5.component.mixin.RuleMixin import RuleMixin from Products.CMFCore.utils import getToolByName from Products.ERP5.Document.ImmobilisationMovement import NO_CHANGE_METHOD diff --git a/bt5/erp5_invoicing/DocumentTemplateItem/portal_components/document.erp5.InvoiceSimulationRule.py b/bt5/erp5_invoicing/DocumentTemplateItem/portal_components/document.erp5.InvoiceSimulationRule.py index 436025b9f624557ee6125a8c875a10e486725298..300305b0f9e5ebda54021d37adde66b7830d3979 100644 --- a/bt5/erp5_invoicing/DocumentTemplateItem/portal_components/document.erp5.InvoiceSimulationRule.py +++ b/bt5/erp5_invoicing/DocumentTemplateItem/portal_components/document.erp5.InvoiceSimulationRule.py @@ -28,11 +28,14 @@ import zope.interface from AccessControl import ClassSecurityInfo -from Products.ERP5Type import Permissions, PropertySheet, interfaces -from Products.ERP5.mixin.rule import RuleMixin +from Products.ERP5Type import Permissions, PropertySheet +from erp5.component.mixin.RuleMixin import RuleMixin from Products.ERP5.mixin.movement_generator import MovementGeneratorMixin -from Products.ERP5.mixin.movement_collection_updater import \ +from erp5.component.mixin.MovementCollectionUpdaterMixin import \ MovementCollectionUpdaterMixin +from erp5.component.interface.IRule import IRule +from erp5.component.interface.IDivergenceController import IDivergenceController +from erp5.component.interface.IMovementCollectionUpdater import IMovementCollectionUpdater class InvoiceSimulationRule(RuleMixin, MovementCollectionUpdaterMixin): """ @@ -47,9 +50,9 @@ class InvoiceSimulationRule(RuleMixin, MovementCollectionUpdaterMixin): security.declareObjectProtected(Permissions.AccessContentsInformation) # Declarative interfaces - zope.interface.implements(interfaces.IRule, - interfaces.IDivergenceController, - interfaces.IMovementCollectionUpdater,) + zope.interface.implements(IRule, + IDivergenceController, + IMovementCollectionUpdater,) # Default Properties property_sheets = ( diff --git a/bt5/erp5_invoicing/DocumentTemplateItem/portal_components/document.erp5.InvoiceTransactionSimulationRule.py b/bt5/erp5_invoicing/DocumentTemplateItem/portal_components/document.erp5.InvoiceTransactionSimulationRule.py index c134d989d8e46eb5be16d633312f41bc7d1f2a41..da95e5e2fb60266ab6908242f26afc45f08d8631 100644 --- a/bt5/erp5_invoicing/DocumentTemplateItem/portal_components/document.erp5.InvoiceTransactionSimulationRule.py +++ b/bt5/erp5_invoicing/DocumentTemplateItem/portal_components/document.erp5.InvoiceTransactionSimulationRule.py @@ -28,13 +28,15 @@ import zope.interface from AccessControl import ClassSecurityInfo -from Products.ERP5Type import Permissions, PropertySheet, interfaces -from Products.ERP5.mixin.rule import RuleMixin +from Products.ERP5Type import Permissions, PropertySheet +from erp5.component.mixin.RuleMixin import RuleMixin from Products.ERP5.mixin.movement_generator import MovementGeneratorMixin -from Products.ERP5.mixin.movement_collection_updater import \ +from erp5.component.mixin.MovementCollectionUpdaterMixin import \ MovementCollectionUpdaterMixin from Products.ERP5.Document.PredicateMatrix import PredicateMatrix - +from erp5.component.interface.IRule import IRule +from erp5.component.interface.IDivergenceController import IDivergenceController +from erp5.component.interface.IMovementCollectionUpdater import IMovementCollectionUpdater class InvoiceTransactionSimulationRule(RuleMixin, MovementCollectionUpdaterMixin, PredicateMatrix): @@ -53,9 +55,9 @@ class InvoiceTransactionSimulationRule(RuleMixin, security.declareObjectProtected(Permissions.AccessContentsInformation) # Declarative interfaces - zope.interface.implements(interfaces.IRule, - interfaces.IDivergenceController, - interfaces.IMovementCollectionUpdater,) + zope.interface.implements(IRule, + IDivergenceController, + IMovementCollectionUpdater,) # Default Properties property_sheets = ( diff --git a/bt5/erp5_invoicing/DocumentTemplateItem/portal_components/document.erp5.TradeModelSimulationRule.py b/bt5/erp5_invoicing/DocumentTemplateItem/portal_components/document.erp5.TradeModelSimulationRule.py index 78c8b3d1f851aace2d0a0e2e9f76e89942a2e216..d8415e25198dde257a8d497409438353b7abbe7c 100644 --- a/bt5/erp5_invoicing/DocumentTemplateItem/portal_components/document.erp5.TradeModelSimulationRule.py +++ b/bt5/erp5_invoicing/DocumentTemplateItem/portal_components/document.erp5.TradeModelSimulationRule.py @@ -29,11 +29,14 @@ import zope.interface from AccessControl import ClassSecurityInfo from Acquisition import aq_base -from Products.ERP5Type import Permissions, PropertySheet, interfaces -from Products.ERP5.mixin.rule import RuleMixin +from Products.ERP5Type import Permissions, PropertySheet +from erp5.component.mixin.RuleMixin import RuleMixin from Products.ERP5.mixin.movement_generator import MovementGeneratorMixin -from Products.ERP5.mixin.movement_collection_updater import \ +from erp5.component.mixin.MovementCollectionUpdaterMixin import \ MovementCollectionUpdaterMixin +from erp5.component.interface.IRule import IRule +from erp5.component.interface.IDivergenceController import IDivergenceController +from erp5.component.interface.IMovementCollectionUpdater import IMovementCollectionUpdater class TradeModelSimulationRule(RuleMixin, MovementCollectionUpdaterMixin): """ @@ -48,9 +51,9 @@ class TradeModelSimulationRule(RuleMixin, MovementCollectionUpdaterMixin): security.declareObjectProtected(Permissions.AccessContentsInformation) # Declarative interfaces - zope.interface.implements(interfaces.IRule, - interfaces.IDivergenceController, - interfaces.IMovementCollectionUpdater,) + zope.interface.implements(IRule, + IDivergenceController, + IMovementCollectionUpdater,) # Default Properties property_sheets = ( diff --git a/bt5/erp5_mrp/DocumentTemplateItem/portal_components/document.erp5.TransformationSimulationRule.py b/bt5/erp5_mrp/DocumentTemplateItem/portal_components/document.erp5.TransformationSimulationRule.py index e844625c72c5ddd925ed914af32495731dc9c6ee..aebe1367bb20e25f00f4a85f817231d55508dacf 100644 --- a/bt5/erp5_mrp/DocumentTemplateItem/portal_components/document.erp5.TransformationSimulationRule.py +++ b/bt5/erp5_mrp/DocumentTemplateItem/portal_components/document.erp5.TransformationSimulationRule.py @@ -30,9 +30,9 @@ from AccessControl import ClassSecurityInfo from Acquisition import aq_base from Products.ERP5Type import Permissions, PropertySheet from Products.ERP5Type.TransactionalVariable import getTransactionalVariable -from Products.ERP5.mixin.rule import RuleMixin +from erp5.component.mixin.RuleMixin import RuleMixin from Products.ERP5.mixin.movement_generator import MovementGeneratorMixin -from Products.ERP5.mixin.movement_collection_updater import \ +from erp5.component.mixin.MovementCollectionUpdaterMixin import \ MovementCollectionUpdaterMixin class TransformationSimulationRule(RuleMixin, MovementCollectionUpdaterMixin): diff --git a/bt5/erp5_mrp/DocumentTemplateItem/portal_components/document.erp5.TransformationSourcingSimulationRule.py b/bt5/erp5_mrp/DocumentTemplateItem/portal_components/document.erp5.TransformationSourcingSimulationRule.py index d88b661e999e5b14fbc6308f7571d0521445cb11..901eea2637b9428fecc941b887f5ce494c387297 100644 --- a/bt5/erp5_mrp/DocumentTemplateItem/portal_components/document.erp5.TransformationSourcingSimulationRule.py +++ b/bt5/erp5_mrp/DocumentTemplateItem/portal_components/document.erp5.TransformationSourcingSimulationRule.py @@ -29,9 +29,9 @@ from AccessControl import ClassSecurityInfo from Acquisition import aq_base from Products.ERP5Type import Permissions, PropertySheet -from Products.ERP5.mixin.rule import RuleMixin +from erp5.component.mixin.RuleMixin import RuleMixin from Products.ERP5.mixin.movement_generator import MovementGeneratorMixin -from Products.ERP5.mixin.movement_collection_updater import \ +from erp5.component.mixin.MovementCollectionUpdaterMixin import \ MovementCollectionUpdaterMixin class TransformationSourcingSimulationRule(RuleMixin, MovementCollectionUpdaterMixin): diff --git a/bt5/erp5_real_time_inventory_accounting/DocumentTemplateItem/portal_components/document.erp5.InventoryAssetPriceAccountingSimulationRule.py b/bt5/erp5_real_time_inventory_accounting/DocumentTemplateItem/portal_components/document.erp5.InventoryAssetPriceAccountingSimulationRule.py index 03747bbc7e58db0a84e4ff47337d75506a48c407..878b588d983f548d817faabd4a95777912c61016 100644 --- a/bt5/erp5_real_time_inventory_accounting/DocumentTemplateItem/portal_components/document.erp5.InventoryAssetPriceAccountingSimulationRule.py +++ b/bt5/erp5_real_time_inventory_accounting/DocumentTemplateItem/portal_components/document.erp5.InventoryAssetPriceAccountingSimulationRule.py @@ -36,7 +36,7 @@ class InventoryAssetPriceAccountingRuleMovementGenerator(InvoiceTransactionRuleM meta_type = 'ERP5 Inventory Asset Price Accounting Simulation Rule' portal_type = 'Inventory Asset Price Accounting Simulation Rule' - # XXX: Copy/paste from Products.ERP5.mixin.rule to support Transit use case + # XXX: Copy/paste from erp5.component.mixin.RuleMixin to support Transit use case def getGeneratedMovementList(self, movement_list=None, rounding=False): """ Returns a list of movements generated by that rule. diff --git a/bt5/erp5_simulation/DocumentTemplateItem/portal_components/document.erp5.ItemListSplitSolver.py b/bt5/erp5_simulation/DocumentTemplateItem/portal_components/document.erp5.ItemListSplitSolver.py index 4d166bc47b7f7a895992901c0401c7fc2bbec404..c0dde7d5b8e5ece25a0650cd7019dc2bea24ee49 100644 --- a/bt5/erp5_simulation/DocumentTemplateItem/portal_components/document.erp5.ItemListSplitSolver.py +++ b/bt5/erp5_simulation/DocumentTemplateItem/portal_components/document.erp5.ItemListSplitSolver.py @@ -34,7 +34,7 @@ from Products.ERP5Type import Permissions, PropertySheet from Products.ERP5Type.XMLObject import XMLObject from erp5.component.mixin.SolverMixin import SolverMixin from erp5.component.mixin.ConfigurableMixin import ConfigurableMixin -from Products.ERP5.MovementCollectionDiff import _getPropertyAndCategoryList +from erp5.component.module.MovementCollectionDiff import _getPropertyAndCategoryList from erp5.component.interface.ISolver import ISolver from erp5.component.interface.IConfigurable import IConfigurable diff --git a/bt5/erp5_simulation/DocumentTemplateItem/portal_components/document.erp5.QuantitySplitSolver.py b/bt5/erp5_simulation/DocumentTemplateItem/portal_components/document.erp5.QuantitySplitSolver.py index e32d84f8d7c61323ff8b31ece34f2af2a2c64a0a..b1837e6332e07ecf0ddfa99911960f67a85ba0cb 100644 --- a/bt5/erp5_simulation/DocumentTemplateItem/portal_components/document.erp5.QuantitySplitSolver.py +++ b/bt5/erp5_simulation/DocumentTemplateItem/portal_components/document.erp5.QuantitySplitSolver.py @@ -35,7 +35,7 @@ from Products.ERP5Type.XMLObject import XMLObject from Products.ERP5Type.Globals import PersistentMapping from erp5.component.mixin.SolverMixin import SolverMixin from erp5.component.mixin.ConfigurableMixin import ConfigurableMixin -from Products.ERP5.MovementCollectionDiff import _getPropertyAndCategoryList +from erp5.component.module.MovementCollectionDiff import _getPropertyAndCategoryList from erp5.component.interface.ISolver import ISolver from erp5.component.interface.IConfigurable import IConfigurable diff --git a/bt5/erp5_simulation/MixinTemplateItem/portal_components/mixin.erp5.EquivalenceTesterMixin.py b/bt5/erp5_simulation/MixinTemplateItem/portal_components/mixin.erp5.EquivalenceTesterMixin.py index c535880b12be43cec42ae1f5db0a72b2b41d99c0..3ee94bc00d1b8f8ad53c4997c68ec1f04191fea7 100644 --- a/bt5/erp5_simulation/MixinTemplateItem/portal_components/mixin.erp5.EquivalenceTesterMixin.py +++ b/bt5/erp5_simulation/MixinTemplateItem/portal_components/mixin.erp5.EquivalenceTesterMixin.py @@ -30,7 +30,7 @@ import zope.interface from AccessControl import ClassSecurityInfo from Products.ERP5Type.Globals import InitializeClass from Products.ERP5Type import Permissions -from Products.ERP5Type.DivergenceMessage import DivergenceMessage +from erp5.component.module.DivergenceMessage import DivergenceMessage from Products.ERP5Type.Message import Message from Products.PythonScripts.standard import html_quote as h from zLOG import LOG, WARNING diff --git a/bt5/erp5_simulation/ToolComponentTemplateItem/portal_components/tool.erp5.SolverProcessTool.py b/bt5/erp5_simulation/ToolComponentTemplateItem/portal_components/tool.erp5.SolverProcessTool.py index 1381588488e51d967ffe55a1bf8bb75413d83d0f..738c0b7c02e4486c750021334343e41873f116a0 100644 --- a/bt5/erp5_simulation/ToolComponentTemplateItem/portal_components/tool.erp5.SolverProcessTool.py +++ b/bt5/erp5_simulation/ToolComponentTemplateItem/portal_components/tool.erp5.SolverProcessTool.py @@ -31,10 +31,11 @@ import zope.interface from AccessControl import ClassSecurityInfo from Products.ERP5Type.Globals import InitializeClass -from Products.ERP5Type import Permissions, interfaces +from Products.ERP5Type import Permissions from Products.ERP5Type.Tool.BaseTool import BaseTool from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod +from erp5.component.interface.IDivergenceController import IDivergenceController class SolverProcessTool(BaseTool): """ Container for solver processes. @@ -48,7 +49,7 @@ class SolverProcessTool(BaseTool): security = ClassSecurityInfo() # Declarative interfaces - zope.interface.implements(interfaces.IDivergenceController, ) + zope.interface.implements(IDivergenceController, ) # IDivergenceController implementation security.declareProtected(Permissions.AccessContentsInformation, diff --git a/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.BusinessProcess.py b/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.BusinessProcess.py index f6b9a24791a1469214695af63c1e70bf4643803a..4bfd97cc8e1da7336d81f75de4f680d8162fb176 100644 --- a/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.BusinessProcess.py +++ b/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.BusinessProcess.py @@ -35,7 +35,7 @@ from Products.ERP5Type.TransactionalVariable import getTransactionalVariable from Products.ERP5Type.XMLObject import XMLObject from Products.ERP5.Document.Path import Path from Products.ERP5.ExplanationCache import _getExplanationCache, _getBusinessLinkClosure -from Products.ERP5.MovementCollectionDiff import _getPropertyAndCategoryList +from erp5.component.module.MovementCollectionDiff import _getPropertyAndCategoryList from erp5.component.interface.IBusinessProcess import IBusinessProcess import zope.interface diff --git a/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.DeliveryRootSimulationRule.py b/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.DeliveryRootSimulationRule.py index 2cf5e2bac3072502665d8f925483095db1ca743b..0b3d8c551bb5c6b037e00873cd55a18cd4530f30 100644 --- a/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.DeliveryRootSimulationRule.py +++ b/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.DeliveryRootSimulationRule.py @@ -28,11 +28,14 @@ import zope.interface from AccessControl import ClassSecurityInfo -from Products.ERP5Type import Permissions, PropertySheet, interfaces -from Products.ERP5.mixin.rule import RuleMixin +from Products.ERP5Type import Permissions, PropertySheet +from erp5.component.mixin.RuleMixin import RuleMixin from Products.ERP5.mixin.movement_generator import MovementGeneratorMixin -from Products.ERP5.mixin.movement_collection_updater import \ +from erp5.component.mixin.MovementCollectionUpdaterMixin import \ MovementCollectionUpdaterMixin +from erp5.component.interface.IRule import IRule +from erp5.component.interface.IDivergenceController import IDivergenceController +from erp5.component.interface.IMovementCollectionUpdater import IMovementCollectionUpdater class DeliveryRootSimulationRule(RuleMixin, MovementCollectionUpdaterMixin): """ @@ -50,9 +53,9 @@ class DeliveryRootSimulationRule(RuleMixin, MovementCollectionUpdaterMixin): security.declareObjectProtected(Permissions.AccessContentsInformation) # Declarative interfaces - zope.interface.implements(interfaces.IRule, - interfaces.IDivergenceController, - interfaces.IMovementCollectionUpdater,) + zope.interface.implements(IRule, + IDivergenceController, + IMovementCollectionUpdater,) # Default Properties property_sheets = ( diff --git a/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.DeliverySimulationRule.py b/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.DeliverySimulationRule.py index e96933ab543b28251398867b0051740935a7e935..45d9c6fcb727a7394fe830c392dac89f19c86f5b 100644 --- a/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.DeliverySimulationRule.py +++ b/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.DeliverySimulationRule.py @@ -28,11 +28,14 @@ import zope.interface from AccessControl import ClassSecurityInfo -from Products.ERP5Type import Permissions, PropertySheet, interfaces -from Products.ERP5.mixin.rule import RuleMixin +from Products.ERP5Type import Permissions, PropertySheet +from erp5.component.mixin.RuleMixin import RuleMixin from Products.ERP5.mixin.movement_generator import MovementGeneratorMixin -from Products.ERP5.mixin.movement_collection_updater import \ +from erp5.component.mixin.MovementCollectionUpdaterMixin import \ MovementCollectionUpdaterMixin +from erp5.component.interface.IRule import IRule +from erp5.component.interface.IDivergenceController import IDivergenceController +from erp5.component.interface.IMovementCollectionUpdater import IMovementCollectionUpdater class DeliverySimulationRule(RuleMixin, MovementCollectionUpdaterMixin): """ @@ -50,9 +53,9 @@ class DeliverySimulationRule(RuleMixin, MovementCollectionUpdaterMixin): security.declareObjectProtected(Permissions.AccessContentsInformation) # Declarative interfaces - zope.interface.implements(interfaces.IRule, - interfaces.IDivergenceController, - interfaces.IMovementCollectionUpdater,) + zope.interface.implements(IRule, + IDivergenceController, + IMovementCollectionUpdater,) # Default Properties property_sheets = ( diff --git a/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.OrderRootSimulationRule.py b/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.OrderRootSimulationRule.py index ce184bc8a2776ac1b0dc319efe4a82f4e836c84e..02d971fa6f270e67f186959d7746925d039e84db 100644 --- a/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.OrderRootSimulationRule.py +++ b/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.OrderRootSimulationRule.py @@ -28,11 +28,14 @@ import zope.interface from AccessControl import ClassSecurityInfo -from Products.ERP5Type import Permissions, PropertySheet, interfaces -from Products.ERP5.mixin.rule import RuleMixin +from Products.ERP5Type import Permissions, PropertySheet +from erp5.component.mixin.RuleMixin import RuleMixin from Products.ERP5.mixin.movement_generator import MovementGeneratorMixin -from Products.ERP5.mixin.movement_collection_updater import \ +from erp5.component.mixin.MovementCollectionUpdaterMixin import \ MovementCollectionUpdaterMixin +from erp5.component.interface.IRule import IRule +from erp5.component.interface.IDivergenceController import IDivergenceController +from erp5.component.interface.IMovementCollectionUpdater import IMovementCollectionUpdater class OrderRootSimulationRule(RuleMixin, MovementCollectionUpdaterMixin): """ @@ -50,9 +53,9 @@ class OrderRootSimulationRule(RuleMixin, MovementCollectionUpdaterMixin): security.declareObjectProtected(Permissions.AccessContentsInformation) # Declarative interfaces - zope.interface.implements(interfaces.IRule, - interfaces.IDivergenceController, - interfaces.IMovementCollectionUpdater,) + zope.interface.implements(IRule, + IDivergenceController, + IMovementCollectionUpdater,) # Default Properties property_sheets = ( diff --git a/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.TradeCondition.py b/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.TradeCondition.py index da83871cb3b9c151c42e524c72d698742daad8fe..46164729f51f9cb3ba091b93d61848c230ce01f6 100644 --- a/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.TradeCondition.py +++ b/bt5/erp5_trade/DocumentTemplateItem/portal_components/document.erp5.TradeCondition.py @@ -40,6 +40,7 @@ from Products.ERP5.mixin.composition import _getEffectiveModel from Products.ERP5.Document.MappedValue import MappedValue from Products.ERP5.mixin.amount_generator import AmountGeneratorMixin from Products.ERP5.mixin.variated import VariatedMixin +from erp5.component.interface.IMovementCollectionUpdater import IMovementCollectionUpdater class TradeCondition(MappedValue, AmountGeneratorMixin, VariatedMixin): """ @@ -74,7 +75,7 @@ class TradeCondition(MappedValue, AmountGeneratorMixin, VariatedMixin): zope.interface.implements(interfaces.IAmountGenerator, interfaces.IMovementGenerator, - interfaces.IMovementCollectionUpdater,) + IMovementCollectionUpdater,) # Mapped Value implementation diff --git a/product/ERP5/TargetSolver/SplitAndDefer.py b/product/ERP5/TargetSolver/SplitAndDefer.py index 16d5330b3c7ed32534f05d43951914ae69509b2d..a6aed2278cf9d2ad017d10fbd08dacfca366b705 100644 --- a/product/ERP5/TargetSolver/SplitAndDefer.py +++ b/product/ERP5/TargetSolver/SplitAndDefer.py @@ -27,7 +27,6 @@ # ############################################################################## -from Products.ERP5.MovementCollectionDiff import _getPropertyAndCategoryList from Products.ERP5Type.Globals import PersistentMapping from CopyToTarget import CopyToTarget from Acquisition import aq_base @@ -64,6 +63,7 @@ class SplitAndDefer(CopyToTarget): split_index += 1 new_id = "%s_split_%s" % (simulation_movement.getId(), split_index) # Adopt different dates for deferred movements + from erp5.component.module.MovementCollectionDiff import _getPropertyAndCategoryList movement_dict = _getPropertyAndCategoryList(simulation_movement) # new properties movement_dict.update( diff --git a/product/ERP5/Tool/SimulationTool.py b/product/ERP5/Tool/SimulationTool.py deleted file mode 100644 index 85b810016fdc7a205a8ecbfa19494817c5d408e8..0000000000000000000000000000000000000000 --- a/product/ERP5/Tool/SimulationTool.py +++ /dev/null @@ -1,3032 +0,0 @@ -############################################################################## -# -# Copyright (c) 2002, 2005 Nexedi SARL and Contributors. All Rights Reserved. -# Jean-Paul Smets-Solanes <jp@nexedi.com> -# Romain Courteaud <romain@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 Products.CMFCore.utils import getToolByName - -from AccessControl import ClassSecurityInfo -from Products.ERP5Type.Globals import InitializeClass, DTMLFile -from Products.ERP5Type import Permissions -from Products.ERP5Type.Tool.BaseTool import BaseTool -from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod - -from Products.ERP5 import _dtmldir - -from zLOG import LOG, PROBLEM, WARNING, INFO - -from Products.ERP5.Capacity.GLPK import solve -from numpy import zeros, resize -from DateTime import DateTime - -from Products.ERP5 import DeliverySolver -from Products.ERP5 import TargetSolver -from Products.PythonScripts.Utility import allow_class - -from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery, SimpleQuery - -from Shared.DC.ZRDB.Results import Results -from Products.ERP5Type.Utils import mergeZRDBResults -from App.Extensions import getBrain -from MySQLdb import ProgrammingError -from MySQLdb.constants.ER import NO_SUCH_TABLE - -from hashlib import md5 -from warnings import warn -from cPickle import loads, dumps -from copy import deepcopy - -MYSQL_MIN_DATETIME_RESOLUTION = 1/86400. - -class StockOptimisationError(Exception): - pass - -class SimulationTool(BaseTool): - """ - The SimulationTool implements the ERP5 - simulation algorithmics. - - - Examples of applications: - - - - - - - ERP5 main purpose: - - - - - - - - """ - id = 'portal_simulation' - meta_type = 'ERP5 Simulation Tool' - portal_type = 'Simulation Tool' - allowed_types = ( 'ERP5 Applied Rule', ) - - # Declarative Security - security = ClassSecurityInfo() - - # - # ZMI methods - # - security.declareProtected( Permissions.ManagePortal, 'manage_overview' ) - manage_overview = DTMLFile( 'explainSimulationTool', _dtmldir ) - - def filtered_meta_types(self, user=None): - # Filters the list of available meta types. - all = SimulationTool.inheritedAttribute('filtered_meta_types')(self) - meta_types = [] - for meta_type in self.all_meta_types(): - if meta_type['name'] in self.allowed_types: - meta_types.append(meta_type) - return meta_types - - def tpValues(self) : - """ show the content in the left pane of the ZMI """ - return self.objectValues() - - security.declarePrivate('manage_afterAdd') - def manage_afterAdd(self, item, container) : - """Init permissions right after creation. - - Permissions in simulation tool are simple: - o Each member can access and create some content. - o Only manager can view, because simulation can be seen as - sensitive information. - """ - item.manage_permission(Permissions.AddPortalContent, - ['Member', 'Author', 'Manager']) - item.manage_permission(Permissions.AccessContentsInformation, - ['Member', 'Auditor', 'Manager']) - item.manage_permission(Permissions.View, - ['Manager',]) - BaseTool.inheritedAttribute('manage_afterAdd')(self, item, container) - - security.declareProtected(Permissions.AccessContentsInformation, - 'solveDelivery') - def solveDelivery(self, delivery, delivery_solver_name, target_solver_name, - additional_parameters=None, **kw): - """ - XXX obsoleted API - - Solves a delivery by calling first DeliverySolver, then TargetSolver - """ - return self._solveMovementOrDelivery(delivery, delivery_solver_name, - target_solver_name, delivery=1, - additional_parameters=additional_parameters, **kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'solveMovement') - def solveMovement(self, movement, delivery_solver_name, target_solver_name, - additional_parameters=None, **kw): - """ - XXX obsoleted API - - Solves a movement by calling first DeliverySolver, then TargetSolver - """ - return self._solveMovementOrDelivery(movement, delivery_solver_name, - target_solver_name, movement=1, - additional_parameters=additional_parameters, **kw) - - def _solveMovementOrDelivery(self, document, delivery_solver_name, - target_solver_name, movement=0, delivery=0, - additional_parameters=None,**kw): - """ - Solves a document by calling first DeliverySolver, then TargetSolver - """ - if movement == delivery: - raise ValueError('Parameters movement and delivery have to be' - ' different') - - solve_result = [] - for solver_name, solver_module in ((delivery_solver_name, DeliverySolver), - (target_solver_name, TargetSolver)): - result = None - if solver_name is not None: - solver_file_path = "%s.%s" % (solver_module.__name__, - solver_name) - __import__(solver_file_path) - solver_file = getattr(solver_module, solver_name) - solver_class = getattr(solver_file, solver_name) - solver = solver_class(additional_parameters=additional_parameters, - **kw) - - if movement: - result = solver.solveMovement(document) - if delivery: - result = solver.solveDelivery(document) - solve_result.append(result) - return solve_result - - ####################################################### - # Stock Management - - def _generatePropertyUidList(self, prop, as_text=0): - """ - converts relative_url or text (single element or list or dict) - to an object usable by buildSQLQuery - - as_text == 0: tries to lookup an uid from the relative_url - as_text == 1: directly passes the argument as text - """ - if prop is None : - return [] - category_tool = getToolByName(self, 'portal_categories') - property_uid_list = [] - if isinstance(prop, str): - if not as_text: - prop_value = category_tool.getCategoryValue(prop) - if prop_value is None: - raise ValueError, 'Category %s does not exists' % prop - property_uid_list.append(prop_value.getUid()) - else: - property_uid_list.append(prop) - elif isinstance(prop, (list, tuple)): - for property_item in prop : - if not as_text: - prop_value = category_tool.getCategoryValue(property_item) - if prop_value is None: - raise ValueError, 'Category %s does not exists' % property_item - property_uid_list.append(prop_value.getUid()) - else: - property_uid_list.append(property_item) - elif isinstance(prop, dict): - tmp_uid_list = [] - if isinstance(prop['query'], str): - prop['query'] = [prop['query']] - for property_item in prop['query'] : - if not as_text: - prop_value = category_tool.getCategoryValue(property_item) - if prop_value is None: - raise ValueError, 'Category %s does not exists' % property_item - tmp_uid_list.append(prop_value.getUid()) - else: - tmp_uid_list.append(property_item) - if tmp_uid_list: - property_uid_list = {} - property_uid_list['operator'] = prop['operator'] - property_uid_list['query'] = tmp_uid_list - return property_uid_list - - def _getSimulationStateQuery(self, **kw): - simulation_state_dict = self._getSimulationStateDict(**kw) - return self._buildSimulationStateQuery(simulation_state_dict=simulation_state_dict) - - def _buildSimulationStateQuery(self, simulation_state_dict, table='stock'): - simulation_state = simulation_state_dict.get('simulation_state') - if simulation_state is not None: - return SimpleQuery(**{table + '.simulation_state': simulation_state}) - input_simulation_state = simulation_state_dict.get('input_simulation_state') - if input_simulation_state is not None: - simulation_query = ComplexQuery( - self._getIncreaseQuery(table, 'quantity', True), - SimpleQuery(**{table + '.simulation_state': input_simulation_state}), - logical_operator='AND', - ) - output_simulation_state = simulation_state_dict.get('output_simulation_state') - if output_simulation_state is not None: - simulation_query = ComplexQuery( - simulation_query, - ComplexQuery( - self._getIncreaseQuery(table, 'quantity', False), - SimpleQuery(**{table + '.simulation_state': output_simulation_state}), - logical_operator='AND', - ), - logical_operator='OR' - ) - return simulation_query - - def _getSimulationStateDict(self, simulation_state=None, omit_transit=0, - input_simulation_state=None, - output_simulation_state=None, - transit_simulation_state=None, - strict_simulation_state=None): - """ - This method is used in order to give what should be - the input_simulation_state or output_simulation_state - depending on many parameters - """ - string_or_list = (str, list, tuple) - # Simulation States - # If strict_simulation_state is set, we directly put it into the dictionary - simulation_dict = {} - if strict_simulation_state: - if isinstance(simulation_state, string_or_list)\ - and simulation_state: - simulation_query = SimpleQuery( - **{'stock.simulation_state': simulation_state}) - else: - # first, we evaluate simulation_state - sql_kw = {} - if simulation_state and isinstance(simulation_state, string_or_list): - if isinstance(simulation_state, str): - sql_kw['input_simulation_state'] = [simulation_state] - sql_kw['output_simulation_state'] = [simulation_state] - else: - sql_kw['input_simulation_state'] = simulation_state - sql_kw['output_simulation_state'] = simulation_state - # then, if omit_transit == 1, we evaluate (simulation_state - - # transit_simulation_state) for input_simulation_state - if omit_transit: - if isinstance(simulation_state, string_or_list)\ - and simulation_state: - if isinstance(transit_simulation_state, string_or_list)\ - and transit_simulation_state: - # when we know both are usable, we try to calculate - # (simulation_state - transit_simulation_state) - if isinstance(simulation_state, str): - simulation_state = [simulation_state] - if isinstance(transit_simulation_state, str) : - transit_simulation_state = [transit_simulation_state] - delivered_simulation_state_list = [] - for state in simulation_state : - if state not in transit_simulation_state : - delivered_simulation_state_list.append(state) - sql_kw['input_simulation_state'] = delivered_simulation_state_list - - # alternatively, the user can directly define input_simulation_state - # and output_simulation_state - if input_simulation_state and isinstance(input_simulation_state, - string_or_list): - if isinstance(input_simulation_state, str): - input_simulation_state = [input_simulation_state] - sql_kw['input_simulation_state'] = input_simulation_state - if output_simulation_state and isinstance(output_simulation_state, - string_or_list): - if isinstance(output_simulation_state, str): - output_simulation_state = [output_simulation_state] - sql_kw['output_simulation_state'] = output_simulation_state - # XXX In this case, we must not set sql_kw[input_simumlation_state] before - input_simulation_state = None - output_simulation_state = None - if sql_kw.has_key('input_simulation_state'): - input_simulation_state = sql_kw.get('input_simulation_state') - if sql_kw.has_key('output_simulation_state'): - output_simulation_state = sql_kw.get('output_simulation_state') - if input_simulation_state is not None \ - or output_simulation_state is not None: - sql_kw.pop('input_simulation_state',None) - sql_kw.pop('output_simulation_state',None) - if input_simulation_state is not None: - if output_simulation_state is not None: - if input_simulation_state == output_simulation_state: - simulation_dict['simulation_state'] = input_simulation_state - else: - simulation_dict['input_simulation_state'] = input_simulation_state - simulation_dict['output_simulation_state'] = output_simulation_state - else: - simulation_dict['input_simulation_state'] = input_simulation_state - elif output_simulation_state is not None: - simulation_dict['simulation_state'] = output_simulation_state - return simulation_dict - - def _getIncreaseQuery(self, table, column, increase, sql_catalog_id=None): - """ - Returns a Query filtering rows depending on whether they represent an - increase or a decrease. - table (string) - Name of table to use as stock table. - column (string) - Name of interesting column. Supported values are: - - total_price for asset price increase/decrease - - quantity for quantity increase (aka input)/decrease (aka output) - increase (bool) - False: decreasing rows are kept - True: increasing rows are kept - sql_catalog_id (string or None) - Idenfitier of an SQLCatalog object relevant to table, or None for - default one. - """ - if column == 'total_price': - dedicated_column = 'is_asset_increase' - elif column == 'quantity': - dedicated_column = 'is_input' - else: - raise ValueError('Unknown column %r' % (column, )) - if self.getPortalObject().portal_catalog.hasColumn( - dedicated_column, - sql_catalog_id, - ): - return SimpleQuery(**{dedicated_column: increase}) - # Dedicated columns are not present, compute on the fly. - return ComplexQuery( - ComplexQuery( - SimpleQuery(comparison_operator='<', **{table + '.' + column: 0}), - SimpleQuery(**{table + '.is_cancellation': increase}), - logical_operator='AND', - ), - ComplexQuery( - SimpleQuery(comparison_operator='>=', **{table + '.' + column: 0}), - SimpleQuery(**{table + '.is_cancellation': not increase}), - logical_operator='AND', - ), - logical_operator='OR', - ) - - def _generateSQLKeywordDict(self, table='stock', **kw): - sql_kw, new_kw = self._generateKeywordDict(**kw) - return self._generateSQLKeywordDictFromKeywordDict(table=table, - sql_kw=sql_kw, new_kw=new_kw) - - def _generateSQLKeywordDictFromKeywordDict(self, table='stock', sql_kw={}, - new_kw={}): - ctool = getToolByName(self, 'portal_catalog') - sql_kw = sql_kw.copy() - new_kw = new_kw.copy() - - # Group-by expression (eg. group_by=['node_uid']) - group_by = new_kw.pop('group_by_list', []) - - # group by from stock table (eg. group_by_node=True) - # prepend table name to avoid ambiguities. - column_group_by = new_kw.pop('column_group_by', []) - if column_group_by: - group_by.extend(['%s.%s' % (table, x) for x in column_group_by]) - - # group by from related keys columns (eg. group_by_node_category=True) - related_key_group_by = new_kw.pop('related_key_group_by', []) - if related_key_group_by: - group_by.extend(['%s_%s' % (table, x) for x in related_key_group_by]) - - # group by involving a related key (eg. group_by=['product_line_uid']) - related_key_dict_passthrough_group_by = new_kw.get( - 'related_key_dict_passthrough', {}).pop('group_by_list', []) - if isinstance(related_key_dict_passthrough_group_by, basestring): - related_key_dict_passthrough_group_by = ( - related_key_dict_passthrough_group_by,) - group_by.extend(related_key_dict_passthrough_group_by) - - if group_by: - new_kw['group_by_list'] = group_by - - # select expression - select_dict = new_kw.setdefault('select_dict', {}) - related_key_select_expression_list = new_kw.pop( - 'related_key_select_expression_list', []) - for related_key_select in related_key_select_expression_list: - select_dict[related_key_select] = '%s_%s' % (table, - related_key_select) - - # Column values - column_value_dict = new_kw.pop('column_value_dict', {}) - for key, value in column_value_dict.iteritems(): - new_kw['%s.%s' % (table, key)] = value - # Related keys - # First, the passthrough (acts as default values) - for key, value in new_kw.pop('related_key_dict_passthrough', {})\ - .iteritems(): - new_kw[key] = value - # Second, calculated values - for key, value in new_kw.pop('related_key_dict', {}).iteritems(): - new_kw['%s_%s' % (table, key)] = value - # Simulation states matched with input and output omission - def getSimulationQuery(simulation_dict, omit_dict): - simulation_query = self._buildSimulationStateQuery( - simulation_state_dict=simulation_dict, - table=table, - ) - query_list = [ - self._getIncreaseQuery(table, column, value) - for key, column, value in ( - ('input', 'quantity', False), - ('output', 'quantity', True), - ('asset_increase', 'total_price', False), - ('asset_decrease', 'total_price', True), - ) - if omit_dict.get(key) - ] - if query_list: - if simulation_query is not None: - query_list.append(simulation_query) - return ComplexQuery( - query_list, - logical_operator='AND', - ) - return simulation_query - simulation_query = getSimulationQuery( - new_kw.pop('simulation_dict', {}), - new_kw.pop('omit_dict', {}), - ) - reserved_kw = new_kw.pop('reserved_kw', None) - if reserved_kw is not None: - reserved_query = getSimulationQuery( - reserved_kw.pop('simulation_dict', {}), - reserved_kw.pop('omit_dict', {}), - ) - if simulation_query is None: - simulation_query = reserved_query - elif reserved_query is not None: - simulation_query = ComplexQuery( - simulation_query, - reserved_query, - logical_operator='OR', - ) - if simulation_query is not None: - new_kw['query'] = simulation_query - - # Sort on - if 'sort_on' in new_kw: - table_column_list = ctool.getSQLCatalog().getTableColumnList(table) - sort_on = new_kw['sort_on'] - new_sort_on = [] - for column_id, sort_direction in sort_on: - if column_id in table_column_list: - column_id = '%s.%s' % (table, column_id) - new_sort_on.append((column_id, sort_direction)) - new_kw['sort_on'] = tuple(new_sort_on) - - # Remove some internal parameters that does not have any meaning for - # catalog - new_kw.pop('ignore_group_by', None) - - catalog_sql_kw = ctool.buildSQLQuery(**new_kw) - from_table_dict = dict(sql_kw.pop('from_table_list', [])) - for alias, table in catalog_sql_kw.pop('from_table_list', None) or []: - assert from_table_dict.get(alias) in (None, table), ( - alias, - table, - from_table_dict[alias], - ) - from_table_dict[alias] = table - sql_kw.update(catalog_sql_kw) - sql_kw['from_table_list'] = from_table_dict.items() - return sql_kw - - def _generateKeywordDict(self, - # dates - from_date=None, to_date=None, at_date=None, - omit_mirror_date=1, - # instances - resource=None, node=None, payment=None, - section=None, mirror_section=None, item=None, - function=None, project=None, funding=None, payment_request=None, - transformed_resource=None, ledger=None, - # used for tracking - input=0, output=0, - # categories - resource_category=None, node_category=None, payment_category=None, - section_category=None, mirror_section_category=None, - function_category=None, project_category=None, funding_category=None, - ledger_category=None, payment_request_category=None, - # categories with strict membership - resource_category_strict_membership=None, - node_category_strict_membership=None, - payment_category_strict_membership=None, - section_category_strict_membership=None, - mirror_section_category_strict_membership=None, - function_category_strict_membership=None, - project_category_strict_membership=None, - funding_category_strict_membership=None, - ledger_category_strict_membership=None, - payment_request_category_strict_membership=None, - # simulation_state - strict_simulation_state=0, - simulation_state=None, transit_simulation_state = None, omit_transit=0, - input_simulation_state=None, output_simulation_state=None, - reserved_kw=None, - # variations - variation_text=None, sub_variation_text=None, - variation_category=None, - transformed_variation_text=None, - # uids - resource_uid=None, node_uid=None, section_uid=None, payment_uid=None, - mirror_node_uid=None, mirror_section_uid=None, function_uid=None, - project_uid=None, funding_uid=None, ledger_uid=None, - payment_request_uid=None, - # omit input and output - omit_input=0, - omit_output=0, - omit_asset_increase=0, - omit_asset_decrease=0, - # group by - group_by_node=0, - group_by_node_category=0, - group_by_node_category_strict_membership=0, - group_by_mirror_node=0, - group_by_mirror_node_category=0, - group_by_mirror_node_category_strict_membership=0, - group_by_section=0, - group_by_section_category=0, - group_by_section_category_strict_membership=0, - group_by_mirror_section=0, - group_by_mirror_section_category=0, - group_by_mirror_section_category_strict_membership=0, - group_by_payment=0, - group_by_payment_category=0, - group_by_payment_category_strict_membership=0, - group_by_sub_variation=0, - group_by_variation=0, - group_by_movement=0, - group_by_resource=0, - group_by_project=0, - group_by_project_category=0, - group_by_project_category_strict_membership=0, - group_by_funding=0, - group_by_funding_category=0, - group_by_funding_category_strict_membership=0, - group_by_ledger=0, - group_by_ledger_category=0, - group_by_ledger_category_strict_membership=0, - group_by_payment_request=0, - group_by_payment_request_category=0, - group_by_payment_request_category_strict_membership=0, - group_by_function=0, - group_by_function_category=0, - group_by_function_category_strict_membership=0, - group_by_date=0, - # sort_on - sort_on=None, - group_by=None, - # selection - selection_domain=None, - selection_report=None, - # keywords for related keys - **kw): - """ - Generates keywords and calls buildSQLQuery - - - omit_mirror_date: normally, date's parameters are only based on date - column. If 0, it also used the mirror_date column. - """ - new_kw = {} - sql_kw = { - 'from_table_list': [], - # Set of catalog aliases that must be joined in the ZSQLMethod ('foo' - # meaning something along the lines of 'foo.uid = stock.foo_uid') - 'selection_domain_catalog_alias_set': [], - # input and output are used by getTrackingList - 'input': input, - 'output': output, - # BBB - 'selection_domain': None, - 'selection_report': None, - } - - if selection_domain is None: - sql_kw['selection_domain_from_expression'] = None - sql_kw['selection_domain_where_expression'] = None - else: - # Pre-render selection_domain, as it is easier done here than in DTML. - if isinstance(selection_domain, dict): - selection_domain_dict = selection_domain - else: - selection_domain_dict = selection_domain.asDomainDict() - if 'ledger' in selection_domain_dict: - # XXX: what if both 'node' and 'ledger' are present ? - # Finer configuration may be needed here. - query_table_alias = 'ledger' - else: - query_table_alias = 'node' - selection_domain_sql_dict = self.getPortalObject().portal_catalog.buildSQLQuery( - selection_domain=selection_domain, - query_table_alias=query_table_alias, - ) - sql_kw['selection_domain_from_expression'] = selection_domain_sql_dict['from_expression'] - sql_kw['from_table_list'].extend(selection_domain_sql_dict['from_table_list']) - sql_kw['selection_domain_where_expression'] = selection_domain_sql_dict['where_expression'] - sql_kw['selection_domain_catalog_alias_set'].append(query_table_alias) - if selection_report is not None: - new_kw['selection_report'] = selection_report - - # Add sort_on parameter if defined - if sort_on is not None: - new_kw['sort_on'] = sort_on - - class DictMixIn(dict): - def set(dictionary, key, value): - result = bool(value) - if result: - dictionary[key] = value - return result - - def setUIDList(dictionary, key, value, as_text=0): - uid_list = self._generatePropertyUidList(value, as_text=as_text) - return dictionary.set(key, uid_list) - - column_value_dict = DictMixIn() - - if omit_mirror_date: - date_dict = {} - if from_date : - date_dict.setdefault('query', []).append(from_date) - date_dict['range'] = 'min' - if to_date : - date_dict.setdefault('query', []).append(to_date) - date_dict['range'] = 'minmax' - elif at_date : - date_dict.setdefault('query', []).append(at_date) - date_dict['range'] = 'minngt' - elif to_date : - date_dict.setdefault('query', []).append(to_date) - date_dict['range'] = 'max' - elif at_date : - date_dict.setdefault('query', []).append(at_date) - date_dict['range'] = 'ngt' - if date_dict: - column_value_dict['date'] = date_dict - else: - column_value_dict['date'] = {'query': [to_date], 'range': 'ngt'} - column_value_dict['mirror_date'] = {'query': [from_date], 'range': 'nlt'} - - column_value_dict.set('resource_uid', resource_uid) - column_value_dict.set('payment_uid', payment_uid) - column_value_dict.set('project_uid', project_uid) - column_value_dict.set('funding_uid', funding_uid) - column_value_dict.set('ledger_uid', ledger_uid) - column_value_dict.set('payment_request_uid', payment_request_uid) - column_value_dict.set('function_uid', function_uid) - column_value_dict.set('section_uid', section_uid) - column_value_dict.set('node_uid', node_uid) - column_value_dict.set('mirror_node_uid', mirror_node_uid) - column_value_dict.set('mirror_section_uid', mirror_section_uid) - column_value_dict.setUIDList('resource_uid', resource) - column_value_dict.setUIDList('aggregate_uid', item) - column_value_dict.setUIDList('node_uid', node) - column_value_dict.setUIDList('payment_uid', payment) - column_value_dict.setUIDList('project_uid', project) - column_value_dict.setUIDList('funding_uid', funding) - column_value_dict.setUIDList('ledger_uid', ledger) - column_value_dict.setUIDList('payment_request_uid', payment_request) - column_value_dict.setUIDList('function_uid', function) - - sql_kw['transformed_uid'] = self._generatePropertyUidList(transformed_resource) - - column_value_dict.setUIDList('section_uid', section) - column_value_dict.setUIDList('mirror_section_uid', mirror_section) - - # Handle variation_category as variation_text - if variation_category: - if variation_text: - raise ValueError( - "Passing both variation_category and variation_text is not supported") - warn("variation_category is deprecated, please use variation_text instead", - DeprecationWarning) - if isinstance(variation_category, basestring): - variation_category = (variation_category,) - # variation text is a \n separated list of variation categories, but without - # trailing nor leading \n - variation_text = [ - "{}\n%".format(x) for x in variation_category] + [ - "%\n{}\n%".format(x) for x in variation_category] + [ - "%\n{}".format(x) for x in variation_category] + [ - "{}".format(x) for x in variation_category] - - column_value_dict.setUIDList('variation_text', variation_text, - as_text=1) - column_value_dict.setUIDList('sub_variation_text', sub_variation_text, - as_text=1) - new_kw['column_value_dict'] = column_value_dict.copy() - - related_key_dict = DictMixIn() - # category membership - related_key_dict.setUIDList('resource_category_uid', resource_category) - related_key_dict.setUIDList('node_category_uid', node_category) - related_key_dict.setUIDList('project_category_uid', project_category) - related_key_dict.setUIDList('funding_category_uid', funding_category) - related_key_dict.setUIDList('ledger_category_uid', ledger_category) - related_key_dict.setUIDList('payment_request_category_uid', payment_request_category) - related_key_dict.setUIDList('function_category_uid', function_category) - related_key_dict.setUIDList('payment_category_uid', payment_category) - related_key_dict.setUIDList('section_category_uid', section_category) - related_key_dict.setUIDList('mirror_section_category_uid', - mirror_section_category) - # category strict membership - related_key_dict.setUIDList('resource_category_strict_membership_uid', - resource_category_strict_membership) - related_key_dict.setUIDList('node_category_strict_membership_uid', - node_category_strict_membership) - related_key_dict.setUIDList('project_category_strict_membership_uid', - project_category_strict_membership) - related_key_dict.setUIDList('funding_category_strict_membership_uid', - funding_category_strict_membership) - related_key_dict.setUIDList('ledger_category_strict_membership_uid', - ledger_category_strict_membership) - related_key_dict.setUIDList('payment_request_category_strict_membership_uid', - payment_request_category_strict_membership) - related_key_dict.setUIDList('function_category_strict_membership_uid', - function_category_strict_membership) - related_key_dict.setUIDList('payment_category_strict_membership_uid', - payment_category_strict_membership) - related_key_dict.setUIDList('section_category_strict_membership_uid', - section_category_strict_membership) - related_key_dict.setUIDList( - 'mirror_section_category_strict_membership_uid', - mirror_section_category_strict_membership) - - new_kw['related_key_dict'] = related_key_dict.copy() - new_kw['related_key_dict_passthrough'] = kw - # Check we do not get a known group_by - related_group_by = [] - if group_by: - if isinstance(group_by, basestring): - group_by = (group_by,) - for value in group_by: - if value == "node_uid": - group_by_node = 1 - elif value == 'mirror_node_uid': - group_by_mirror_node = 1 - elif value == 'section_uid': - group_by_section = 1 - elif value == 'mirror_section_uid': - group_by_mirror_section = 1 - elif value == 'payment_uid': - group_by_payment = 1 - elif value == 'sub_variation_text': - group_by_sub_variation = 1 - elif value == 'variation_text': - group_by_variation = 1 - elif value == 'uid': - group_by_movement = 1 - elif value == 'resource_uid': - group_by_resource = 1 - elif value == 'project_uid': - group_by_project = 1 - elif value == 'funding_uid': - group_by_funding = 1 - elif value == 'ledger_uid': - group_by_ledger = 1 - elif value == 'payment_request_uid': - group_by_payment_request = 1 - elif value == "function_uid": - group_by_function = 1 - elif value == 'date': - group_by_date = 1 - else: - related_group_by.append(value) - if related_group_by: - new_kw['related_key_dict_passthrough']['group_by_list'] = related_group_by - - new_kw['simulation_dict'] = self._getSimulationStateDict( - simulation_state=simulation_state, - omit_transit=omit_transit, - input_simulation_state=input_simulation_state, - output_simulation_state=output_simulation_state, - transit_simulation_state=transit_simulation_state, - strict_simulation_state=strict_simulation_state, - ) - new_kw['omit_dict'] = { - 'input': omit_input, - 'output': omit_output, - 'asset_increase': omit_asset_increase, - 'asset_decrease': omit_asset_decrease, - } - if reserved_kw is not None: - if not isinstance(reserved_kw, dict): - # Not a dict when taken from URL, so, cast is needed - # to make pop method available - reserved_kw = dict(reserved_kw) - new_kw['reserved_kw'] = { - 'omit_dict': { - 'input': reserved_kw.pop('omit_input', False), - 'output': reserved_kw.pop('omit_output', False), - }, - 'simulation_dict': self._getSimulationStateDict(**reserved_kw), - } - - # build the group by expression - # if we group by a criterion, we also add this criterion to the select - # expression, unless it is already selected in Resource_zGetInventoryList - # the caller can also pass select_dict or select_list. select_expression, - # which is deprecated in ZSQLCatalog is not supported here. - select_dict = kw.get('select_dict', {}) - select_dict.update(dict.fromkeys(list(kw.pop('select_list', [])) + related_group_by)) - new_kw['select_dict'] = select_dict - related_key_select_expression_list = [] - - column_group_by_expression_list = [] - related_key_group_by_expression_list = [] - if group_by_node: - column_group_by_expression_list.append('node_uid') - if group_by_mirror_node: - column_group_by_expression_list.append('mirror_node_uid') - if group_by_section: - column_group_by_expression_list.append('section_uid') - if group_by_mirror_section: - column_group_by_expression_list.append('mirror_section_uid') - if group_by_payment: - column_group_by_expression_list.append('payment_uid') - if group_by_sub_variation: - column_group_by_expression_list.append('sub_variation_text') - if group_by_variation: - column_group_by_expression_list.append('variation_text') - if group_by_movement: - column_group_by_expression_list.append('uid') - if group_by_resource: - column_group_by_expression_list.append('resource_uid') - if group_by_project: - column_group_by_expression_list.append('project_uid') - if group_by_funding: - column_group_by_expression_list.append('funding_uid') - if group_by_ledger: - column_group_by_expression_list.append('ledger_uid') - if group_by_payment_request: - column_group_by_expression_list.append('payment_request_uid') - if group_by_function: - column_group_by_expression_list.append('function_uid') - if group_by_date: - column_group_by_expression_list.append('date') - - if column_group_by_expression_list: - new_kw['column_group_by'] = column_group_by_expression_list - - if group_by_section_category: - related_key_group_by_expression_list.append('section_category_uid') - related_key_select_expression_list.append('section_category_uid') - if group_by_section_category_strict_membership: - related_key_group_by_expression_list.append( - 'section_category_strict_membership_uid') - related_key_select_expression_list.append( - 'section_category_strict_membership_uid') - if group_by_mirror_section_category: - related_key_group_by_expression_list.append('mirror_section_category_uid') - related_key_select_expression_list.append('mirror_section_category_uid') - if group_by_mirror_section_category_strict_membership: - related_key_group_by_expression_list.append( - 'mirror_section_category_strict_membership_uid') - related_key_select_expression_list.append( - 'mirror_section_category_strict_membership_uid') - if group_by_node_category: - related_key_group_by_expression_list.append('node_category_uid') - related_key_select_expression_list.append('node_category_uid') - if group_by_node_category_strict_membership: - related_key_group_by_expression_list.append( - 'node_category_strict_membership_uid') - related_key_select_expression_list.append( - 'node_category_strict_membership_uid') - if group_by_mirror_node_category: - related_key_group_by_expression_list.append('mirror_node_category_uid') - if group_by_mirror_node_category_strict_membership: - related_key_group_by_expression_list.append( - 'mirror_node_category_strict_membership_uid') - related_key_select_expression_list.append( - 'mirror_node_category_strict_membership_uid') - if group_by_payment_category: - related_key_group_by_expression_list.append('payment_category_uid') - related_key_select_expression_list.append('payment_category_uid') - if group_by_payment_category_strict_membership: - related_key_group_by_expression_list.append( - 'payment_category_strict_membership_uid') - related_key_select_expression_list.append( - 'payment_category_strict_membership_uid') - if group_by_function_category: - related_key_group_by_expression_list.append('function_category_uid') - related_key_select_expression_list.append('function_category_uid') - if group_by_function_category_strict_membership: - related_key_group_by_expression_list.append( - 'function_category_strict_membership_uid') - related_key_select_expression_list.append( - 'function_category_strict_membership_uid') - if group_by_project_category: - related_key_group_by_expression_list.append('project_category_uid') - related_key_select_expression_list.append('project_category_uid') - if group_by_project_category_strict_membership: - related_key_group_by_expression_list.append( - 'project_category_strict_membership_uid') - related_key_select_expression_list.append( - 'project_category_strict_membership_uid') - if group_by_funding_category: - related_key_group_by_expression_list.append('funding_category_uid') - related_key_select_expression_list.append('funding_category_uid') - if group_by_funding_category_strict_membership: - related_key_group_by_expression_list.append( - 'funding_category_strict_membership_uid') - related_key_select_expression_list.append( - 'funding_category_strict_membership_uid') - if group_by_ledger_category: - related_key_group_by_expression_list.append('ledger_category_uid') - related_key_select_expression_list.append('ledger_category_uid') - if group_by_ledger_category_strict_membership: - related_key_group_by_expression_list.append( - 'ledger_category_strict_membership_uid') - related_key_select_expression_list.append( - 'ledger_category_strict_membership_uid') - if group_by_payment_category: - related_key_group_by_expression_list.append('payment_request_category_uid') - related_key_select_expression_list.append('payment_request_category_uid') - if group_by_payment_request_category_strict_membership: - related_key_group_by_expression_list.append( - 'payment_request_category_strict_membership_uid') - related_key_select_expression_list.append( - 'payment_request_category_strict_membership_uid') - - if related_key_group_by_expression_list: - new_kw['related_key_group_by'] = related_key_group_by_expression_list - if related_key_select_expression_list: - new_kw['related_key_select_expression_list'] =\ - related_key_select_expression_list - - return sql_kw, new_kw - - ####################################################### - # Inventory management - security.declareProtected(Permissions.AccessContentsInformation, - 'getInventory') - def getInventory(self, src__=0, simulation_period='', **kw): - """ - Returns an inventory of a single or multiple resources on a single or - multiple nodes as a single float value - - from_date (>=) - only take rows which date is >= from_date - - to_date (<) - only take rows which date is < to_date - - at_date (<=) - only take rows which date is <= at_date - - resource (only in generic API in simulation) - - node - only take rows in stock table which node_uid is - equivalent to node - - payment - only take rows in stock table which payment_uid is - equivalent to payment - - section - only take rows in stock table which section_uid is - equivalent to section - - mirror_section - only take rows in stock table which mirror_section_uid is - mirror_section - - resource_category - only take rows in stock table which - resource_uid is member of resource_category - - node_category - only take rows in stock table which node_uid is - member of section_category - - payment_category - only take rows in stock table which payment_uid - is in section_category - - section_category - only take rows in stock table which section_uid is - member of section_category - - mirror_section_category - only take rows in stock table which - mirror_section_uid is member of - mirror_section_category - - node_filter - only take rows in stock table which node_uid - matches node_filter - - payment_filter - only take rows in stock table which payment_uid - matches payment_filter - - section_filter - only take rows in stock table which section_uid - matches section_filter - - mirror_section_filter - only take rows in stock table which - mirror_section_uid matches mirror_section_filter - - variation_text - only take rows in stock table with specified - variation_text. - XXX this way of implementing variation selection is far - from perfect - - sub_variation_text - only take rows in stock table with specified - variation_text - - variation_category - variation or list of possible variations (it is not - a cross-search ; SQL query uses OR). - Deprecated, use variation_text. - - simulation_state - only take rows with specified simulation_state - - transit_simulation_state - specifies which states are transit states - - omit_transit - do not evaluate transit_simulation_state - - input_simulation_state - only take rows with specified simulation_state - and quantity > 0 - - output_simulation_state - only take rows with specified simulation_state - and quantity < 0 - - ignore_variation - do not take into account variation in inventory - calculation (useless on getInventory, but useful on - getInventoryList) - - standardise - provide a standard quantity rather than an SKU (XXX - not implemented yet) - - omit_simulation - doesn't take into account simulation movements - - only_accountable - Only take into account accountable movements. By - default, only movements for which isAccountable() is - true will be taken into account. - - omit_input - doesn't take into account movement with quantity > 0 - - omit_output - doesn't take into account movement with quantity < 0 - - omit_asset_increase - doesn't take into account movement with asset_price > 0 - - omit_asset_decrease - doesn't take into account movement with asset_price < 0 - - selection_domain, selection_report - see ListBox - - group_by_variation - (useless on getInventory, but useful on - getInventoryList) - - group_by_node - (useless on getInventory, but useful on - getInventoryList) - - group_by_mirror_node - (useless on getInventory, but useful on - getInventoryList) - - group_by_sub_variation - (useless on getInventory, but useful on - getInventoryList) - - group_by_movement - (useless on getInventory, but useful on - getInventoryList) - - precision - the precision used to round quantities and prices. - - metric_type - convert the results to a specific metric_type - - quantity_unit - display results using this specific quantity unit - - transformed_resource - one, or several resources. list resources that can - be produced using those resources as input in a - transformation. - relative_resource_url for each returned line will - point to the transformed resource, while the stock - will be the stock of the produced resource, - expressed in number of transformed resources. - transformed_variation_text - to be used with transformed_resource, to - to refine the transformation selection only - to those using variated resources as input. - - **kw - if we want extended selection with more keywords (but - bad performance) check what we can do with - buildSQLQuery - - NOTE: we may want to define a parameter so that we can select the kind of - inventory statistics we want to display (ex. sum, average, cost, etc.) - """ - # JPS: this is a hint for implementation of xxx_filter arguments - # node_uid_count = portal_catalog.countResults(**node_filter) - # if node_uid_count not too big: - # node_uid_list = cache(portal_catalog(**node_filter)) - # pass this list to ZSQL method - # else: - # build a table in MySQL - # and join that table with the stock table - method = getattr(self,'get%sInventoryList' % simulation_period) - kw['ignore_group_by'] = 1 - result = method(inventory_list=0, src__=src__, **kw) - if src__: - return result - - total_result = 0.0 - if len(result) > 0: - if len(result) != 1: - raise ValueError, 'Sorry we must have only one' - result = result[0] - - if hasattr(result, "converted_quantity"): - total_result = result.converted_quantity - else: - inventory = result.total_quantity - if inventory is not None: - total_result = inventory - - return total_result - - security.declareProtected(Permissions.AccessContentsInformation, - 'getCurrentInventory') - def getCurrentInventory(self, **kw): - """ - Returns current inventory - """ - return self.getInventory(simulation_period='Current', **kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getAvailableInventory') - def getAvailableInventory(self, **kw): - """ - Returns available inventory - (current inventory - reserved_inventory) - """ - return self.getInventory(simulation_period='Available', **kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getFutureInventory') - def getFutureInventory(self, **kw): - """ - Returns future inventory - """ - return self.getInventory(simulation_period='Future', **kw) - - def _getDefaultGroupByParameters(self, ignore_group_by=0, - group_by_node=0, group_by_mirror_node=0, - group_by_section=0, group_by_mirror_section=0, - group_by_payment=0, group_by_project=0, group_by_funding=0, - group_by_ledger=0, group_by_function=0, - group_by_variation=0, group_by_sub_variation=0, - group_by_movement=0, group_by_date=0, - group_by_section_category=0, - group_by_section_category_strict_membership=0, - group_by_resource=None, - group_by=None, - **ignored): - """ - Set defaults group_by parameters - - If ignore_group_by is true, this function returns an empty dict. - - If any group-by is provided, automatically group by resource aswell - unless group_by_resource is explicitely set to false. - If no group by is provided, use the default group by: movement, node and - resource. - """ - new_group_by_dict = {} - if not ignore_group_by and group_by is None: - if group_by_node or group_by_mirror_node or group_by_section or \ - group_by_project or group_by_funding or group_by_ledger or \ - group_by_function or group_by_mirror_section or group_by_payment or \ - group_by_sub_variation or group_by_variation or \ - group_by_movement or group_by_date or group_by_section_category or\ - group_by_section_category_strict_membership: - if group_by_resource is None: - group_by_resource = 1 - new_group_by_dict['group_by_resource'] = group_by_resource - elif group_by_resource is None: - new_group_by_dict['group_by_movement'] = 1 - new_group_by_dict['group_by_node'] = 1 - new_group_by_dict['group_by_resource'] = 1 - return new_group_by_dict - - security.declareProtected(Permissions.AccessContentsInformation, - 'getInventoryList') - def getInventoryList(self, src__=0, optimisation__=True, - ignore_variation=0, standardise=0, - omit_simulation=0, - only_accountable=True, - default_stock_table='stock', - statistic=0, inventory_list=1, - precision=None, connection_id=None, - **kw): - """ - Returns a list of inventories for a single or multiple - resources on a single or multiple nodes, grouped by resource, - node, section, etc. Every line defines an inventory value for - a given group of resource, node, section. - NOTE: we may want to define a parameter so that we can select - the kind of inventory statistics we want to display (ex. sum, - average, cost, etc.) - - Optimisation queries. - Optimisation of a stock lookup is done to avoid a table scan - of all lines corresponding to a given node, section or payment, - because they grow with time and query time should not. - First query: Fetch fitting full inventory dates. - For each node, section or payment, find the first anterior full - inventory. - Second query: Fetch full inventory amounts. - Fetch values of inventory identified in the first query. - Third query: Classic stock table read. - Fetch all rows in stock table which are posterior to the inventory. - Final result - Add results of the second and third queries, and return it. - - Missing optimisations: - - In a getInventory case where everything is specified for the - resource, it's not required for the inventory to be full, it - just need to be done for the right resource. - If the resource isn't completely defined, we require inventory - to be full, which is implemented. - - Querying multiple nodes/categories/payments in one call prevents - from using optimisation, it should be equivalent to multiple calls - on individual nodes/categories/payments. - - - """ - getCategory = self.getPortalObject().portal_categories.getCategoryUid - - result_column_id_dict = {} - - metric_type = kw.pop('metric_type', None) - quantity_unit = kw.pop('quantity_unit', None) - quantity_unit_uid = None - - if quantity_unit is not None: - - if isinstance(quantity_unit, str): - quantity_unit_uid = getCategory(quantity_unit, 'quantity_unit') - if quantity_unit_uid is not None: - result_column_id_dict['converted_quantity'] = None - if metric_type is None: - # use the default metric type - metric_type = quantity_unit.split("/", 1)[0] - elif isinstance(quantity_unit, (int, float)): - # quantity_unit used to be a numerical parameter.. - raise ValueError('Numeric values for quantity_unit are not supported') - - - convert_quantity_result = False - if metric_type is not None: - metric_type_uid = getCategory(metric_type, 'metric_type') - - if metric_type_uid is not None: - convert_quantity_result = True - kw['metric_type_uid'] = Query( - metric_type_uid=metric_type_uid, - table_alias_list=(("measure", "measure"),)) - - if src__: - sql_source_list = [] - # If no group at all, give a default sort group by - kw.update(self._getDefaultGroupByParameters(**kw)) - base_inventory_kw = { - 'stock_table_id': default_stock_table, - 'src__': src__, - 'ignore_variation': ignore_variation, - 'standardise': standardise, - 'omit_simulation': omit_simulation, - 'only_accountable': only_accountable, - 'precision': precision, - 'inventory_list': inventory_list, - 'connection_id': connection_id, - 'statistic': statistic, - 'convert_quantity_result': convert_quantity_result, - 'quantity_unit_uid': quantity_unit_uid, - } - # Get cached data - if getattr(self, "Resource_zGetInventoryCacheResult", None) is not None and \ - optimisation__ and (not kw.get('from_date')) and \ - 'transformed_resource' not in kw: - # Here is the different kind of date - # from_date : >= - # to_date : < - # at_date : <= - # As we just have from_date, it means that we must use - # the to_date for the cache in order to avoid double computation - # of the same line - at_date = kw.pop("at_date", None) - if at_date is None: - to_date = kw.pop("to_date", None) - else: - # add one second so that we can use to_date - to_date = at_date + MYSQL_MIN_DATETIME_RESOLUTION - try: - cached_result, cached_date = self._getCachedInventoryList( - to_date=to_date, - sql_kw=kw, - **base_inventory_kw) - except StockOptimisationError: - cached_result = [] - kw['to_date'] = to_date - else: - if src__: - sql_source_list.extend(cached_result) - # Now must generate query for date diff - kw['to_date'] = to_date - kw['from_date'] = cached_date - else: - cached_result = [] - sql_kw, new_kw = self._generateKeywordDict(**kw) - # Copy kw content as _generateSQLKeywordDictFromKeywordDict - # remove some values from it - try: - new_kw_copy = deepcopy(new_kw) - except TypeError: - # new_kw contains wrong parameters - # as optimisation has already been disable we - # do not care about the deepcopy - new_kw_copy = new_kw - stock_sql_kw = self._generateSQLKeywordDictFromKeywordDict( - table=default_stock_table, sql_kw=sql_kw, new_kw=new_kw_copy) - stock_sql_kw.update(base_inventory_kw) - delta_result = self.Resource_zGetInventoryList( - **stock_sql_kw) - if src__: - sql_source_list.append(delta_result) - result = ';\n-- NEXT QUERY\n'.join(sql_source_list) - else: - if cached_result: - result = self._addBrainResults(delta_result, cached_result, new_kw) - else: - result = delta_result - return result - - security.declareProtected(Permissions.AccessContentsInformation, - 'getInventoryCacheLag') - def getInventoryCacheLag(self): - """ - Returns a duration, in days, for stock cache management. - If data in stock cache is older than lag compared to query's date - (at_date or to_date), then it becomes a "soft miss": use found value, - but add a new entry to cache at query's date minus half the lag. - So this value should be: - - Small enough that few enough rows need to be table-scanned for - average queries (probably queries against current date). - - Large enough that few enough documents get modified past that date, - otherwise cache entries would be removed from cache all the time. - """ - return self.SimulationTool_getInventoryCacheLag() - - def _getCachedInventoryList(self, to_date, sql_kw, stock_table_id, src__=False, **kw): - """ - Try to get a cached inventory list result - If not existing, fill the cache - """ - Resource_zGetInventoryList = self.Resource_zGetInventoryList - # Generate the SQL source without date parameter - # This will be the cache key - try: - no_date_kw = deepcopy(sql_kw) - except TypeError: - LOG("SimulationTool._getCachedInventoryList", WARNING, - "Failed copying sql_kw, disabling stock cache", - error=True) - raise StockOptimisationError - no_date_sql_kw, no_date_new_kw = self._generateKeywordDict(**no_date_kw) - no_date_stock_sql_kw = self._generateSQLKeywordDictFromKeywordDict( - table=stock_table_id, sql_kw=no_date_sql_kw, - new_kw=no_date_new_kw) - kw.update(no_date_stock_sql_kw) - if src__: - sql_source_list = [] - # Generate the cache key (md5 of query source) - sql_text_hash = md5(Resource_zGetInventoryList( - stock_table_id=stock_table_id, - src__=1, - **kw)).digest() - # Try to get result from cache - Resource_zGetInventoryCacheResult = self.Resource_zGetInventoryCacheResult - inventory_cache_kw = {'query': sql_text_hash} - if to_date is not None: - inventory_cache_kw['date'] = to_date - try: - cached_sql_result = Resource_zGetInventoryCacheResult(**inventory_cache_kw) - except ProgrammingError, (code, _): - if code != NO_SUCH_TABLE: - raise - # First use of the optimisation, we need to create the table - LOG("SimulationTool._getCachedInventoryList", INFO, - "Creating inventory cache stock") - if src__: - sql_source_list.append(self.SimulationTool_zCreateInventoryCache(src__=1)) - else: - self.SimulationTool_zCreateInventoryCache() - cached_sql_result = None - - if src__: - sql_source_list.append(Resource_zGetInventoryCacheResult(src__=1, **inventory_cache_kw)) - if cached_sql_result: - brain_result = loads(cached_sql_result[0].result) - # Rebuild the brains - cached_result = Results( - (brain_result['items'], brain_result['data']), - brains=getBrain( - Resource_zGetInventoryList.class_file_, - Resource_zGetInventoryList.class_name_, - ), - parent=self, - ) - else: - cached_result = [] - cache_lag = self.getInventoryCacheLag() - if cached_sql_result and (to_date is None or (to_date - DateTime(cached_sql_result[0].date) < cache_lag)): - cached_date = DateTime(cached_sql_result[0].date) - result = cached_result - elif to_date is not None: - # Cache miss, or hit with old data: store a new entry in cache. - # Don't store it at to_date, as it risks being flushed soon (ie, when - # any document older than to_date gets reindexed in stock table). - # Don't store it at to_date - cache_lag, as it would risk expiring - # soon as we store it (except if to_date is fixed for many queries, - # which we cannot tell here). - # So store it at half the cache_lag before to_date. - cached_date = to_date - cache_lag / 2 - new_cache_kw = deepcopy(sql_kw) - if cached_result: - # We can use cached result to generate new cache result - new_cache_kw['from_date'] = DateTime(cached_sql_result[0].date) - sql_kw, new_kw = self._generateKeywordDict( - to_date=cached_date, - **new_cache_kw) - kw.update(self._generateSQLKeywordDictFromKeywordDict( - table=stock_table_id, - sql_kw=sql_kw, - new_kw=new_kw, - ) - ) - new_result = Resource_zGetInventoryList( - stock_table_id=stock_table_id, - src__=src__, - **kw) - if src__: - sql_source_list.append(new_result) - else: - result = self._addBrainResults(new_result, cached_result, new_kw) - self.Resource_zInsertInventoryCacheResult( - query=sql_text_hash, - date=cached_date, - result=dumps({ - 'items': result.__items__, - 'data': result._data, - }), - ) - else: - # Cache miss and this getInventory() not specifying to_date, - # and other getInventory() have not created usable caches. - # In such case, do not create cache, do not use cache. - result = [] - cached_date = None - if src__: - result = sql_source_list - return result, cached_date - - def _addBrainResults(self, first_result, second_result, new_kw): - """ - Build a Results which is the addition of two other result - """ - # This part defined key to group lines from different Results - group_by_id_list = [] - group_by_id_list_append = group_by_id_list.append - - for group_by_id in new_kw.get('column_group_by', []): - if group_by_id == 'uid': - group_by_id_list_append('stock_uid') - else: - group_by_id_list_append(group_by_id) - # Add related key group by - related_key_dict_passthrough = new_kw.get("related_key_dict_passthrough", {}) - group_by_list = related_key_dict_passthrough.get('group_by_list', []) - cannot_group_by = set(group_by_list).difference( - related_key_dict_passthrough.get('select_list', []), - ) - if cannot_group_by: - # XXX-Aurel : to review & change, must prevent coming here before - raise ValueError("Impossible to group by %s" % (cannot_group_by, )) - group_by_id_list += group_by_list - - if len(group_by_id_list): - def getInventoryListKey(line): - """ - Generate a key based on values used in SQL group_by - """ - return tuple([line[x] for x in group_by_id_list]) - - else: - def getInventoryListKey(line): - """ - Return a dummy key, all line will be summed - """ - return "dummy" - result_column_id_dict = { - 'inventory': None, - 'total_quantity': None, - 'total_price': None - } - def addLineValues(line_a=None, line_b=None): - """ - Add columns of 2 lines and return a line with same - schema. If one of the parameters is None, returns the - other parameters. - - Arithmetic modifications on additions: - None + x = x - None + None = None - """ - if line_a is None: - return line_b - if line_b is None: - return line_a - # Create a new Shared.DC.ZRDB.Results.Results.__class__ - # instance to add the line values. - # the logic for the 5 lines below is taken from - # Shared.DC.ZRDB.Results.Results.__getitem__ - Result = line_a.__class__ - parent = line_a.aq_parent - result = Result((), parent) - try: - # We must copy the path so that getObject works - setattr(result, 'path', line_a.path) - except ValueError: # XXX: ValueError ? really ? - # getInventory return no object, so no path available - pass - if parent is not None: - result = result.__of__(parent) - for key in line_a.__record_schema__: - value = line_a[key] - if key in result_column_id_dict: - value_b = line_b[key] - if None not in (value, value_b): - result[key] = value + value_b - elif value is not None: - result[key] = value - else: - result[key] = value_b - elif line_a[key] == line_b[key]: - result[key] = line_a[key] - elif key not in ('date', 'stock_uid', 'path'): - # There are 2 possible reasons to end up here: - # - key corresponds to a projected column for which are neither - # known aggregated columns (in which case they should be in - # result_column_id_dict) nor part of grouping columns, and the - # result happens to be unstable. There are cases in ERP5 where - # such result is suposed to be stable, for example - # group_by=('xxx_uid'), selection_list=('xxx_path') because the - # relation is bijective (although the database doesn't know it). - # These should result in stable results (but don't necessarily - # do, ex: xxx_title when object title has been changed between - # cache fill and cache lookup). - # - line_a and line_b are indeed mismatched, and code calling us - # has a bug. - LOG('InventoryTool.getInventoryList.addLineValues', - PROBLEM, - 'mismatch for %s column: %s and %s' % ( - key, line_a[key], line_b[key])) - return result - # Add lines - inventory_list_dict = {} - for line_list in (first_result, second_result): - for line in line_list: - line_key = getInventoryListKey(line) - line_a = inventory_list_dict.get(line_key) - inventory_list_dict[line_key] = addLineValues(line_a, line) - sorted_inventory_list = inventory_list_dict.values() - # Sort results manually when required - sort_on = new_kw.get('sort_on') - if sort_on: - def cmp_inventory_line(line_a, line_b): - """ - Compare 2 inventory lines and sort them according to - sort_on parameter. - """ - result = 0 - for key, sort_direction in sort_on: - try: - result = cmp(line_a[key], line_b[key]) - except KeyError: - raise Exception('Impossible to sort result since columns sort ' - 'happens on are not available in result: %r' % (key, )) - if result: - if not sort_direction.upper().startswith('A'): - # Default sort is ascending, if a sort is given and - # it does not start with an 'A' then reverse sort. - # Tedious syntax checking is MySQL's job, and - # happened when queries were executed. - result *= -1 - break - return result - sorted_inventory_list.sort(cmp_inventory_line) - # Brain is rebuild properly using tuple not r instance - column_list = first_result._searchable_result_columns() - column_name_list = [x['name'] for x in column_list] - # Rebuild a result object based on added results - Resource_zGetInventoryList = self.Resource_zGetInventoryList - return Results( - (column_list, tuple([tuple([getattr(y, x) for x in column_name_list]) \ - for y in sorted_inventory_list])), - parent=self, - brains=getBrain( - Resource_zGetInventoryList.class_file_, - Resource_zGetInventoryList.class_name_, - ), - ) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getConvertedInventoryList') - def getConvertedInventoryList(self, simulation_period='', **kw): - """ - Return list of inventory with a 'converted_quantity' additional column, - which contains the sum of measurements for the specified metric type, - expressed in the 'quantity_unit' unit. - - metric_type - category - quantity_unit - category - """ - - warn('getConvertedInventoryList is Deprecated, use ' \ - 'getInventory instead.', DeprecationWarning) - - method = getattr(self,'get%sInventoryList' % simulation_period) - - return method(**kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getAllInventoryList') - def getAllInventoryList(self, src__=0, **kw): - """ - Returns list of inventory, for all periods. - Performs 1 SQL request for each simulation state, and merge the results. - Rename relevant columns with a '${simulation}_' prefix - (ex: 'total_price' -> 'current_total_price'). - """ - columns = ('total_quantity', 'total_price', 'converted_quantity') - - # Guess the columns to use to identify each row, by looking at the GROUP - # clause. Note that the call to 'mergeZRDBResults' will crash if the GROUP - # clause contains a column not requested in the SELECT clause. - kw.update(self._getDefaultGroupByParameters(**kw), ignore_group_by=1) - group_by_list = self._generateKeywordDict(**kw)[1].get('group_by_list', ()) - - results = [] - edit_result = {} - get_false_value = lambda row, column_name: row.get(column_name) or 0 - - for simulation in 'current', 'available', 'future': - method = getattr(self, 'get%sInventoryList' % simulation.capitalize()) - rename = {'inventory': None} # inventory column is deprecated - for column in columns: - rename[column] = new_name = '%s_%s' % (simulation, column) - edit_result[new_name] = get_false_value - results += (method(src__=src__, **kw), rename), - - if src__: - return ';\n-- NEXT QUERY\n'.join(r[0] for r in results) - return mergeZRDBResults(results, group_by_list, edit_result) - - - security.declareProtected(Permissions.AccessContentsInformation, - 'getCurrentInventoryList') - def getCurrentInventoryList(self, omit_transit=1, - transit_simulation_state=None, **kw): - """ - Returns list of current inventory grouped by section or site - """ - portal = self.getPortalObject() - kw['simulation_state'] = portal.getPortalCurrentInventoryStateList() + \ - portal.getPortalTransitInventoryStateList() - if transit_simulation_state is None: - transit_simulation_state = portal.getPortalTransitInventoryStateList() - - return self.getInventoryList( - omit_transit=omit_transit, - transit_simulation_state=transit_simulation_state, - **kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getAvailableInventoryList') - def getAvailableInventoryList(self, omit_transit=1, transit_simulation_state=None, **kw): - """ - Returns list of current inventory grouped by section or site - """ - portal = self.getPortalObject() - if transit_simulation_state is None: - transit_simulation_state = portal.getPortalTransitInventoryStateList() - kw['simulation_state'] = portal.getPortalCurrentInventoryStateList() + \ - portal.getPortalTransitInventoryStateList() - reserved_kw = {'simulation_state': portal.getPortalReservedInventoryStateList(), - 'transit_simulation_state': transit_simulation_state, - 'omit_input': 1} - return self.getInventoryList(reserved_kw=reserved_kw, omit_transit=omit_transit, - transit_simulation_state=transit_simulation_state, **kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getFutureInventoryList') - def getFutureInventoryList(self, **kw): - """ - Returns list of future inventory grouped by section or site - """ - portal = self.getPortalObject() - kw['simulation_state'] = portal.getPortalFutureInventoryStateList() + \ - portal.getPortalTransitInventoryStateList() + \ - portal.getPortalReservedInventoryStateList() + \ - portal.getPortalCurrentInventoryStateList() - return self.getInventoryList(**kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getInventoryStat') - def getInventoryStat(self, simulation_period='', **kw): - """ - getInventoryStat is the pending to getInventoryList in order to - provide statistics on getInventoryList lines in ListBox such as: - total of inventories, number of variations, number of different - nodes, etc. - """ - kw['group_by_variation'] = 0 - method = getattr(self,'get%sInventoryList' % simulation_period) - return method(statistic=1, inventory_list=0, optimisation__=False, - ignore_group_by=1, **kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getCurrentInventoryStat') - def getCurrentInventoryStat(self, **kw): - """ - Returns statistics of current inventory grouped by section or site - """ - return self.getInventoryStat(simulation_period='Current', **kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getAvailableInventoryStat') - def getAvailableInventoryStat(self, **kw): - """ - Returns statistics of available inventory grouped by section or site - """ - return self.getInventoryStat(simulation_period='Available', **kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getFutureInventoryStat') - def getFutureInventoryStat(self, **kw): - """ - Returns statistics of future inventory grouped by section or site - """ - return self.getInventoryStat(simulation_period='Future', **kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getInventoryChart') - def getInventoryChart(self, src__=0, **kw): - """ - Returns a list of couples derived from getInventoryList in order - to feed a chart renderer. Each couple consist of a label - (node, section, payment, combination of node & section, etc.) - and an inventory value. - - Mostly useful for charts in ERP5 forms. - """ - result = self.getInventoryList(src__=src__, **kw) - if src__ : - return result - - return map(lambda r: (r.node_title, r.total_quantity), result) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getCurrentInventoryChart') - def getCurrentInventoryChart(self, **kw): - """ - Returns list of current inventory grouped by section or site - """ - kw['simulation_state'] = self.getPortalObject()\ - .getPortalCurrentInventoryStateList() - return self.getInventoryChart(**kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getFutureInventoryChart') - def getFutureInventoryChart(self, **kw): - """ - Returns list of future inventory grouped by section or site - """ - portal = self.getPortalObject() - kw['simulation_state'] = portal.getPortalFutureInventoryStateList() + \ - portal.getPortalTransitInventoryStateList() + \ - portal.getPortalReservedInventoryStateList() + \ - portal.getPortalCurrentInventoryStateList() - return self.getInventoryChart(**kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getInventoryAssetPrice') - def getInventoryAssetPrice(self, src__=0, - simulation_period='', - valuation_method=None, - **kw): - """ - Same thing as getInventory but returns an asset - price rather than an inventory. - - If valuation method is None, returns the sum of total prices. - - Else it should be a string, in: - Filo - Fifo - WeightedAverage - MonthlyWeightedAverage - MovingAverage - When using a specific valuation method, a resource_uid is expected - as well as one of (section_uid or node_uid). - """ - if valuation_method is None: - method = getattr(self,'get%sInventoryList' % simulation_period) - kw['ignore_group_by'] = 1 - result = method( src__=src__, inventory_list=0, **kw) - if src__ : - return result - - if len(result) == 0: - return 0.0 - - total_result = 0.0 - for result_line in result: - if result_line.total_price is not None: - total_result += result_line.total_price - - return total_result - - if valuation_method not in ('Fifo', 'Filo', 'WeightedAverage', - 'MonthlyWeightedAverage', 'MovingAverage'): - raise ValueError("Invalid valuation method: %s" % valuation_method) - - assert 'node_uid' in kw or 'section_uid' in kw - sql_kw = self._generateSQLKeywordDict(**kw) - - if 'section_uid' in kw and 'node_uid' not in kw: - # ignore internal movements if ignore node - sql_kw['where_expression'] += ' AND ' \ - 'NOT(stock.section_uid<=>stock.mirror_section_uid)' - - result = self.Resource_zGetAssetPrice( - valuation_method=valuation_method, - src__=src__, - **sql_kw) - - if src__ : - return result - - if len(result) > 0: - return result[-1].total_asset_price - - security.declareProtected(Permissions.AccessContentsInformation, - 'getCurrentInventoryAssetPrice') - def getCurrentInventoryAssetPrice(self, **kw): - """ - Returns list of current inventory grouped by section or site - """ - kw['simulation_state'] = self.getPortalCurrentInventoryStateList() - return self.getInventoryAssetPrice(simulation_period='Current',**kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getAvailableInventoryAssetPrice') - def getAvailableInventoryAssetPrice(self, **kw): - """ - Returns list of available inventory grouped by section or site - (current inventory - deliverable) - """ - portal = self.getPortalObject() - kw['simulation_state'] = portal.getPortalReservedInventoryStateList() + \ - portal.getPortalCurrentInventoryStateList() - return self.getInventoryAssetPrice(**kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getFutureInventoryAssetPrice') - def getFutureInventoryAssetPrice(self, **kw): - """ - Returns list of future inventory grouped by section or site - """ - portal = self.getPortalObject() - kw['simulation_state'] = portal.getPortalFutureInventoryStateList() + \ - portal.getPortalReservedInventoryStateList() + \ - portal.getPortalCurrentInventoryStateList() - return self.getInventoryAssetPrice(**kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getInventoryHistoryList') - def getInventoryHistoryList(self, src__=0, ignore_variation=0, - standardise=0, omit_simulation=0, - only_accountable=True, omit_input=0, - omit_output=0, precision=None, **kw): - """ - Returns a time based serie of inventory values - for a single or a group of resource, node, section, etc. This is useful - to list the evolution with time of inventory values (quantity, asset price). - - TODO: - - make sure getInventoryHistoryList can return - cumulative values calculated by SQL (JPS) - """ - sql_kw = self._generateSQLKeywordDict(**kw) - return self.Resource_getInventoryHistoryList( - src__=src__, ignore_variation=ignore_variation, - standardise=standardise, omit_simulation=omit_simulation, - only_accountable=only_accountable, - omit_input=omit_input, omit_output=omit_output, - precision=precision, - **sql_kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getInventoryHistoryChart') - def getInventoryHistoryChart(self, src__=0, ignore_variation=0, - standardise=0, omit_simulation=0, - only_accountable=True, - omit_input=0, omit_output=0, - precision=None, **kw): - """ - getInventoryHistoryChart is the pensing to getInventoryHistoryList - to ease the rendering of time based graphs which show the evolution - of one ore more inventories. Each item in the serie consists of - time, value and "colour" (multiple graphs can be drawn for example - for each variation of a resource) - """ - sql_kw = self._generateSQLKeywordDict(**kw) - - return self.Resource_getInventoryHistoryChart( - src__=src__, ignore_variation=ignore_variation, - standardise=standardise, omit_simulation=omit_simulation, - only_accountable=only_accountable, - omit_input=omit_input, omit_output=omit_output, - precision=precision, - **sql_kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getMovementHistoryList') - def getMovementHistoryList(self, src__=0, ignore_variation=0, - standardise=0, omit_simulation=0, - omit_input=0, omit_output=0, - only_accountable=True, - omit_asset_increase=0, omit_asset_decrease=0, - initial_running_total_quantity=0, - initial_running_total_price=0, precision=None, - **kw): - """Returns a list of movements which modify the inventory - for a single or a group of resource, node, section, etc. - A running total quantity and a running total price are available on - brains. The initial values can be passed, in case you want to have an - "initial summary line". - """ - # Extend select_dict by order_by_list columns. - catalog = self.getPortalObject().portal_catalog.getSQLCatalog() - kw = catalog.getCannonicalArgumentDict(kw) - extra_column_set = {i[0] for i in kw.get('order_by_list', ())} - kw.setdefault('select_dict', {}).update( - (x.replace('.', '_') + '__ext__', x) - for x in extra_column_set if not x.endswith('__score__')) - - sql_kw = self._generateSQLKeywordDict(**kw) - - return self.Resource_zGetMovementHistoryList( - src__=src__, ignore_variation=ignore_variation, - standardise=standardise, - omit_simulation=omit_simulation, - only_accountable=only_accountable, - omit_input=omit_input, omit_output=omit_output, - omit_asset_increase=omit_asset_increase, - omit_asset_decrease=omit_asset_decrease, - initial_running_total_quantity= - initial_running_total_quantity, - initial_running_total_price= - initial_running_total_price, - precision=precision, **sql_kw) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getMovementHistoryStat') - def getMovementHistoryStat(self, src__=0, **kw): - """ - getMovementHistoryStat is the pending to getMovementHistoryList - for ListBox stat - - supported parameters are similar to ones accepted by getInventoryList - with the exception of group_by_* - """ - sql_kw = self._generateSQLKeywordDict(**kw) - inventory_list = self.getInventoryList(ignore_group_by=1, **kw) - assert len(inventory_list) == 1 - return inventory_list[0] - - security.declareProtected(Permissions.AccessContentsInformation, - 'getNextAlertInventoryDate') - def getNextAlertInventoryDate(self, reference_quantity=0, src__=0, - simulation_period='Future', from_date=None, - range='min', - initial_inventory_kw=None, - inventory_list_kw=None, - **kw): - """ - Give the next date where the quantity is lower than the - reference quantity. This is calculated by first looking if inventory - right now is good or not. If not, then look at inventory list until - a movement makes the inventory like expected. - - range - either 'min' (default) or 'nlt'. With 'nlt', returns - the next date where inventory is above reference_quantity - - initial_inventory_kw - additional parameters for the initial inventory - - inventory_list_kw - additional parameters for looking at next movements - (exemple: use omit_output) - """ - result = None - # First look at current inventory, we might have already an inventory - # lower than reference_quantity - def getCheckQuantityMethod(): - if range == 'min': - return lambda x: x < reference_quantity - elif range == 'nlt': - return lambda x: x >= reference_quantity - else: - raise ValueError("Uknown range type : %s" % (range,)) - - checkQuantity = getCheckQuantityMethod() - if from_date is None: - from_date = DateTime() - def getAugmentedInventoryKeyword(additional_kw): - inventory_kw = kw - if additional_kw: - inventory_kw = kw.copy() - inventory_kw.update(additional_kw) - return inventory_kw - inventory_method = getattr(self, "get%sInventory" % simulation_period) - initial_inventory = inventory_method(at_date=from_date, - **getAugmentedInventoryKeyword(initial_inventory_kw)) - if checkQuantity(initial_inventory): - result = from_date - else: - inventory_list_method = getattr(self, - "get%sInventoryList" % simulation_period) - inventory_list = inventory_list_method(src__=src__, from_date=from_date, - sort_on = (('date', 'ascending'),), group_by_movement=1, - **getAugmentedInventoryKeyword(inventory_list_kw)) - if src__ : - return inventory_list - total_inventory = initial_inventory - for inventory in inventory_list: - if inventory['inventory'] is not None: - total_inventory += inventory['inventory'] - if checkQuantity(total_inventory): - result = inventory['date'] - break - return result - - security.declareProtected(Permissions.AccessContentsInformation, - 'getNextNegativeInventoryDate') - def getNextNegativeInventoryDate(self, **kw): - """ - Deficient Inventory with a reference_quantity of 0, so when the - stock will be negative - """ - return self.getNextAlertInventoryDate(reference_quantity=0, **kw) - - ####################################################### - # Traceability management - security.declareProtected(Permissions.AccessContentsInformation, 'getTrackingList') - def getTrackingList(self, src__=0, - strict_simulation_state=1, history=0, **kw) : - """ - Returns a list of items in the form - - uid (of item) - date - node_uid - section_uid - resource_uid - variation_text - delivery_uid - - If at_date is provided, returns the a list which answers - to the question "where are those items at this date" or - "which are those items which are there a this date" - - If at_date is not provided, returns a history of positions - which answers the question "where have those items been - between this time and this time". This will be handled by - something like getTrackingHistoryList - - This method is only suitable for singleton items (an item which can - only be at a single place at a given time). Such items include - containers, serial numbers (ex. for engine), rolls with subrolls, - - This method is not suitable for batches (ex. a coloring batch). - For such items, standard getInventoryList method is appropriate - - Parameters are the same as for getInventory. - - Default sort orders is based on dates, reverse. - - - from_date (>=) - - - to_date (<) - - - at_date (<=) - only take rows which date is <= at_date - - history (boolean) - keep history variations - - resource (only in generic API in simulation) - - node - only take rows in stock table which node_uid is equivalent to node - - section - only take rows in stock table which section_uid is equivalent to section - - resource_category - only take rows in stock table which resource_uid is in resource_category - - node_category - only take rows in stock table which node_uid is in section_category - - section_category - only take rows in stock table which section_uid is in section_category - - variation_text - only take rows in stock table with specified variation_text - XXX this way of implementing variation selection is far from perfect - - variation_category - variation or list of possible variations - Deprecated, use variation_text - - simulation_state - only take rows with specified simulation_state - - selection_domain, selection_report - see ListBox - - **kw - if we want extended selection with more keywords (but bad performance) - check what we can do with buildSQLQuery - - Extra parameters for getTrackingList : - - item - - input - if set, answers to the question "which are those items which have been - delivered for the first time after from_date". Cannot be used with output - - output - if set, answers to the question "which are those items which have been - delivered for the last time before at_date or to_date". Cannot be used with input - - """ - next_item_simulation_state = kw.pop('next_item_simulation_state', None) - new_kw = self._generateSQLKeywordDict(table='item',strict_simulation_state=strict_simulation_state,**kw) - for key in ('at_date', 'to_date'): - value = kw.get(key, None) - if value is not None: - if isinstance(value, DateTime): - value = value.toZone('UTC').ISO() - value = '%s' % (value, ) - # Do not remove dates in new_kw, they are required in - # order to do a "select item left join item on date" - new_kw[key] = value - - # Extra parameters for the SQL Method - new_kw['join_on_item'] = not history and (new_kw.get('at_date') or \ - new_kw.get('to_date') or \ - new_kw.get('input') or \ - new_kw.get('output')) - new_kw['date_condition_in_join'] = not (new_kw.get('input') or new_kw.get('output')) - - # Pass simulation state to request - if next_item_simulation_state: - new_kw['simulation_state_list'] = next_item_simulation_state - elif kw.has_key('item.simulation_state'): - new_kw['simulation_state_list'] = kw['item.simulation_state'] - else: - new_kw['simulation_state_list'] = None - - return self.Resource_zGetTrackingList(src__=src__, - **new_kw) - - security.declareProtected(Permissions.AccessContentsInformation, 'getCurrentTrackingList') - def getCurrentTrackingList(self, **kw): - """ - Returns list of current inventory grouped by section or site - """ - portal = self.getPortalObject() - kw['item.simulation_state'] = portal\ - .getPortalCurrentInventoryStateList() - kw['next_item_simulation_state'] = portal\ - .getPortalCurrentInventoryStateList() + portal\ - .getPortalTransitInventoryStateList() - return self.getTrackingList(**kw) - - security.declareProtected(Permissions.AccessContentsInformation, 'getCurrentTrackingHistoryList') - def getCurrentTrackingHistoryList(self, **kw): - """ - Returns list of current inventory grouped by section or site - """ - kw['item.simulation_state'] = self.getPortalObject()\ - .getPortalCurrentInventoryStateList() - return self.getTrackingHistoryList(**kw) - - security.declareProtected(Permissions.AccessContentsInformation, 'getTrackingHistoryList') - def getTrackingHistoryList(self, **kw): - """ - Returns history list of inventory grouped by section or site - """ - kw['history'] = 1 - return self.getTrackingList(**kw) - - security.declareProtected(Permissions.AccessContentsInformation, 'getFutureTrackingList') - def getFutureTrackingList(self, **kw): - """ - Returns list of future inventory grouped by section or site - """ - portal = self.getPortalObject() - kw['item.simulation_state'] = portal.getPortalFutureInventoryStateList() + \ - portal.getPortalTransitInventoryStateList() + \ - portal.getPortalReservedInventoryStateList() + \ - portal.getPortalCurrentInventoryStateList() - return self.getTrackingList(**kw) - - ####################################################### - # Capacity Management - security.declareProtected( Permissions.ModifyPortalContent, 'updateCapacity' ) - def updateCapacity(self, node): - capacity_item_list = [] - for o in node.contentValues(): - if o.isCapacity(): - # Do whatever is needed - capacity_item_list += o.asCapacityItemList() - pass - # Do whatever with capacity_item_list - # and store the resulting new capacity in node - node._capacity_item_list = capacity_item_list - - security.declareProtected( Permissions.ModifyPortalContent, 'isMovementInsideCapacity' ) - def isMovementInsideCapacity(self, movement): - """ - Purpose: provide answer to customer for the question "can you do it ?" - - movement: - date - source destination (2 nodes) - source_section ... - """ - # Get nodes and dat - source_node = movement.getSourceValue() - destination_node = movement.getDestinationValue() - start_date = movement.getStartDate() - stop_date = movement.getStopDate() - # Return result - return self.isNodeInsideCapacity(source_node, start_date, additional_movement=movement, sign=1) and self.isNodeInsideCapacity(destination_node, stop_date, additional_movement=movement, sign=-1) - - security.declareProtected( Permissions.ModifyPortalContent, 'isNodeInsideCapacity' ) - def isNodeInsideCapacity(self, node, date, simulation_state=None, additional_movement=None, sign=1): - """ - Purpose: decide if a node is consistent with its capacity definitions - at a certain date (ie. considreing the stock / production history - """ - # First get the current inventory situation for this node - inventory_list = node.getInventoryList(XXXXX) - # Add additional movement - if additional_movement: - inventory_list = inventory_list + sign * additional_movement # needs to be implemented - # Return answer - return self.isAmountListInsideCapacity(node, inventory_list) - - security.declareProtected( Permissions.ModifyPortalContent, 'isAmountListInsideCapacity' ) - def isAmountListInsideCapacity(self, node, amount_list, - resource_aggregation_base_category=None, resource_aggregation_depth=None): - """ - Purpose: decide if a list of amounts is consistent with the capacity of a node - - If any resource in amount_list is missing in the capacity of the node, resource - aggregation is performed, based on resource_aggregation_base_category. If the - base category is not specified, it is an error (should guess instead?). The resource - aggregation is done at the level of resource_aggregation_depth in the tree - of categories. If resource_aggregation_depth is not specified, it's an error. - - Assumptions: amount_list is an association list, like ((R1 V1) (R2 V2)). - node has an attribute '_capacity_item_list' which is a list of association lists. - resource_aggregation_base_category is a Base Category object or a list of Base - Category objects or None. - resource_aggregation_depth is a strictly positive integer or None. - """ - # Make a copy of the attribute _capacity_item_list, because it may be necessary - # to modify it for resource aggregation. - capacity_item_list = node._capacity_item_list[:] - - # Make a mapping between resources and its indices. - resource_map = {} - index = 0 - for alist in capacity_item_list: - for pair in alist: - resource = pair[0] -# LOG('isAmountListInsideCapacity', 0, -# "resource is %s" % repr(resource)) - if resource not in resource_map: - resource_map[resource] = index - index += 1 - - # Build a point from the amount list. - point = zeros(index, 'd') # Fill up zeros for safety. - mask_map = {} # This is used to skip items in amount_list. - for amount in amount_list: - if amount[0] in mask_map: - continue - # This will fail, if amount_list has any different resource from the capacity. - # If it has any different point, then we should ...... - # - # There would be two possible different solutions: - # 1) If a missing resource is a meta-resource of resources supported by the capacity, - # it is possible to add the resource into the capacity by aggregation. - # 2) If a missing resource has a meta-resource as a parent and the capacity supports - # the meta-resource directly or indirectly (`indirectly' means `by aggregation'), - # it is possible to convert the missing resource into the meta-resource. - # - # However, another way has been implemented here. This does the following, if the resource - # is not present in the capacity: - # 1) If the value is zero, just ignore the resource, because zero is always acceptable. - # 2) Attempt to aggregate resources both of the capacity and of the amount list. This aggregation - # is performed at the depth of 'resource_aggregation_depth' under the base category - # 'resource_aggregation_base_category'. - # - resource = amount[0] - if resource in resource_map: - point[resource_map[amount[0]]] = amount[1] - else: - if amount[1] == 0: - # If the value is zero, no need to consider. - pass - elif resource_aggregation_base_category is None or resource_aggregation_depth is None: - # XXX use an appropriate error class - # XXX should guess a base category instead of emitting an exception - raise RuntimeError, "The resource '%s' is not found in the capacity, and the argument 'resource_aggregation_base_category' or the argument 'resource_aggregation_depth' is not specified" % resource - else: - # It is necessary to aggregate resources, to guess the capacity of this resource. - - def getAggregationResourceUrl(url, depth): - # Return a partial url of the argument 'url'. - # If 'url' is '/foo/bar/baz' and 'depth' is 2, return '/foo/bar'. - pos = 0 - for i in range(resource_aggregation_depth): - pos = url.find('/', pos+1) - if pos < 0: - break - if pos < 0: - return None - pos = url.find('/', pos+1) - if pos < 0: - pos = len(url) - return url[:pos] - - def getAggregatedResourceList(aggregation_url, category, resource_list): - # Return a list of resources which should be aggregated. 'aggregation_url' is used - # for a top url of those resources. 'category' is a base category for the aggregation. - aggregated_resource_list = [] - for resource in resource_list: - for url in resource.getCategoryMembershipList(category, base=1): - if url.startswith(aggregation_url): - aggregated_resource_list.append(resource) - return aggregated_resource_list - - def getAggregatedItemList(item_list, resource_list, aggregation_resource): - # Return a list of association lists, which is a result of an aggregation. - # 'resource_list' is a list of resources which should be aggregated. - # 'aggregation_resource' is a category object which is a new resource created by - # this aggregation. - # 'item_list' is a list of association lists. - new_item_list = [] - for alist in item_list: - new_val = 0 - new_alist = [] - # If a resource is not a aggregated, then add it to the new alist as it is. - # Otherwise, aggregate it to a single value. - for pair in alist: - if pair[0] in resource_list: - new_val += pair[1] - else: - new_alist.append(pair) - # If it is zero, ignore this alist, as it is nonsense. - if new_val != 0: - new_alist.append([aggregation_resource, new_val]) - new_item_list.append(new_alist) - return new_item_list - - # Convert this to a string if necessary, for convenience. - if not isinstance(resource_aggregation_base_category, (tuple, list)): - resource_aggregation_base_category = (resource_aggregation_base_category,) - - done = 0 -# LOG('isAmountListInsideCapacity', 0, -# "resource_aggregation_base_category is %s" % repr(resource_aggregation_base_category)) - for category in resource_aggregation_base_category: - for resource_url in resource.getCategoryMembershipList(category, base=1): - aggregation_url = getAggregationResourceUrl(resource_url, - resource_aggregation_depth) - if aggregation_url is None: - continue - aggregated_resource_list = getAggregatedResourceList (aggregation_url, - category, - resource_map.keys()) - # If any, do the aggregation. - if len(aggregated_resource_list) > 0: - aggregation_resource = self.portal_categories.resolveCategory(aggregation_url) - # Add the resource to the mapping. - # LOG('aggregation_resource', 0, str(aggregation_resource)) - resource_map[aggregation_resource] = index - index += 1 - # Add the resource to the point. - point = resize(point, (index,)) - val = 0 - for aggregated_amount in amount_list: - for url in aggregated_amount[0].getCategoryMembershipList(category, base=1): - if url.startswith(aggregation_url): - val += aggregated_amount[1] - mask_map[aggregated_amount[0]] = None - break - point[index-1] = val - # Add capacity definitions of the resource into the capacity. - capacity_item_list += getAggregatedItemList(capacity_item_list, - aggregated_resource_list, - aggregation_resource) - done = 1 - break - if done: - break - if not done: - raise RuntimeError, "Aggregation failed" - - # Build a matrix from the capacity item list. -# LOG('resource_map', 0, str(resource_map)) - matrix = zeros((len(capacity_item_list)+1, index), 'd') - for index in range(len(capacity_item_list)): - for pair in capacity_item_list[index]: - matrix[index,resource_map[pair[0]]] = pair[1] - -# LOG('isAmountListInsideCapacity', 0, -# "matrix = %s, point = %s, capacity_item_list = %s" % (str(matrix), str(point), str(capacity_item_list))) - return solve(matrix, point) - - - # Asset Price Calculation - def updateAssetPrice(self, resource, variation_text, section_category, node_category, - strict_membership=0, simulation_state=None): - if simulation_state is None: - simulation_state = self.getPortalCurrentInventoryStateList() - category_tool = getToolByName(self, 'portal_categories') - section_value = category_tool.resolveCategory(section_category) - node_value = category_tool.resolveCategory(node_category) - # Initialize price - current_asset_price = 0.0 # Missing: initial inventory price !!! - current_inventory = 0.0 - # Parse each movement - brain_list = self.Resource_zGetMovementHistoryList(resource=[resource], - variation_text=variation_text, - section_category=section_category, - node_category=node_category, - strict_membership=strict_membership, - simulation_state=simulation_state) # strict_membership not taken into account - # We select movements related to certain nodes (ex. Stock) and sections (ex.Coramy Group) - result = [] - for b in brain_list: - m = b.getObject() - if m is not None: - previous_inventory = current_inventory - inventory_quantity = b.quantity # We should use the aggregated quantity provided by Resource_zGetMovementHistoryList - quantity = m.getQuantity() # The movement quantity is important to determine the meaning of source and destination - # Maybe we should take care of target qty in delired deliveries - if quantity is None: - quantity = 0.0 - if m.getSourceValue() is None: - # This is a production movement or an inventory movement - # Use Industrial Price - current_inventory += inventory_quantity # Update inventory - if m.getPortalType() in ('Inventory Line', 'Inventory Cell'): # XX should be replaced by isInventory ??? - asset_price = m.getPrice() - if asset_price in (0.0, None): - asset_price = current_asset_price # Use current price if no price defined - else: # this is a production - asset_price = m.getIndustrialPrice() - if asset_price is None: asset_price = current_asset_price # Use current price if no price defined - result.append((m.getRelativeUrl(), m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), - m.getQuantity(), 'Production or Inventory', 'Price: %s' % asset_price - )) - elif m.getDestinationValue() is None: - # This is a consumption movement or an inventory movement - current_inventory += inventory_quantity # Update inventory - asset_price = current_asset_price - result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), - m.getQuantity(), 'Consumption or Inventory', 'Price: %s' % asset_price - )) - elif m.getSourceValue().isAcquiredMemberOf(node_category) and m.getDestinationValue().isAcquiredMemberOf(node_category): - # This is an internal movement - current_inventory += inventory_quantity # Update inventory - asset_price = current_asset_price - result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), - m.getQuantity(), 'Internal', 'Price: %s' % asset_price - )) - elif m.getSourceValue().isAcquiredMemberOf(node_category) and quantity < 0: - # This is a physically inbound movement - try to use commercial price - if m.getSourceSectionValue() is None: - # No meaning - current_inventory += inventory_quantity # Update inventory - asset_price = current_asset_price - result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), - m.getQuantity(), 'Error', 'Price: %s' % asset_price - )) - elif m.getDestinationSectionValue() is None: - # No meaning - current_inventory += inventory_quantity # Update inventory - asset_price = current_asset_price - result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), - m.getQuantity(), 'Error', 'Price: %s' % asset_price - )) - elif m.getDestinationSectionValue().isAcquiredMemberOf(section_category): - current_inventory += inventory_quantity # Update inventory - if m.getDestinationValue().isAcquiredMemberOf('site/Piquage'): - # Production - asset_price = m.getIndustrialPrice() - if asset_price is None: asset_price = current_asset_price # Use current price if no price defined - result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), - m.getQuantity(), 'Production', 'Price: %s' % asset_price - )) - else: - # Inbound from same section - asset_price = current_asset_price - result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), - m.getQuantity(), 'Inbound same section', 'Price: %s' % asset_price - )) - else: - current_inventory += inventory_quantity # Update inventory - asset_price = m.getPrice() - result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), - m.getQuantity(), 'Inbound different section', 'Price: %s' % asset_price - )) - elif m.getDestinationValue().isAcquiredMemberOf(node_category) and quantity > 0: - # This is a physically inbound movement - try to use commercial price - if m.getSourceSectionValue() is None: - # No meaning - current_inventory += inventory_quantity # Update inventory - asset_price = current_asset_price - result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), - m.getQuantity(), 'Error', 'Price: %s' % asset_price - )) - elif m.getDestinationSectionValue() is None: - # No meaning - current_inventory += inventory_quantity # Update inventory - asset_price = current_asset_price - result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), - m.getQuantity(), 'Error', 'Price: %s' % asset_price - )) - elif m.getSourceSectionValue().isAcquiredMemberOf(section_category): - current_inventory += inventory_quantity # Update inventory - if m.getSourceValue().isAcquiredMemberOf('site/Piquage'): - # Production - asset_price = m.getIndustrialPrice() - if asset_price is None: asset_price = current_asset_price # Use current price if no price defined - result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), - m.getQuantity(), 'Production', 'Price: %s' % asset_price - )) - else: - # Inbound from same section - asset_price = current_asset_price - result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), - m.getQuantity(), 'Inbound same section', 'Price: %s' % asset_price - )) - else: - current_inventory += inventory_quantity # Update inventory - asset_price = m.getPrice() - result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), - m.getQuantity(), 'Inbound different section', 'Price: %s' % asset_price - )) - else: - # Outbound movement - current_inventory += inventory_quantity # Update inventory - asset_price = current_asset_price - result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), - m.getQuantity(), 'Outbound', 'Price: %s' % asset_price - )) - - # Update asset_price - if current_inventory > 0: - if inventory_quantity is not None: - # Update price with an average of incoming goods and current goods - current_asset_price = ( current_asset_price * previous_inventory + asset_price * inventory_quantity ) / float(current_inventory) - else: - # New price is the price of incoming goods - negative stock has no meaning for asset calculation - current_asset_price = asset_price - - result.append(('###New Asset Price', current_asset_price, 'New Inventory', current_inventory)) - - # Update Asset Price on the right side - if m.getSourceSectionValue() is not None and m.getSourceSectionValue().isAcquiredMemberOf(section_category): - # for each movement, source section is member of one and one only accounting category - # therefore there is only one and one only source asset price - m._setSourceAssetPrice(current_asset_price) - #quantity = m.getInventoriatedQuantity() - #if quantity: - # #total_asset_price = - current_asset_price * quantity - # #m.Movement_zSetSourceTotalAssetPrice(uid=m.getUid(), total_asset_price = total_asset_price) - # m._setSourceAssetPrice(current_asset_price) - if m.getDestinationSectionValue() is not None and m.getDestinationSectionValue().isMemberOf(section_category): - # for each movement, destination section is member of one and one only accounting category - # therefore there is only one and one only destination asset price - m._setDestinationAssetPrice(current_asset_price) - #quantity = m.getInventoriatedQuantity() - #if quantity: - # total_asset_price = current_asset_price * quantity - # m.Movement_zSetDestinationTotalAssetPrice(uid=m.getUid(), total_asset_price = total_asset_price) - # Global reindexing required afterwards in any case: so let us do it now - # Until we get faster methods (->reindexObject()) - m.reindexObject() - - return result - - def _findBuilderForDelivery(self, delivery, movement_portal_type): - """ - Find out the builder corresponding to a delivery by looking at the business process - """ - builder = None - portal_type = delivery.getPortalType() - for business_link in delivery.asComposedDocument().objectValues(portal_type="Business Link"): - for business_link_builder in business_link.getDeliveryBuilderValueList(): - if business_link_builder.getDeliveryPortalType() == portal_type \ - and business_link_builder.getDeliveryLinePortalType() == movement_portal_type: - builder = business_link_builder - break - if builder is not None: - break - return builder - - security.declareProtected( Permissions.ModifyPortalContent, 'mergeDeliveryList' ) - def mergeDeliveryList(self, delivery_list): - """ - Merge multiple deliveries into one delivery. - All delivery lines are merged into the first one. - The first one is therefore called main_delivery here. - The others are cancelled. - """ - # Sanity checks. - if not(len(delivery_list) >=2): - raise ValueError("Please select at least 2 deliveries") - portal= self.getPortalObject() - translateString = portal.Base_translateString - error_list = [] - if len(delivery_list) > 1: - portal_type_set = set([x.getPortalType() for x in delivery_list]) - if len(portal_type_set) != 1: - error_list.append(translateString("Please select only deliveries of same type")) - else: - allowed_state_set = set(portal.getPortalReservedInventoryStateList() + \ - portal.getPortalFutureInventoryStateList()) - found_state_set = set([x.getSimulationState() for x in delivery_list]) - if found_state_set.difference(allowed_state_set): - error_list.append(translateString("Found delivery having unexpected status for merge")) - else: - movement_portal_type_set = set() - for delivery in delivery_list: - movement_portal_type_set.update([x.getPortalType() for x in delivery.getMovementList()]) - if len(movement_portal_type_set) != 1: - error_list.append(translateString("Please Select only movement of same type")) - else: - # Allow to call a script to do custom checking conditions before merge - main_delivery = delivery_list[0] - check_merge_condition_method = main_delivery._getTypeBasedMethod("checkMergeConditionOnDeliveryList") - if check_merge_condition_method is not None: - error_list.extend(check_merge_condition_method(delivery_list=delivery_list)) - if len(error_list) == 0: - # so far so good - # in delivery_list we have list of delivery to merge - simulation_movement_list = [] - to_copy_delivery_line_list = [] # for lines not coming from upper simulation, thus - # created by hand should be manually added to main - # delivery since they are not coming from builder - for delivery in delivery_list: - line_id_to_delete_list = [] - for movement in delivery.getMovementList(): - related_simulation_movement_list = movement.getDeliveryRelatedValueList() - for simulation_movement in related_simulation_movement_list: - # if we are on a root applied rule directly, so in the case of - # a manually added line, we have to copy - # the simulation movement into to main delivery - if simulation_movement.getParentValue().getParentValue().getId() == "portal_simulation": - # For manually added lines, make sure we have only one simulation movement - assert len(related_simulation_movement_list) == 1 - if not(delivery is main_delivery): - to_copy_delivery_line_list.append(movement) - else: - simulation_movement.setDeliveryValue(None) - simulation_movement_list.append(simulation_movement) - # Since we keep the main delivery, we remove existing lines already - # coming from builder to let builder recreate them in the same time - # as other ones (to possibly merge lines also) - movement_id = movement.getId() - if delivery is main_delivery and not(movement_id in line_id_to_delete_list): - line_id_to_delete_list.append(movement.getId()) - if line_id_to_delete_list: - delivery.manage_delObjects(ids=line_id_to_delete_list) - # It is required to expand again simulation movement, because - # we unlinked them from delivery, so it is possible that some - # properties will change on simulation movement (mostly categories). - # By expanding again, we will avoid having many deliveries instead - # of one when doing "merge" - for simulation_movement in simulation_movement_list: - simulation_movement.expand(expand_policy='immediate') - - # activate builder - movement_portal_type, = movement_portal_type_set - merged_builder = self._findBuilderForDelivery(main_delivery, movement_portal_type) - if merged_builder is None: - error_list.append(translateString("Unable to find builder")) - else: - merged_builder.build(movement_relative_url_list=[q.getRelativeUrl() for q in \ - simulation_movement_list], merge_delivery=True, - delivery_relative_url_list=[main_delivery.getRelativeUrl()]) - # Finally, copy all lines that were created manually on all deliveries except - # the main one - @UnrestrictedMethod - def setMainDeliveryModifiable(delivery): - # set causality state in such way we can modify delivery - delivery.diverge() - setMainDeliveryModifiable(main_delivery) - delivery_type_list = portal.getPortalDeliveryTypeList() - for delivery_line in to_copy_delivery_line_list: - delivery = delivery_line.getParentValue() - if not(delivery.getPortalType() in delivery_type_list): - raise NotImplementedError("Merge of deliveries doe not yet handle case of cells") - copy_data = delivery.manage_copyObjects(ids=[delivery_line.getId()]) - main_delivery.manage_pasteObjects(copy_data) - main_delivery.updateCausalityState() - - # Finally do cleanup - for delivery in delivery_list[1:]: - # cancel, delete - to disallow any user related operations on those deliveries - after_merge_method = delivery._getTypeBasedMethod('cleanDeliveryAfterMerge') - if after_merge_method is not None: - after_merge_method() - else: - error_list.append(translateString("Please select at least two deliveries")) - return error_list - - ####################################################### - # Sequence - security.declareProtected(Permissions.AccessContentsInformation, - 'getSequence') - def getSequence(self, **kw): - """ - getSequence is take the same parameters as Sequence constructor, - and return a Sequence. - """ - return Sequence(**kw) - - ####################################################### - # Time Management - security.declareProtected(Permissions.AccessContentsInformation, - 'getAvailableTime') - def getAvailableTime(self, from_date=None, to_date=None, - portal_type=[], node=[], - resource=[], src__=0, **kw): - """ - Calculate available time for a node - Returns an inventory of a single or multiple resources on a single - node as a single float value - - from_date (>=) - only take rows which mirror_date is >= from_date - - to_date (<) - only take rows which date is < to_date - - node - only take rows in stock table which node_uid is - equivalent to node - - resource - only take rows in stock table which resource_uid is - equivalent to resource - - portal_type - only take rows in stock table which portal_type - is in portal_type parameter - """ - # XXX For now, consider that from_date and to_date are required - if (from_date is None) or (to_date is None): - raise NotImplementedError, \ - "getAvailableTime does not managed yet None values" - portal = self.getPortalObject() - # Calculate portal_type - if portal_type == []: - portal_type = portal.getPortalCalendarPeriodTypeList() - - simulation_state = portal.getPortalCurrentInventoryStateList() + \ - portal.getPortalTransitInventoryStateList() + \ - portal.getPortalReservedInventoryStateList() - - sql_result = portal.Node_zGetAvailableTime( - from_date=from_date, - to_date=to_date, - portal_type=portal_type, - node=node, - resource=resource, - simulation_state=simulation_state, - src__=src__, **kw) - if not src__: - result = 0 - if len(sql_result) == 1: - result = sql_result[0].total_quantity - else: - result = sql_result - return result - - security.declareProtected(Permissions.AccessContentsInformation, - 'getAvailableTimeSequence') - def getAvailableTimeSequence(self, from_date, to_date, - portal_type=[], node=[], - resource=[], - src__=0, - **kw): - """ - Calculate available time for a node in multiple period of time. - Each row is the available time for a specific period - - node - only take rows in stock table which node_uid is - equivalent to node - - portal_type - only take rows in stock table which portal_type - is in portal_type parameter - - resource - only take rows in stock table which resource_uid is - equivalent to resource - - from_date (>=) - return period which start >= from_date - - to_date (<) - return period which start < to_date - - second, minute, - hour, day, - month, year - duration of each time period (cumulative) - """ - portal = self.getPortalObject() - # Calculate portal_type - if portal_type == []: - portal_type = portal.getPortalCalendarPeriodTypeList() - - sequence = Sequence(from_date, to_date, **kw) - for sequence_item in sequence: - setattr(sequence_item, 'total_quantity', - self.getAvailableTime( - from_date=sequence_item.from_date, - to_date=sequence_item.to_date, - portal_type=portal_type, - node=node, - resource=resource, - src__=src__)) - return sequence - - security.declareProtected(Permissions.AccessContentsInformation, - 'getAvailableTimeMovementList') - def getAvailableTimeMovementList(self, from_date, to_date, - **kw): - """ - Calculate available time movement list by taking into account - both available time and not available time. - - Necessary parameter is at least node. - - Parameters supported by getMovementHistoryList are supported here. - - from_date (>=) - return period which start >= from_date - - to_date (<) - return period which start < to_date - """ - portal = self.getPortalObject() - if kw.get("simulation_state", None) is None: - kw["simulation_state"] = portal.getPortalCurrentInventoryStateList() + \ - portal.getPortalTransitInventoryStateList() + \ - portal.getPortalReservedInventoryStateList() - movement_list = self.getMovementHistoryList(from_date=from_date, - to_date=to_date, group_by_movement=1, - group_by_date=1, **kw) - # do import on top, but better to avoid breaking instances with older softwares - from interval import IntervalSet, Interval - # we look at all movements, and we build a set of intervals for available - # time, another for not available time, and we do substraction of both sets - assignment_interval_set = IntervalSet() - leave_interval_set = IntervalSet() - result_list = [] - - def getOrderedMovementDates(movement): - date_list = [movement.date, movement.mirror_date] - date_list.sort() - return date_list - - movement_availability_dict = {} # to later map availability intervals with their movements - for movement in movement_list: - start_date, stop_date = getOrderedMovementDates(movement) - current_interval = Interval(start_date, stop_date) - # case of available time - if movement.total_quantity > 0: - assignment_interval_set.add(current_interval) - movement_availability_dict[current_interval] = movement - # case of not available time - else: - leave_interval_set.add(current_interval) - i = 0 - # Parse all calculated availability_interval to find matching movements to - # be returned in the result. IntervalSet are already ordered - for availability_interval in (assignment_interval_set - leave_interval_set): - while True: - assignment_interval = assignment_interval_set[i] - if availability_interval in assignment_interval: - result_list.append(movement_availability_dict[assignment_interval].asContext( - start_date=availability_interval.lower_bound, - stop_date=availability_interval.upper_bound)) - break - else: - i += 1 - return result_list - - def _checkExpandAll(self, activate_kw={}): - """Check all simulation trees using AppliedRule._checkExpand - """ - portal = self.getPortalObject() - active_process = portal.portal_activities.newActiveProcess().getPath() - kw = dict(priority=3, tag='checkExpand') - kw.update(group_method_cost=1, max_retry=0, - active_process=active_process, **activate_kw) - self._recurseCallMethod('_checkExpand', min_depth=1, max_depth=1, - activate_kw=kw) - return active_process - -from Products.ERP5Type.DateUtils import addToDate - -class SequenceItem: - """ - SequenceItem define a time period. - period. - """ - def __init__(self, from_date, to_date): - self.from_date = from_date - self.to_date = to_date - -class Sequence: - """ - Sequence is a iterable object, which calculate a range of time - period. - """ - def __init__(self, from_date, to_date, - second=None, minute=None, hour=None, - day=None, month=None, year=None): - """ - Calculate a list of time period. - Time period is a 2-tuple of 2 DateTime, which represent the from date - and to date of the period. - - The start date of a period is calculated with the rule - start_date of the previous + period duration - - from_date (>=) - return period which start >= from_date - - to_date (<) - return period which start < to_date - - second, minute, - hour, day, - month, year - duration of each time period (cumulative) - at least one of those parameters must be specified. - """ - if not (second or minute or hour or day or month or year): - raise ValueError('Period duration must be specified') - - self.item_list = [] - # Calculate all time period - current_from_date = from_date - while current_from_date < to_date: - current_to_date = addToDate(current_from_date, - second=second, - minute=minute, - hour=hour, - day=day, - month=month, - year=year) - self.item_list.append(SequenceItem(current_from_date, - current_to_date)) - current_from_date = current_to_date - - def __len__(self): - return len(self.item_list) - - def __getitem__(self, key): - return self.item_list[key] - - def __contains__(self, value): - return (value in self.item_list) - - def __iter__(self): - for x in self.item_list: - yield x - -InitializeClass(Sequence) -allow_class(Sequence) -InitializeClass(SequenceItem) -allow_class(SequenceItem) - -InitializeClass(SimulationTool) diff --git a/product/ERP5/__init__.py b/product/ERP5/__init__.py index 0c4a1fb4b6437861c701906d418d30c25d685d8f..0558a91f46990418faf37446f3412077cefc0234 100644 --- a/product/ERP5/__init__.py +++ b/product/ERP5/__init__.py @@ -35,17 +35,13 @@ from Products.ERP5Type.Utils import initializeProduct, updateGlobals import sys, Permissions this_module = sys.modules[ __name__ ] document_classes = updateGlobals( this_module, globals(), permissions_module = Permissions) -from Products.PythonScripts.Utility import allow_class from AccessControl import ModuleSecurityInfo -import MovementGroup -allow_class(MovementGroup) - from Products.ERP5Type.Globals import package_home product_path = package_home( globals() ) # Define object classes and tools -from Tool import CategoryTool, SimulationTool, RuleTool, IdTool, TemplateTool,\ +from Tool import CategoryTool, RuleTool, IdTool, TemplateTool,\ TestTool, DomainTool, AlarmTool, OrderTool, DeliveryTool,\ TrashTool, ContributionTool, NotificationTool, PasswordTool,\ GadgetTool, ContributionRegistryTool, IntrospectionTool,\ @@ -59,7 +55,6 @@ object_classes = ( ERP5Site.ERP5Site, SQLMethod.SQLMethod, ) portal_tools = ( CategoryTool.CategoryTool, - SimulationTool.SimulationTool, RuleTool.RuleTool, IdTool.IdTool, TemplateTool.TemplateTool, diff --git a/product/ERP5/Document/AppliedRule.py b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.AppliedRule.py similarity index 95% rename from product/ERP5/Document/AppliedRule.py rename to product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.AppliedRule.py index 598cc6013e1ebd84fb1344327792d9ceb6947983..06f61fca84456843d1c4f53e70ad281980405034 100644 --- a/product/ERP5/Document/AppliedRule.py +++ b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.AppliedRule.py @@ -34,15 +34,17 @@ import zope.interface from zExceptions import ExceptionFormatter from ZODB.POSException import ConflictError from AccessControl import ClassSecurityInfo -from Products.ERP5Type import Permissions, PropertySheet, interfaces +from Products.ERP5Type import Permissions, PropertySheet from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter from Products.ERP5Type.Base import WorkflowMethod from Products.ERP5Type.Globals import PersistentMapping from Products.ERP5Type.XMLObject import XMLObject from Products.ERP5Type.TransactionalVariable import getTransactionalVariable -from Products.ERP5.ExpandPolicy import TREE_DELIVERED_CACHE_KEY -from Products.ERP5.mixin.explainable import ExplainableMixin -from Products.ERP5.mixin.rule import RuleMixin +from erp5.component.module.ExpandPolicy import TREE_DELIVERED_CACHE_KEY +from erp5.component.mixin.ExplainableMixin import ExplainableMixin +from erp5.component.mixin.RuleMixin import RuleMixin +from erp5.component.interface.IExpandable import IExpandable +from erp5.component.interface.IMovementCollection import IMovementCollection class AppliedRule(XMLObject, ExplainableMixin): """ @@ -80,8 +82,8 @@ class AppliedRule(XMLObject, ExplainableMixin): ) # Declarative interfaces - zope.interface.implements(interfaces.IExpandable, - interfaces.IMovementCollection) + zope.interface.implements(IExpandable, + IMovementCollection) def tpValues(self) : """ show the content in the left pane of the ZMI """ @@ -170,6 +172,7 @@ class AppliedRule(XMLObject, ExplainableMixin): """ return self.objectValues(portal_type=RuleMixin.movement_type) + # pylint: disable=cell-var-from-loop def _migrateSimulationTree(self, get_matching_key, get_original_property_dict, @@ -370,8 +373,8 @@ class AppliedRule(XMLObject, ExplainableMixin): for delivery, sm_dict in deleted: if not sm_dict: del old_dict[delivery] - from Products.ERP5.Document.SimulationMovement import SimulationMovement - from Products.ERP5.mixin.movement_collection_updater import \ + from erp5.component.document.SimulationMovement import SimulationMovement + from erp5.component.mixin.MovementCollectionUpdaterMixin import \ MovementCollectionUpdaterMixin as mixin # Patch is already protected by WorkflowMethod.disable lock. orig_updateMovementCollection = mixin.__dict__['updateMovementCollection'] @@ -404,8 +407,8 @@ class AppliedRule(XMLObject, ExplainableMixin): document = object_list.popleft() portal_type = document.getPortalType() document_dict = {'portal_type': portal_type} - for property in property_dict[portal_type]: - document_dict[property] = document.getProperty(property) + for property_ in property_dict[portal_type]: + document_dict[property_] = document.getProperty(property_) rule_dict[document.getRelativeUrl()] = document_dict object_list += document.objectValues() return rule_dict diff --git a/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.AppliedRule.xml b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.AppliedRule.xml new file mode 100644 index 0000000000000000000000000000000000000000..224bc81415c0bd8d5f662421b43b357c9dbe35ad --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.AppliedRule.xml @@ -0,0 +1,110 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Document Component" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>default_reference</string> </key> + <value> <string>AppliedRule</string> </value> + </item> + <item> + <key> <string>default_source_reference</string> </key> + <value> <string>Products.ERP5.Document.AppliedRule</string> </value> + </item> + <item> + <key> <string>description</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>document.erp5.AppliedRule</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Document Component</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>text_content_error_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>text_content_warning_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>version</string> </key> + <value> <string>erp5</string> </value> + </item> + <item> + <key> <string>workflow_history</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="PersistentMapping" module="Persistence.mapping"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>data</string> </key> + <value> + <dictionary> + <item> + <key> <string>component_validation_workflow</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_log</string> </key> + <value> + <list> + <dictionary> + <item> + <key> <string>action</string> </key> + <value> <string>validate</string> </value> + </item> + <item> + <key> <string>validation_state</string> </key> + <value> <string>validated</string> </value> + </item> + </dictionary> + </list> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.Delivery.py b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.Delivery.py index e5383137b5bbbe81247c53f7d8c1fad6e741395d..2f343590d30c85964de7d83dbf3dd28f7c80b6ee 100644 --- a/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.Delivery.py +++ b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.Delivery.py @@ -44,6 +44,8 @@ from Products.ERP5.mixin.composition import CompositionMixin from erp5.component.mixin.SimulableMixin import SimulableMixin from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod, \ unrestricted_apply +from erp5.component.interface.IMovementCollection import IMovementCollection +from erp5.component.interface.IDivergenceController import IDivergenceController class Delivery(XMLObject, ImmobilisationDelivery, SimulableMixin, CompositionMixin, AmountGeneratorMixin): @@ -75,8 +77,8 @@ class Delivery(XMLObject, ImmobilisationDelivery, SimulableMixin, # Declarative interfaces zope.interface.implements(interfaces.IAmountGenerator, - interfaces.IDivergenceController, - interfaces.IMovementCollection) + IDivergenceController, + IMovementCollection) security.declareProtected(Permissions.AccessContentsInformation, 'isAccountable') def isAccountable(self): diff --git a/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.DeliveryCell.py b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.DeliveryCell.py index 2d36072f5452932e663bf01c2af1e867e40a1161..5e1b6f034f0e67fbe1dc4eb769d6fc284f101bff 100644 --- a/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.DeliveryCell.py +++ b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.DeliveryCell.py @@ -33,11 +33,12 @@ import zope.interface from AccessControl import ClassSecurityInfo from AccessControl.PermissionRole import PermissionRole -from Products.ERP5Type import Permissions, PropertySheet, interfaces +from Products.ERP5Type import Permissions, PropertySheet from Products.ERP5.Document.Movement import Movement from Products.ERP5.Document.MappedValue import MappedValue from Products.ERP5.Document.ImmobilisationMovement import ImmobilisationMovement +from erp5.component.interface.IDivergenceController import IDivergenceController class DeliveryCell(MappedValue, Movement, ImmobilisationMovement): """ @@ -66,7 +67,7 @@ class DeliveryCell(MappedValue, Movement, ImmobilisationMovement): ) # Declarative interfaces - zope.interface.implements(interfaces.IDivergenceController,) + zope.interface.implements(IDivergenceController,) security.declareProtected(Permissions.AccessContentsInformation, 'isPredicate') def isPredicate(self): diff --git a/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.DeliveryLine.py b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.DeliveryLine.py index a834adfc67a760fc57106c1e0c8ba228eb75c1de..dde60f6ab2465e420a45d4975908b3a8f23d2c5e 100644 --- a/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.DeliveryLine.py +++ b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.DeliveryLine.py @@ -30,7 +30,7 @@ import zope.interface from AccessControl import ClassSecurityInfo -from Products.ERP5Type import Permissions, PropertySheet, interfaces +from Products.ERP5Type import Permissions, PropertySheet from Products.ERP5Type.XMLMatrix import XMLMatrix from Products.ERP5.Document.Movement import Movement @@ -40,6 +40,8 @@ from inspect import getargspec from Products.ERP5Type.Base import Base edit_args_list = getargspec(Base._edit).args +from erp5.component.interface.IDivergenceController import IDivergenceController + class DeliveryLine(Movement, XMLMatrix, ImmobilisationMovement): """ A DeliveryLine object allows to implement lines in @@ -70,7 +72,7 @@ class DeliveryLine(Movement, XMLMatrix, ImmobilisationMovement): ) # Declarative interfaces - zope.interface.implements(interfaces.IDivergenceController,) + zope.interface.implements(IDivergenceController,) # Multiple inheritance definition updateRelatedContent = XMLMatrix.updateRelatedContent diff --git a/product/ERP5/Document/SimulationMovement.py b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.SimulationMovement.py similarity index 97% rename from product/ERP5/Document/SimulationMovement.py rename to product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.SimulationMovement.py index 4ad4a8586a5c6a6438adc584c278003d2c737406..fd9248dc252505e83c1b29197d55c8840393e0b6 100644 --- a/product/ERP5/Document/SimulationMovement.py +++ b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.SimulationMovement.py @@ -33,12 +33,13 @@ from AccessControl import ClassSecurityInfo from Products.ERP5Type import Permissions, PropertySheet, interfaces from Products.ERP5Type.TransactionalVariable import getTransactionalVariable from Products.ERP5.Document.Movement import Movement -from Products.ERP5.ExpandPolicy import policy_dict, TREE_DELIVERED_CACHE_KEY +from erp5.component.module.ExpandPolicy import policy_dict, TREE_DELIVERED_CACHE_KEY from zLOG import LOG, WARNING from Products.ERP5.mixin.property_recordable import PropertyRecordableMixin -from Products.ERP5.mixin.explainable import ExplainableMixin +from erp5.component.mixin.ExplainableMixin import ExplainableMixin +from erp5.component.interface.IExpandable import IExpandable # XXX Do we need to create groups ? (ie. confirm group include confirmed, getting_ready and ready @@ -118,7 +119,7 @@ class SimulationMovement(PropertyRecordableMixin, Movement, ExplainableMixin): ) # Declarative interfaces - zope.interface.implements(interfaces.IExpandable, + zope.interface.implements(IExpandable, interfaces.IPropertyRecordable) def tpValues(self) : @@ -656,25 +657,25 @@ class SimulationMovement(PropertyRecordableMixin, Movement, ExplainableMixin): empty; a movement is only yielded if its causality value is in this set """ object_id_list = document.objectIds() - for id in object_id_list: - if id not in tree_node.visited_movement_dict: + for id_ in object_id_list: + if id_ not in tree_node.visited_movement_dict: # we had not visited it in step #2 - subdocument = document._getOb(id) + subdocument = document._getOb(id_) if subdocument.getPortalType() == "Simulation Movement": path = subdocument.getCausalityValue(portal_type='Business Link') t = (subdocument, path) - tree_node.visited_movement_dict[id] = t + tree_node.visited_movement_dict[id_] = t if path in path_set_to_check: yield t else: # it must be an Applied Rule - subtree = tree_node.get(id, treeNode()) + subtree = tree_node.get(id_, treeNode()) for d in descendantGenerator(subdocument, subtree, path_set_to_check): yield d - for id, t in tree_node.visited_movement_dict.iteritems(): + for id_, t in tree_node.visited_movement_dict.iteritems(): subdocument, path = t to_check = path_set_to_check # do we need to change/copy the set? @@ -684,7 +685,7 @@ class SimulationMovement(PropertyRecordableMixin, Movement, ExplainableMixin): continue to_check = to_check.copy() to_check.remove(path) - subtree = tree_node.get(id, treeNode()) + subtree = tree_node.get(id_, treeNode()) for d in descendantGenerator(subdocument, subtree, to_check): yield d diff --git a/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.SimulationMovement.xml b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.SimulationMovement.xml new file mode 100644 index 0000000000000000000000000000000000000000..056aa8092b77f56f796f8c01c021835161b0447c --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.SimulationMovement.xml @@ -0,0 +1,110 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Document Component" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>default_reference</string> </key> + <value> <string>SimulationMovement</string> </value> + </item> + <item> + <key> <string>default_source_reference</string> </key> + <value> <string>Products.ERP5.Document.SimulationMovement</string> </value> + </item> + <item> + <key> <string>description</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>document.erp5.SimulationMovement</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Document Component</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>text_content_error_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>text_content_warning_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>version</string> </key> + <value> <string>erp5</string> </value> + </item> + <item> + <key> <string>workflow_history</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="PersistentMapping" module="Persistence.mapping"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>data</string> </key> + <value> + <dictionary> + <item> + <key> <string>component_validation_workflow</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_log</string> </key> + <value> + <list> + <dictionary> + <item> + <key> <string>action</string> </key> + <value> <string>validate</string> </value> + </item> + <item> + <key> <string>validation_state</string> </key> + <value> <string>validated</string> </value> + </item> + </dictionary> + </list> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/interfaces/divergence_controller.py b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IDivergenceController.py similarity index 100% rename from product/ERP5/interfaces/divergence_controller.py rename to product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IDivergenceController.py diff --git a/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IDivergenceController.xml b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IDivergenceController.xml new file mode 100644 index 0000000000000000000000000000000000000000..81a21a6c690332eb1f299267050aac71dbcc7789 --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IDivergenceController.xml @@ -0,0 +1,104 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Interface Component" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>default_reference</string> </key> + <value> <string>IDivergenceController</string> </value> + </item> + <item> + <key> <string>default_source_reference</string> </key> + <value> <string>Products.ERP5.interfaces.divergence_controller</string> </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>interface.erp5.IDivergenceController</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Interface Component</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>text_content_error_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>text_content_warning_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>version</string> </key> + <value> <string>erp5</string> </value> + </item> + <item> + <key> <string>workflow_history</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="PersistentMapping" module="Persistence.mapping"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>data</string> </key> + <value> + <dictionary> + <item> + <key> <string>component_validation_workflow</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_log</string> </key> + <value> + <list> + <dictionary> + <item> + <key> <string>action</string> </key> + <value> <string>validate</string> </value> + </item> + <item> + <key> <string>validation_state</string> </key> + <value> <string>validated</string> </value> + </item> + </dictionary> + </list> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/interfaces/expandable.py b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IExpandable.py similarity index 98% rename from product/ERP5/interfaces/expandable.py rename to product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IExpandable.py index d6f23e8c3c8d64910963a360c6947a75460db3cd..fd18b45064567091254b46ccbda4025bfab55bf2 100644 --- a/product/ERP5/interfaces/expandable.py +++ b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IExpandable.py @@ -26,7 +26,7 @@ # ############################################################################## """ -Products.ERP5.interfaces.expandable +erp5.component.interface.IExpandable """ from zope.interface import Interface diff --git a/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IExpandable.xml b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IExpandable.xml new file mode 100644 index 0000000000000000000000000000000000000000..2585d602562d12a2e6f08528ccfb514cae274366 --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IExpandable.xml @@ -0,0 +1,104 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Interface Component" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>default_reference</string> </key> + <value> <string>IExpandable</string> </value> + </item> + <item> + <key> <string>default_source_reference</string> </key> + <value> <string>Products.ERP5.interfaces.expandable</string> </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>interface.erp5.IExpandable</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Interface Component</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>text_content_error_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>text_content_warning_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>version</string> </key> + <value> <string>erp5</string> </value> + </item> + <item> + <key> <string>workflow_history</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="PersistentMapping" module="Persistence.mapping"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>data</string> </key> + <value> + <dictionary> + <item> + <key> <string>component_validation_workflow</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_log</string> </key> + <value> + <list> + <dictionary> + <item> + <key> <string>action</string> </key> + <value> <string>validate</string> </value> + </item> + <item> + <key> <string>validation_state</string> </key> + <value> <string>validated</string> </value> + </item> + </dictionary> + </list> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/interfaces/movement_collection.py b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IMovementCollection.py similarity index 100% rename from product/ERP5/interfaces/movement_collection.py rename to product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IMovementCollection.py diff --git a/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IMovementCollection.xml b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IMovementCollection.xml new file mode 100644 index 0000000000000000000000000000000000000000..4c066e9db513ace2597ceb8bcab63fac293085a5 --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IMovementCollection.xml @@ -0,0 +1,104 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Interface Component" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>default_reference</string> </key> + <value> <string>IMovementCollection</string> </value> + </item> + <item> + <key> <string>default_source_reference</string> </key> + <value> <string>Products.ERP5.interfaces.movement_collection</string> </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>interface.erp5.IMovementCollection</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Interface Component</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>text_content_error_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>text_content_warning_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>version</string> </key> + <value> <string>erp5</string> </value> + </item> + <item> + <key> <string>workflow_history</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="PersistentMapping" module="Persistence.mapping"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>data</string> </key> + <value> + <dictionary> + <item> + <key> <string>component_validation_workflow</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_log</string> </key> + <value> + <list> + <dictionary> + <item> + <key> <string>action</string> </key> + <value> <string>validate</string> </value> + </item> + <item> + <key> <string>validation_state</string> </key> + <value> <string>validated</string> </value> + </item> + </dictionary> + </list> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/interfaces/movement_collection_diff.py b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IMovementCollectionDiff.py similarity index 100% rename from product/ERP5/interfaces/movement_collection_diff.py rename to product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IMovementCollectionDiff.py diff --git a/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IMovementCollectionDiff.xml b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IMovementCollectionDiff.xml new file mode 100644 index 0000000000000000000000000000000000000000..6463032e7561966a0a0c7b3d871c699bb8b43c60 --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IMovementCollectionDiff.xml @@ -0,0 +1,104 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Interface Component" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>default_reference</string> </key> + <value> <string>IMovementCollectionDiff</string> </value> + </item> + <item> + <key> <string>default_source_reference</string> </key> + <value> <string>Products.ERP5.interfaces.movement_collection_diff</string> </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>interface.erp5.IMovementCollectionDiff</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Interface Component</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>text_content_error_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>text_content_warning_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>version</string> </key> + <value> <string>erp5</string> </value> + </item> + <item> + <key> <string>workflow_history</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="PersistentMapping" module="Persistence.mapping"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>data</string> </key> + <value> + <dictionary> + <item> + <key> <string>component_validation_workflow</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_log</string> </key> + <value> + <list> + <dictionary> + <item> + <key> <string>action</string> </key> + <value> <string>validate</string> </value> + </item> + <item> + <key> <string>validation_state</string> </key> + <value> <string>validated</string> </value> + </item> + </dictionary> + </list> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/interfaces/movement_collection_updater.py b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IMovementCollectionUpdater.py similarity index 100% rename from product/ERP5/interfaces/movement_collection_updater.py rename to product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IMovementCollectionUpdater.py diff --git a/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IMovementCollectionUpdater.xml b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IMovementCollectionUpdater.xml new file mode 100644 index 0000000000000000000000000000000000000000..5946b0658a92d90da13e014c48495ad4371d87b1 --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IMovementCollectionUpdater.xml @@ -0,0 +1,104 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Interface Component" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>default_reference</string> </key> + <value> <string>IMovementCollectionUpdater</string> </value> + </item> + <item> + <key> <string>default_source_reference</string> </key> + <value> <string>Products.ERP5.interfaces.movement_collection_updater</string> </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>interface.erp5.IMovementCollectionUpdater</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Interface Component</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>text_content_error_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>text_content_warning_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>version</string> </key> + <value> <string>erp5</string> </value> + </item> + <item> + <key> <string>workflow_history</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="PersistentMapping" module="Persistence.mapping"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>data</string> </key> + <value> + <dictionary> + <item> + <key> <string>component_validation_workflow</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_log</string> </key> + <value> + <list> + <dictionary> + <item> + <key> <string>action</string> </key> + <value> <string>validate</string> </value> + </item> + <item> + <key> <string>validation_state</string> </key> + <value> <string>validated</string> </value> + </item> + </dictionary> + </list> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/interfaces/rule.py b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IRule.py similarity index 95% rename from product/ERP5/interfaces/rule.py rename to product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IRule.py index 226a8a3ecfe143a410ac05ea1510d92e31d74db3..70e5c5e5bd48f3e523349395e7650b818564b323 100644 --- a/product/ERP5/interfaces/rule.py +++ b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IRule.py @@ -27,9 +27,9 @@ # ############################################################################## """ -Products.ERP5.interfaces.rule +erp5.component.interface.IRule """ -from Products.ERP5.interfaces.movement_collection_updater import IMovementCollectionUpdater +from erp5.component.interface.IMovementCollectionUpdater import IMovementCollectionUpdater class IRule(IMovementCollectionUpdater): """Rule interface specification diff --git a/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IRule.xml b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IRule.xml new file mode 100644 index 0000000000000000000000000000000000000000..6612d62cdb881cc75f217ee91ee90cb211183cf8 --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.IRule.xml @@ -0,0 +1,104 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Interface Component" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>default_reference</string> </key> + <value> <string>IRule</string> </value> + </item> + <item> + <key> <string>default_source_reference</string> </key> + <value> <string>Products.ERP5.interfaces.rule</string> </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>interface.erp5.IRule</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Interface Component</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>text_content_error_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>text_content_warning_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>version</string> </key> + <value> <string>erp5</string> </value> + </item> + <item> + <key> <string>workflow_history</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="PersistentMapping" module="Persistence.mapping"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>data</string> </key> + <value> + <dictionary> + <item> + <key> <string>component_validation_workflow</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_log</string> </key> + <value> + <list> + <dictionary> + <item> + <key> <string>action</string> </key> + <value> <string>validate</string> </value> + </item> + <item> + <key> <string>validation_state</string> </key> + <value> <string>validated</string> </value> + </item> + </dictionary> + </list> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/interfaces/simulation_movement.py b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.ISimulationMovement.py similarity index 97% rename from product/ERP5/interfaces/simulation_movement.py rename to product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.ISimulationMovement.py index 0d8904d27cf6bf58559bb32a169a444075db7668..8e07a49050b96a28dfd84a3e070d4e9da78540f2 100644 --- a/product/ERP5/interfaces/simulation_movement.py +++ b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.ISimulationMovement.py @@ -27,14 +27,13 @@ # ############################################################################## """ -Products.ERP5.interfaces.simulation_movement +erp5.component.interface.ISimulationMovement """ from Products.ERP5.interfaces.property_recordable import IPropertyRecordable from Products.ERP5.interfaces.movement import IMovement -from Products.ERP5.interfaces.divergence_controller import IDivergenceController +from erp5.component.interface.IDivergenceController import IDivergenceController from Products.ERP5.interfaces.explainable import IExplainable -from zope.interface import Interface class ISimulationMovement(IMovement, IPropertyRecordable, IDivergenceController, IExplainable): """Simulation Movement interface specification diff --git a/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.ISimulationMovement.xml b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.ISimulationMovement.xml new file mode 100644 index 0000000000000000000000000000000000000000..17cc662b2112fb90e9c6fe5be762e228c48658d3 --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/InterfaceTemplateItem/portal_components/interface.erp5.ISimulationMovement.xml @@ -0,0 +1,110 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Interface Component" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>default_reference</string> </key> + <value> <string>ISimulationMovement</string> </value> + </item> + <item> + <key> <string>default_source_reference</string> </key> + <value> <string>Products.ERP5.interfaces.simulation_movement</string> </value> + </item> + <item> + <key> <string>description</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>interface.erp5.ISimulationMovement</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Interface Component</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>text_content_error_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>text_content_warning_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>version</string> </key> + <value> <string>erp5</string> </value> + </item> + <item> + <key> <string>workflow_history</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="PersistentMapping" module="Persistence.mapping"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>data</string> </key> + <value> + <dictionary> + <item> + <key> <string>component_validation_workflow</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_log</string> </key> + <value> + <list> + <dictionary> + <item> + <key> <string>action</string> </key> + <value> <string>validate</string> </value> + </item> + <item> + <key> <string>validation_state</string> </key> + <value> <string>validated</string> </value> + </item> + </dictionary> + </list> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/mixin/explainable.py b/product/ERP5/bootstrap/erp5_core/MixinTemplateItem/portal_components/mixin.erp5.ExplainableMixin.py similarity index 100% rename from product/ERP5/mixin/explainable.py rename to product/ERP5/bootstrap/erp5_core/MixinTemplateItem/portal_components/mixin.erp5.ExplainableMixin.py diff --git a/product/ERP5/bootstrap/erp5_core/MixinTemplateItem/portal_components/mixin.erp5.ExplainableMixin.xml b/product/ERP5/bootstrap/erp5_core/MixinTemplateItem/portal_components/mixin.erp5.ExplainableMixin.xml new file mode 100644 index 0000000000000000000000000000000000000000..758269a88369537b024c0947f0871ab83dabd6bb --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/MixinTemplateItem/portal_components/mixin.erp5.ExplainableMixin.xml @@ -0,0 +1,104 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Mixin Component" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>default_reference</string> </key> + <value> <string>ExplainableMixin</string> </value> + </item> + <item> + <key> <string>default_source_reference</string> </key> + <value> <string>Products.ERP5.mixin.explainable</string> </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>mixin.erp5.ExplainableMixin</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Mixin Component</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>text_content_error_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>text_content_warning_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>version</string> </key> + <value> <string>erp5</string> </value> + </item> + <item> + <key> <string>workflow_history</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="PersistentMapping" module="Persistence.mapping"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>data</string> </key> + <value> + <dictionary> + <item> + <key> <string>component_validation_workflow</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_log</string> </key> + <value> + <list> + <dictionary> + <item> + <key> <string>action</string> </key> + <value> <string>validate</string> </value> + </item> + <item> + <key> <string>validation_state</string> </key> + <value> <string>validated</string> </value> + </item> + </dictionary> + </list> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/mixin/movement_collection_updater.py b/product/ERP5/bootstrap/erp5_core/MixinTemplateItem/portal_components/mixin.erp5.MovementCollectionUpdaterMixin.py similarity index 96% rename from product/ERP5/mixin/movement_collection_updater.py rename to product/ERP5/bootstrap/erp5_core/MixinTemplateItem/portal_components/mixin.erp5.MovementCollectionUpdaterMixin.py index 47283b080489a15aa5d92e0557ce6af180c72d9a..83fe8d2a4b49ea8108978dd40821c020578e8156 100644 --- a/product/ERP5/mixin/movement_collection_updater.py +++ b/product/ERP5/bootstrap/erp5_core/MixinTemplateItem/portal_components/mixin.erp5.MovementCollectionUpdaterMixin.py @@ -29,10 +29,11 @@ import zope.interface from AccessControl import ClassSecurityInfo from Products.ERP5Type.Globals import InitializeClass -from Products.ERP5Type import Permissions, interfaces -from Products.ERP5.MovementCollectionDiff import ( +from Products.ERP5Type import Permissions +from erp5.component.module.MovementCollectionDiff import ( MovementCollectionDiff, _getPropertyAndCategoryList) -from Products.ERP5.mixin.rule import _compare +from erp5.component.mixin.RuleMixin import _compare +from erp5.component.interface.IMovementCollectionUpdater import IMovementCollectionUpdater class MovementCollectionUpdaterMixin: """Movement Collection Updater. @@ -49,7 +50,7 @@ class MovementCollectionUpdaterMixin: security.declareObjectProtected(Permissions.AccessContentsInformation) # Declarative interfaces - zope.interface.implements(interfaces.IMovementCollectionUpdater,) + zope.interface.implements(IMovementCollectionUpdater,) # Implementation of IMovementCollectionUpdater security.declareProtected(Permissions.AccessContentsInformation, diff --git a/product/ERP5/bootstrap/erp5_core/MixinTemplateItem/portal_components/mixin.erp5.MovementCollectionUpdaterMixin.xml b/product/ERP5/bootstrap/erp5_core/MixinTemplateItem/portal_components/mixin.erp5.MovementCollectionUpdaterMixin.xml new file mode 100644 index 0000000000000000000000000000000000000000..2aff2360d41c3f6f7a266048d3b121ea6777bf3d --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/MixinTemplateItem/portal_components/mixin.erp5.MovementCollectionUpdaterMixin.xml @@ -0,0 +1,110 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Mixin Component" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>default_reference</string> </key> + <value> <string>MovementCollectionUpdaterMixin</string> </value> + </item> + <item> + <key> <string>default_source_reference</string> </key> + <value> <string>Products.ERP5.mixin.movement_collection_updater</string> </value> + </item> + <item> + <key> <string>description</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>mixin.erp5.MovementCollectionUpdaterMixin</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Mixin Component</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>text_content_error_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>text_content_warning_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>version</string> </key> + <value> <string>erp5</string> </value> + </item> + <item> + <key> <string>workflow_history</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="PersistentMapping" module="Persistence.mapping"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>data</string> </key> + <value> + <dictionary> + <item> + <key> <string>component_validation_workflow</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_log</string> </key> + <value> + <list> + <dictionary> + <item> + <key> <string>action</string> </key> + <value> <string>validate</string> </value> + </item> + <item> + <key> <string>validation_state</string> </key> + <value> <string>validated</string> </value> + </item> + </dictionary> + </list> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/mixin/rule.py b/product/ERP5/bootstrap/erp5_core/MixinTemplateItem/portal_components/mixin.erp5.RuleMixin.py similarity index 90% rename from product/ERP5/mixin/rule.py rename to product/ERP5/bootstrap/erp5_core/MixinTemplateItem/portal_components/mixin.erp5.RuleMixin.py index 484cfab2050869c0f6080147bf58f03cf0f2ca5c..0ff0509fced87a21e214cb42b61f2aa8a3f71815 100644 --- a/product/ERP5/mixin/rule.py +++ b/product/ERP5/bootstrap/erp5_core/MixinTemplateItem/portal_components/mixin.erp5.RuleMixin.py @@ -29,9 +29,12 @@ import zope.interface from AccessControl import ClassSecurityInfo from Products.ERP5Type.Globals import InitializeClass -from Products.ERP5Type import Permissions, interfaces +from Products.ERP5Type import Permissions from Products.ERP5Type.Core.Predicate import Predicate -from Products.ERP5.ExpandPolicy import policy_dict +from erp5.component.module.ExpandPolicy import policy_dict +from erp5.component.interface.IRule import IRule +from erp5.component.interface.IDivergenceController import IDivergenceController +from erp5.component.interface.IMovementCollectionUpdater import IMovementCollectionUpdater def _compare(tester_list, prevision_movement, decision_movement): for tester in tester_list: @@ -49,9 +52,9 @@ class RuleMixin(Predicate): security.declareObjectProtected(Permissions.AccessContentsInformation) # Declarative interfaces - zope.interface.implements(interfaces.IRule, - interfaces.IDivergenceController, - interfaces.IMovementCollectionUpdater,) + zope.interface.implements(IRule, + IDivergenceController, + IMovementCollectionUpdater,) # Portal Type of created children movement_type = 'Simulation Movement' @@ -83,13 +86,13 @@ class RuleMixin(Predicate): # Rules have a workflow - make sure applicable rule system works # if you wish, add a test here on workflow state to prevent using # rules which are no longer applicable - def test(self, *args, **kw): - """ - If no test method is defined, return False, to prevent infinite loop - """ - if not self.getTestMethodId(): - return False - return super(RuleMixin, self).test(*args, **kw) + def test(self, *args, **kw): + """ + If no test method is defined, return False, to prevent infinite loop + """ + if not self.getTestMethodId(): + return False + return super(RuleMixin, self).test(*args, **kw) security.declareProtected(Permissions.ModifyPortalContent, 'expand') @@ -128,7 +131,7 @@ class RuleMixin(Predicate): # Implementation of IDivergenceController # XXX-JPS move to IDivergenceController only mixin for security.declareProtected( Permissions.AccessContentsInformation, 'isDivergent') - def isDivergent(self, movement, ignore_list=[]): + def isDivergent(self, movement, ignore_list=()): """ Returns true if the Simulation Movement is divergent comparing to the delivery value @@ -179,12 +182,16 @@ class RuleMixin(Predicate): quantity divergence testers """ if exclude_quantity: - return filter(lambda x:x.isDivergenceProvider() and \ - 'quantity' not in x.getTestedPropertyList(), self.objectValues( - portal_type=self.getPortalDivergenceTesterTypeList())) + return [ + x for x in self.objectValues( + portal_type=self.getPortalDivergenceTesterTypeList()) + if (x.isDivergenceProvider() and + 'quantity' not in x.getTestedPropertyList())] else: - return filter(lambda x:x.isDivergenceProvider(), self.objectValues( - portal_type=self.getPortalDivergenceTesterTypeList())) + return [ + x for x in self.objectValues( + portal_type=self.getPortalDivergenceTesterTypeList()) + if x.isDivergenceProvider()] def _getMatchingTesterList(self): """ @@ -192,8 +199,10 @@ class RuleMixin(Predicate): be used to match movements and build the diff (ie. not all divergence testers of the Rule) """ - return filter(lambda x:x.isMatchingProvider(), self.objectValues( - portal_type=self.getPortalDivergenceTesterTypeList())) + return [ + x for x in self.objectValues( + portal_type=self.getPortalDivergenceTesterTypeList()) + if x.isMatchingProvider()] def _getUpdatingTesterList(self, exclude_quantity=False): """ @@ -204,12 +213,16 @@ class RuleMixin(Predicate): quantity divergence testers """ if exclude_quantity: - return filter(lambda x:x.isUpdatingProvider() and \ - 'quantity' not in x.getTestedPropertyList(), self.objectValues( - portal_type=self.getPortalDivergenceTesterTypeList())) + return [ + x for x in self.objectValues( + portal_type=self.getPortalDivergenceTesterTypeList()) + if (x.isUpdatingProvider() and + 'quantity' not in x.getTestedPropertyList())] else: - return filter(lambda x:x.isUpdatingProvider(), self.objectValues( - portal_type=self.getPortalDivergenceTesterTypeList())) + return [ + x for x in self.objectValues( + portal_type=self.getPortalDivergenceTesterTypeList()) + if x.isUpdatingProvider()] def _getQuantityTesterList(self): """ diff --git a/product/ERP5/bootstrap/erp5_core/MixinTemplateItem/portal_components/mixin.erp5.RuleMixin.xml b/product/ERP5/bootstrap/erp5_core/MixinTemplateItem/portal_components/mixin.erp5.RuleMixin.xml new file mode 100644 index 0000000000000000000000000000000000000000..bcf0604fea2b2623b071a83a554083118e72b77e --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/MixinTemplateItem/portal_components/mixin.erp5.RuleMixin.xml @@ -0,0 +1,110 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Mixin Component" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>default_reference</string> </key> + <value> <string>RuleMixin</string> </value> + </item> + <item> + <key> <string>default_source_reference</string> </key> + <value> <string>Products.ERP5.mixin.rule</string> </value> + </item> + <item> + <key> <string>description</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>mixin.erp5.RuleMixin</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Mixin Component</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>text_content_error_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>text_content_warning_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>version</string> </key> + <value> <string>erp5</string> </value> + </item> + <item> + <key> <string>workflow_history</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="PersistentMapping" module="Persistence.mapping"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>data</string> </key> + <value> + <dictionary> + <item> + <key> <string>component_validation_workflow</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_log</string> </key> + <value> + <list> + <dictionary> + <item> + <key> <string>action</string> </key> + <value> <string>validate</string> </value> + </item> + <item> + <key> <string>validation_state</string> </key> + <value> <string>validated</string> </value> + </item> + </dictionary> + </list> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/ExpandPolicy.py b/product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.ExpandPolicy.py similarity index 98% rename from product/ERP5/ExpandPolicy.py rename to product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.ExpandPolicy.py index 81b0c8076d1c8d403726fc73235355c5909a77aa..6c631ce97cc4a10b29a12d7977d75c955f12a191 100644 --- a/product/ERP5/ExpandPolicy.py +++ b/product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.ExpandPolicy.py @@ -130,7 +130,7 @@ class VerticalTimeBound(_Policy): super(VerticalTimeBound, self).__init__(**kw) self.stop = transaction.get().start_time + VERTICAL_EXPAND_TIMEOUT - def test(self, context): + def test(self, context): # pylint: disable=method-hidden if time() < self.stop: return True self.deferAll() diff --git a/product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.ExpandPolicy.xml b/product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.ExpandPolicy.xml new file mode 100644 index 0000000000000000000000000000000000000000..b809340f01bdf4faeab0a2790afc0711eec6271a --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.ExpandPolicy.xml @@ -0,0 +1,110 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Module Component" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>default_reference</string> </key> + <value> <string>ExpandPolicy</string> </value> + </item> + <item> + <key> <string>default_source_reference</string> </key> + <value> <string>Products.ERP5.ExpandPolicy</string> </value> + </item> + <item> + <key> <string>description</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>module.erp5.ExpandPolicy</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Module Component</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>text_content_error_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>text_content_warning_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>version</string> </key> + <value> <string>erp5</string> </value> + </item> + <item> + <key> <string>workflow_history</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="PersistentMapping" module="Persistence.mapping"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>data</string> </key> + <value> + <dictionary> + <item> + <key> <string>component_validation_workflow</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_log</string> </key> + <value> + <list> + <dictionary> + <item> + <key> <string>action</string> </key> + <value> <string>validate</string> </value> + </item> + <item> + <key> <string>validation_state</string> </key> + <value> <string>validated</string> </value> + </item> + </dictionary> + </list> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/MovementCollectionDiff.py b/product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.MovementCollectionDiff.py similarity index 97% rename from product/ERP5/MovementCollectionDiff.py rename to product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.MovementCollectionDiff.py index 7bf1c91b3ccee80e2f7d7844e33fb08b53c2e1da..872989fdbf08db97266ba7cd9f4eaea69509d139 100644 --- a/product/ERP5/MovementCollectionDiff.py +++ b/product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.MovementCollectionDiff.py @@ -27,8 +27,8 @@ ############################################################################## import zope.interface -from Products.ERP5Type import interfaces from Products.ERP5Type.Accessor.TypeDefinition import list_types +from erp5.component.interface.IMovementCollectionDiff import IMovementCollectionDiff class MovementCollectionDiff(object): """ @@ -39,7 +39,7 @@ class MovementCollectionDiff(object): IMovementCollectionUpdater. """ # Declarative interfaces - zope.interface.implements(interfaces.IMovementCollectionDiff,) + zope.interface.implements(IMovementCollectionDiff,) def __init__(self): self._deletable_movement_list = [] diff --git a/product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.MovementCollectionDiff.xml b/product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.MovementCollectionDiff.xml new file mode 100644 index 0000000000000000000000000000000000000000..42dfc58fe4902f9d83e745ef246e70bc10198fc9 --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.MovementCollectionDiff.xml @@ -0,0 +1,104 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Module Component" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>default_reference</string> </key> + <value> <string>MovementCollectionDiff</string> </value> + </item> + <item> + <key> <string>default_source_reference</string> </key> + <value> <string>Products.ERP5.MovementCollectionDiff</string> </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>module.erp5.MovementCollectionDiff</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Module Component</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>text_content_error_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>text_content_warning_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>version</string> </key> + <value> <string>erp5</string> </value> + </item> + <item> + <key> <string>workflow_history</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="PersistentMapping" module="Persistence.mapping"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>data</string> </key> + <value> + <dictionary> + <item> + <key> <string>component_validation_workflow</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_log</string> </key> + <value> + <list> + <dictionary> + <item> + <key> <string>action</string> </key> + <value> <string>validate</string> </value> + </item> + <item> + <key> <string>validation_state</string> </key> + <value> <string>validated</string> </value> + </item> + </dictionary> + </list> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/MovementGroup.py b/product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.MovementGroup.py similarity index 85% rename from product/ERP5/MovementGroup.py rename to product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.MovementGroup.py index 7e46c5e6b2426e571c935d13e2b7d66d951e693b..fda5ae9e62631576aa78509686f22f3d52e7e655 100644 --- a/product/ERP5/MovementGroup.py +++ b/product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.MovementGroup.py @@ -41,7 +41,7 @@ class MovementGroupNode: # a separate method requests separating movements. def __init__(self, movement_group_list=None, movement_list=None, last_line_movement_group=None, - separate_method_name_list=[], movement_group=None, + separate_method_name_list=(), movement_group=None, merge_delivery=None): self._movement_list = [] self._group_list = [] @@ -332,7 +332,7 @@ class FakeMovement: return True return False - def _setDelivery(self, object): + def _setDelivery(self, object): # pylint: disable=redefined-builtin """ Set Delivery value for each movement """ @@ -507,86 +507,4 @@ class FakeMovement: repr_str = '<%s object at 0x%x for %r' % (self.__class__.__name__, id(self), self.getMovementList()) - return repr_str - -# The following classes are not ported to Document/XxxxMovementGroup.py yet. - -class RootMovementGroup(MovementGroupNode): - pass - -class SplitResourceMovementGroup(RootMovementGroup): - - def __init__(self, movement, **kw): - RootMovementGroup.__init__(self, movement=movement, **kw) - self.resource = movement.getResource() - - def test(self, movement): - return movement.getResource() == self.resource - -allow_class(SplitResourceMovementGroup) - -class OptionMovementGroup(RootMovementGroup): - - def __init__(self,movement,**kw): - RootMovementGroup.__init__(self, movement=movement, **kw) - option_base_category_list = movement.getPortalOptionBaseCategoryList() - self.option_category_list = movement.getVariationCategoryList( - base_category_list=option_base_category_list) - if self.option_category_list is None: - self.option_category_list = [] - self.option_category_list.sort() - # XXX This is very bad, but no choice today. - self.setGroupEdit(industrial_phase_list = self.option_category_list) - - def test(self,movement): - option_base_category_list = movement.getPortalOptionBaseCategoryList() - movement_option_category_list = movement.getVariationCategoryList( - base_category_list=option_base_category_list) - if movement_option_category_list is None: - movement_option_category_list = [] - movement_option_category_list.sort() - return movement_option_category_list == self.option_category_list - -allow_class(OptionMovementGroup) - -# XXX This should not be here -# I (seb) have commited this because movement groups are not -# yet configurable through the zope web interface -class IntIndexMovementGroup(RootMovementGroup): - - def getIntIndex(self,movement): - order_value = movement.getOrderValue() - int_index = 0 - if order_value is not None: - if "Line" in order_value.getPortalType(): - int_index = order_value.getIntIndex() - elif "Cell" in order_value.getPortalType(): - int_index = order_value.getParentValue().getIntIndex() - return int_index - - def __init__(self,movement,**kw): - RootMovementGroup.__init__(self, movement=movement, **kw) - int_index = self.getIntIndex(movement) - self.int_index = int_index - self.setGroupEdit( - int_index=int_index - ) - - def test(self,movement): - return self.getIntIndex(movement) == self.int_index - -allow_class(IntIndexMovementGroup) - -class ParentExplanationMovementGroup(RootMovementGroup): pass - -class ParentExplanationCausalityMovementGroup(ParentExplanationMovementGroup): - """ - Like ParentExplanationMovementGroup, and set the causality. - """ - def __init__(self, movement, **kw): - ParentExplanationMovementGroup.__init__(self, movement=movement, **kw) - self.updateGroupEdit( - causality_value = self.explanation_value - ) - -allow_class(ParentExplanationCausalityMovementGroup) + return repr_str \ No newline at end of file diff --git a/product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.MovementGroup.xml b/product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.MovementGroup.xml new file mode 100644 index 0000000000000000000000000000000000000000..4f232d16eacac950c94a487468b4333a7a3eb3c1 --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/ModuleComponentTemplateItem/portal_components/module.erp5.MovementGroup.xml @@ -0,0 +1,110 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Module Component" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>default_reference</string> </key> + <value> <string>MovementGroup</string> </value> + </item> + <item> + <key> <string>default_source_reference</string> </key> + <value> <string>Products.ERP5.MovementGroup</string> </value> + </item> + <item> + <key> <string>description</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>module.erp5.MovementGroup</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Module Component</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>text_content_error_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>text_content_warning_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>version</string> </key> + <value> <string>erp5</string> </value> + </item> + <item> + <key> <string>workflow_history</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="PersistentMapping" module="Persistence.mapping"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>data</string> </key> + <value> + <dictionary> + <item> + <key> <string>component_validation_workflow</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_log</string> </key> + <value> + <list> + <dictionary> + <item> + <key> <string>action</string> </key> + <value> <string>validate</string> </value> + </item> + <item> + <key> <string>validation_state</string> </key> + <value> <string>validated</string> </value> + </item> + </dictionary> + </list> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/bootstrap/erp5_core/ToolComponentTemplateItem/portal_components/tool.erp5.SimulationTool.py b/product/ERP5/bootstrap/erp5_core/ToolComponentTemplateItem/portal_components/tool.erp5.SimulationTool.py new file mode 100644 index 0000000000000000000000000000000000000000..5de52acaa590d75aa189ca15fabcec959da45d12 --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/ToolComponentTemplateItem/portal_components/tool.erp5.SimulationTool.py @@ -0,0 +1,2998 @@ +############################################################################## +# +# Copyright (c) 2002, 2005 Nexedi SARL and Contributors. All Rights Reserved. +# Jean-Paul Smets-Solanes <jp@nexedi.com> +# Romain Courteaud <romain@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 Products.CMFCore.utils import getToolByName + +from AccessControl import ClassSecurityInfo +from Products.ERP5Type.Globals import InitializeClass +from Products.ERP5Type import Permissions +from Products.ERP5Type.Tool.BaseTool import BaseTool +from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod + +from zLOG import LOG, PROBLEM, WARNING, INFO + +from Products.ERP5.Capacity.GLPK import solve +from numpy import zeros, resize +from DateTime import DateTime + +from Products.PythonScripts.Utility import allow_class + +from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery, SimpleQuery + +from Shared.DC.ZRDB.Results import Results +from Products.ERP5Type.Utils import mergeZRDBResults +from App.Extensions import getBrain +from MySQLdb import ProgrammingError +from MySQLdb.constants.ER import NO_SUCH_TABLE + +from hashlib import md5 +from warnings import warn +from cPickle import loads, dumps +from copy import deepcopy + +MYSQL_MIN_DATETIME_RESOLUTION = 1/86400. + +class StockOptimisationError(Exception): + pass + +class SimulationTool(BaseTool): + """ + The SimulationTool implements the ERP5 + simulation algorithmics. + + + Examples of applications: + + - + + - + ERP5 main purpose: + + - + + - + + """ + id = 'portal_simulation' + meta_type = 'ERP5 Simulation Tool' + portal_type = 'Simulation Tool' + allowed_types = ( 'ERP5 Applied Rule', ) + + # Declarative Security + security = ClassSecurityInfo() + + # + # ZMI methods + # + + def filtered_meta_types(self, user=None): + # Filters the list of available meta types. + meta_types = [] + for meta_type in SimulationTool.inheritedAttribute('filtered_meta_types')(self): + if meta_type['name'] in self.allowed_types: + meta_types.append(meta_type) + return meta_types + + def tpValues(self) : + """ show the content in the left pane of the ZMI """ + return self.objectValues() + + security.declarePrivate('manage_afterAdd') + def manage_afterAdd(self, item, container) : + """Init permissions right after creation. + + Permissions in simulation tool are simple: + o Each member can access and create some content. + o Only manager can view, because simulation can be seen as + sensitive information. + """ + item.manage_permission(Permissions.AddPortalContent, + ['Member', 'Author', 'Manager']) + item.manage_permission(Permissions.AccessContentsInformation, + ['Member', 'Auditor', 'Manager']) + item.manage_permission(Permissions.View, + ['Manager',]) + BaseTool.inheritedAttribute('manage_afterAdd')(self, item, container) + + security.declareProtected(Permissions.AccessContentsInformation, + 'solveDelivery') + def solveDelivery(self, delivery, delivery_solver_name, target_solver_name, + additional_parameters=None, **kw): + """ + XXX obsoleted API + + Solves a delivery by calling first DeliverySolver, then TargetSolver + """ + return self._solveMovementOrDelivery(delivery, delivery_solver_name, + target_solver_name, delivery=1, + additional_parameters=additional_parameters, **kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'solveMovement') + def solveMovement(self, movement, delivery_solver_name, target_solver_name, + additional_parameters=None, **kw): + """ + XXX obsoleted API + + Solves a movement by calling first DeliverySolver, then TargetSolver + """ + return self._solveMovementOrDelivery(movement, delivery_solver_name, + target_solver_name, movement=1, + additional_parameters=additional_parameters, **kw) + + def _solveMovementOrDelivery(self, document, delivery_solver_name, + target_solver_name, movement=0, delivery=0, + additional_parameters=None,**kw): + """ + Solves a document by calling first DeliverySolver, then TargetSolver + """ + if movement == delivery: + raise ValueError('Parameters movement and delivery have to be' + ' different') + + from Products.ERP5 import DeliverySolver + from Products.ERP5 import TargetSolver + solve_result = [] + for solver_name, solver_module in ((delivery_solver_name, DeliverySolver), + (target_solver_name, TargetSolver)): + result = None + if solver_name is not None: + solver_file_path = "%s.%s" % (solver_module.__name__, + solver_name) + __import__(solver_file_path) + solver_file = getattr(solver_module, solver_name) + solver_class = getattr(solver_file, solver_name) + solver = solver_class(additional_parameters=additional_parameters, + **kw) + + if movement: + result = solver.solveMovement(document) + if delivery: + result = solver.solveDelivery(document) + solve_result.append(result) + return solve_result + + ####################################################### + # Stock Management + + def _generatePropertyUidList(self, prop, as_text=0): + """ + converts relative_url or text (single element or list or dict) + to an object usable by buildSQLQuery + + as_text == 0: tries to lookup an uid from the relative_url + as_text == 1: directly passes the argument as text + """ + if prop is None : + return [] + category_tool = getToolByName(self, 'portal_categories') + property_uid_list = [] + if isinstance(prop, str): + if not as_text: + prop_value = category_tool.getCategoryValue(prop) + if prop_value is None: + raise ValueError, 'Category %s does not exists' % prop + property_uid_list.append(prop_value.getUid()) + else: + property_uid_list.append(prop) + elif isinstance(prop, (list, tuple)): + for property_item in prop : + if not as_text: + prop_value = category_tool.getCategoryValue(property_item) + if prop_value is None: + raise ValueError, 'Category %s does not exists' % property_item + property_uid_list.append(prop_value.getUid()) + else: + property_uid_list.append(property_item) + elif isinstance(prop, dict): + tmp_uid_list = [] + if isinstance(prop['query'], str): + prop['query'] = [prop['query']] + for property_item in prop['query'] : + if not as_text: + prop_value = category_tool.getCategoryValue(property_item) + if prop_value is None: + raise ValueError, 'Category %s does not exists' % property_item + tmp_uid_list.append(prop_value.getUid()) + else: + tmp_uid_list.append(property_item) + if tmp_uid_list: + property_uid_list = {} + property_uid_list['operator'] = prop['operator'] + property_uid_list['query'] = tmp_uid_list + return property_uid_list + + def _getSimulationStateQuery(self, **kw): + simulation_state_dict = self._getSimulationStateDict(**kw) + return self._buildSimulationStateQuery(simulation_state_dict=simulation_state_dict) + + def _buildSimulationStateQuery(self, simulation_state_dict, table='stock'): + simulation_state = simulation_state_dict.get('simulation_state') + if simulation_state is not None: + return SimpleQuery(**{table + '.simulation_state': simulation_state}) + input_simulation_state = simulation_state_dict.get('input_simulation_state') + if input_simulation_state is not None: + simulation_query = ComplexQuery( + self._getIncreaseQuery(table, 'quantity', True), + SimpleQuery(**{table + '.simulation_state': input_simulation_state}), + logical_operator='AND', + ) + output_simulation_state = simulation_state_dict.get('output_simulation_state') + if output_simulation_state is not None: + simulation_query = ComplexQuery( + simulation_query, + ComplexQuery( + self._getIncreaseQuery(table, 'quantity', False), + SimpleQuery(**{table + '.simulation_state': output_simulation_state}), + logical_operator='AND', + ), + logical_operator='OR' + ) + return simulation_query + + def _getSimulationStateDict(self, simulation_state=None, omit_transit=0, + input_simulation_state=None, + output_simulation_state=None, + transit_simulation_state=None, + strict_simulation_state=None): + """ + This method is used in order to give what should be + the input_simulation_state or output_simulation_state + depending on many parameters + """ + string_or_list = (str, list, tuple) + # Simulation States + # If strict_simulation_state is set, we directly put it into the dictionary + simulation_dict = {} + if strict_simulation_state: + if isinstance(simulation_state, string_or_list)\ + and simulation_state: + simulation_dict['simulation_state'] = simulation_state + else: + # first, we evaluate simulation_state + sql_kw = {} + if simulation_state and isinstance(simulation_state, string_or_list): + if isinstance(simulation_state, str): + sql_kw['input_simulation_state'] = [simulation_state] + sql_kw['output_simulation_state'] = [simulation_state] + else: + sql_kw['input_simulation_state'] = simulation_state + sql_kw['output_simulation_state'] = simulation_state + # then, if omit_transit == 1, we evaluate (simulation_state - + # transit_simulation_state) for input_simulation_state + if omit_transit: + if isinstance(simulation_state, string_or_list)\ + and simulation_state: + if isinstance(transit_simulation_state, string_or_list)\ + and transit_simulation_state: + # when we know both are usable, we try to calculate + # (simulation_state - transit_simulation_state) + if isinstance(simulation_state, str): + simulation_state = [simulation_state] + if isinstance(transit_simulation_state, str) : + transit_simulation_state = [transit_simulation_state] + delivered_simulation_state_list = [] + for state in simulation_state : + if state not in transit_simulation_state : + delivered_simulation_state_list.append(state) + sql_kw['input_simulation_state'] = delivered_simulation_state_list + + # alternatively, the user can directly define input_simulation_state + # and output_simulation_state + if input_simulation_state and isinstance(input_simulation_state, + string_or_list): + if isinstance(input_simulation_state, str): + input_simulation_state = [input_simulation_state] + sql_kw['input_simulation_state'] = input_simulation_state + if output_simulation_state and isinstance(output_simulation_state, + string_or_list): + if isinstance(output_simulation_state, str): + output_simulation_state = [output_simulation_state] + sql_kw['output_simulation_state'] = output_simulation_state + # XXX In this case, we must not set sql_kw[input_simumlation_state] before + input_simulation_state = None + output_simulation_state = None + if sql_kw.has_key('input_simulation_state'): + input_simulation_state = sql_kw.get('input_simulation_state') + if sql_kw.has_key('output_simulation_state'): + output_simulation_state = sql_kw.get('output_simulation_state') + if input_simulation_state is not None \ + or output_simulation_state is not None: + sql_kw.pop('input_simulation_state',None) + sql_kw.pop('output_simulation_state',None) + if input_simulation_state is not None: + if output_simulation_state is not None: + if input_simulation_state == output_simulation_state: + simulation_dict['simulation_state'] = input_simulation_state + else: + simulation_dict['input_simulation_state'] = input_simulation_state + simulation_dict['output_simulation_state'] = output_simulation_state + else: + simulation_dict['input_simulation_state'] = input_simulation_state + elif output_simulation_state is not None: + simulation_dict['simulation_state'] = output_simulation_state + return simulation_dict + + def _getIncreaseQuery(self, table, column, increase, sql_catalog_id=None): + """ + Returns a Query filtering rows depending on whether they represent an + increase or a decrease. + table (string) + Name of table to use as stock table. + column (string) + Name of interesting column. Supported values are: + - total_price for asset price increase/decrease + - quantity for quantity increase (aka input)/decrease (aka output) + increase (bool) + False: decreasing rows are kept + True: increasing rows are kept + sql_catalog_id (string or None) + Idenfitier of an SQLCatalog object relevant to table, or None for + default one. + """ + if column == 'total_price': + dedicated_column = 'is_asset_increase' + elif column == 'quantity': + dedicated_column = 'is_input' + else: + raise ValueError('Unknown column %r' % (column, )) + if self.getPortalObject().portal_catalog.hasColumn( + dedicated_column, + sql_catalog_id, + ): + return SimpleQuery(**{dedicated_column: increase}) + # Dedicated columns are not present, compute on the fly. + return ComplexQuery( + ComplexQuery( + SimpleQuery(comparison_operator='<', **{table + '.' + column: 0}), + SimpleQuery(**{table + '.is_cancellation': increase}), + logical_operator='AND', + ), + ComplexQuery( + SimpleQuery(comparison_operator='>=', **{table + '.' + column: 0}), + SimpleQuery(**{table + '.is_cancellation': not increase}), + logical_operator='AND', + ), + logical_operator='OR', + ) + + def _generateSQLKeywordDict(self, table='stock', **kw): + sql_kw, new_kw = self._generateKeywordDict(**kw) + return self._generateSQLKeywordDictFromKeywordDict(table=table, + sql_kw=sql_kw, new_kw=new_kw) + + def _generateSQLKeywordDictFromKeywordDict(self, table='stock', sql_kw=None, + new_kw=None): + ctool = getToolByName(self, 'portal_catalog') + if sql_kw is None: + sql_kw = {} + else: + sql_kw = sql_kw.copy() + if new_kw is None: + new_kw = {} + else: + new_kw = new_kw.copy() + + # Group-by expression (eg. group_by=['node_uid']) + group_by = new_kw.pop('group_by_list', []) + + # group by from stock table (eg. group_by_node=True) + # prepend table name to avoid ambiguities. + column_group_by = new_kw.pop('column_group_by', []) + if column_group_by: + group_by.extend(['%s.%s' % (table, x) for x in column_group_by]) + + # group by from related keys columns (eg. group_by_node_category=True) + related_key_group_by = new_kw.pop('related_key_group_by', []) + if related_key_group_by: + group_by.extend(['%s_%s' % (table, x) for x in related_key_group_by]) + + # group by involving a related key (eg. group_by=['product_line_uid']) + related_key_dict_passthrough_group_by = new_kw.get( + 'related_key_dict_passthrough', {}).pop('group_by_list', []) + if isinstance(related_key_dict_passthrough_group_by, basestring): + related_key_dict_passthrough_group_by = ( + related_key_dict_passthrough_group_by,) + group_by.extend(related_key_dict_passthrough_group_by) + + if group_by: + new_kw['group_by_list'] = group_by + + # select expression + select_dict = new_kw.setdefault('select_dict', {}) + related_key_select_expression_list = new_kw.pop( + 'related_key_select_expression_list', []) + for related_key_select in related_key_select_expression_list: + select_dict[related_key_select] = '%s_%s' % (table, + related_key_select) + + # Column values + column_value_dict = new_kw.pop('column_value_dict', {}) + for key, value in column_value_dict.iteritems(): + new_kw['%s.%s' % (table, key)] = value + # Related keys + # First, the passthrough (acts as default values) + for key, value in new_kw.pop('related_key_dict_passthrough', {})\ + .iteritems(): + new_kw[key] = value + # Second, calculated values + for key, value in new_kw.pop('related_key_dict', {}).iteritems(): + new_kw['%s_%s' % (table, key)] = value + # Simulation states matched with input and output omission + def getSimulationQuery(simulation_dict, omit_dict): + simulation_query = self._buildSimulationStateQuery( + simulation_state_dict=simulation_dict, + table=table, + ) + query_list = [ + self._getIncreaseQuery(table, column, value) + for key, column, value in ( + ('input', 'quantity', False), + ('output', 'quantity', True), + ('asset_increase', 'total_price', False), + ('asset_decrease', 'total_price', True), + ) + if omit_dict.get(key) + ] + if query_list: + if simulation_query is not None: + query_list.append(simulation_query) + return ComplexQuery( + query_list, + logical_operator='AND', + ) + return simulation_query + simulation_query = getSimulationQuery( + new_kw.pop('simulation_dict', {}), + new_kw.pop('omit_dict', {}), + ) + reserved_kw = new_kw.pop('reserved_kw', None) + if reserved_kw is not None: + reserved_query = getSimulationQuery( + reserved_kw.pop('simulation_dict', {}), + reserved_kw.pop('omit_dict', {}), + ) + if simulation_query is None: + simulation_query = reserved_query + elif reserved_query is not None: + simulation_query = ComplexQuery( + simulation_query, + reserved_query, + logical_operator='OR', + ) + if simulation_query is not None: + new_kw['query'] = simulation_query + + # Sort on + if 'sort_on' in new_kw: + table_column_list = ctool.getSQLCatalog().getTableColumnList(table) + sort_on = new_kw['sort_on'] + new_sort_on = [] + for column_id, sort_direction in sort_on: + if column_id in table_column_list: + column_id = '%s.%s' % (table, column_id) + new_sort_on.append((column_id, sort_direction)) + new_kw['sort_on'] = tuple(new_sort_on) + + # Remove some internal parameters that does not have any meaning for + # catalog + new_kw.pop('ignore_group_by', None) + + catalog_sql_kw = ctool.buildSQLQuery(**new_kw) + from_table_dict = dict(sql_kw.pop('from_table_list', [])) + for alias, table in catalog_sql_kw.pop('from_table_list', None) or []: + assert from_table_dict.get(alias) in (None, table), ( + alias, + table, + from_table_dict[alias], + ) + from_table_dict[alias] = table + sql_kw.update(catalog_sql_kw) + sql_kw['from_table_list'] = from_table_dict.items() + return sql_kw + + def _generateKeywordDict(self, + # dates + from_date=None, to_date=None, at_date=None, + omit_mirror_date=1, + # instances + resource=None, node=None, payment=None, + section=None, mirror_section=None, item=None, + function=None, project=None, funding=None, payment_request=None, + transformed_resource=None, ledger=None, + # used for tracking + input=0, # pylint: disable=redefined-builtin + output=0, + # categories + resource_category=None, node_category=None, payment_category=None, + section_category=None, mirror_section_category=None, + function_category=None, project_category=None, funding_category=None, + ledger_category=None, payment_request_category=None, + # categories with strict membership + resource_category_strict_membership=None, + node_category_strict_membership=None, + payment_category_strict_membership=None, + section_category_strict_membership=None, + mirror_section_category_strict_membership=None, + function_category_strict_membership=None, + project_category_strict_membership=None, + funding_category_strict_membership=None, + ledger_category_strict_membership=None, + payment_request_category_strict_membership=None, + # simulation_state + strict_simulation_state=0, + simulation_state=None, transit_simulation_state = None, omit_transit=0, + input_simulation_state=None, output_simulation_state=None, + reserved_kw=None, + # variations + variation_text=None, sub_variation_text=None, + variation_category=None, + transformed_variation_text=None, + # uids + resource_uid=None, node_uid=None, section_uid=None, payment_uid=None, + mirror_node_uid=None, mirror_section_uid=None, function_uid=None, + project_uid=None, funding_uid=None, ledger_uid=None, + payment_request_uid=None, + # omit input and output + omit_input=0, + omit_output=0, + omit_asset_increase=0, + omit_asset_decrease=0, + # group by + group_by_node=0, + group_by_node_category=0, + group_by_node_category_strict_membership=0, + group_by_mirror_node=0, + group_by_mirror_node_category=0, + group_by_mirror_node_category_strict_membership=0, + group_by_section=0, + group_by_section_category=0, + group_by_section_category_strict_membership=0, + group_by_mirror_section=0, + group_by_mirror_section_category=0, + group_by_mirror_section_category_strict_membership=0, + group_by_payment=0, + group_by_payment_category=0, + group_by_payment_category_strict_membership=0, + group_by_sub_variation=0, + group_by_variation=0, + group_by_movement=0, + group_by_resource=0, + group_by_project=0, + group_by_project_category=0, + group_by_project_category_strict_membership=0, + group_by_funding=0, + group_by_funding_category=0, + group_by_funding_category_strict_membership=0, + group_by_ledger=0, + group_by_ledger_category=0, + group_by_ledger_category_strict_membership=0, + group_by_payment_request=0, + group_by_payment_request_category=0, + group_by_payment_request_category_strict_membership=0, + group_by_function=0, + group_by_function_category=0, + group_by_function_category_strict_membership=0, + group_by_date=0, + # sort_on + sort_on=None, + group_by=None, + # selection + selection_domain=None, + selection_report=None, + # keywords for related keys + **kw): + """ + Generates keywords and calls buildSQLQuery + + - omit_mirror_date: normally, date's parameters are only based on date + column. If 0, it also used the mirror_date column. + """ + new_kw = {} + sql_kw = { + 'from_table_list': [], + # Set of catalog aliases that must be joined in the ZSQLMethod ('foo' + # meaning something along the lines of 'foo.uid = stock.foo_uid') + 'selection_domain_catalog_alias_set': [], + # input and output are used by getTrackingList + 'input': input, + 'output': output, + # BBB + 'selection_domain': None, + 'selection_report': None, + } + + if selection_domain is None: + sql_kw['selection_domain_from_expression'] = None + sql_kw['selection_domain_where_expression'] = None + else: + # Pre-render selection_domain, as it is easier done here than in DTML. + if isinstance(selection_domain, dict): + selection_domain_dict = selection_domain + else: + selection_domain_dict = selection_domain.asDomainDict() + if 'ledger' in selection_domain_dict: + # XXX: what if both 'node' and 'ledger' are present ? + # Finer configuration may be needed here. + query_table_alias = 'ledger' + else: + query_table_alias = 'node' + selection_domain_sql_dict = self.getPortalObject().portal_catalog.buildSQLQuery( + selection_domain=selection_domain, + query_table_alias=query_table_alias, + ) + sql_kw['selection_domain_from_expression'] = selection_domain_sql_dict['from_expression'] + sql_kw['from_table_list'].extend(selection_domain_sql_dict['from_table_list']) + sql_kw['selection_domain_where_expression'] = selection_domain_sql_dict['where_expression'] + sql_kw['selection_domain_catalog_alias_set'].append(query_table_alias) + if selection_report is not None: + new_kw['selection_report'] = selection_report + + # Add sort_on parameter if defined + if sort_on is not None: + new_kw['sort_on'] = sort_on + + class DictMixIn(dict): + def set(dictionary, key, value): # pylint: disable=no-self-argument + result = bool(value) + if result: + dictionary[key] = value + return result + + def setUIDList(dictionary, key, value, as_text=0): # pylint: disable=no-self-argument + uid_list = self._generatePropertyUidList(value, as_text=as_text) + return dictionary.set(key, uid_list) + + column_value_dict = DictMixIn() + + if omit_mirror_date: + date_dict = {} + if from_date : + date_dict.setdefault('query', []).append(from_date) + date_dict['range'] = 'min' + if to_date : + date_dict.setdefault('query', []).append(to_date) + date_dict['range'] = 'minmax' + elif at_date : + date_dict.setdefault('query', []).append(at_date) + date_dict['range'] = 'minngt' + elif to_date : + date_dict.setdefault('query', []).append(to_date) + date_dict['range'] = 'max' + elif at_date : + date_dict.setdefault('query', []).append(at_date) + date_dict['range'] = 'ngt' + if date_dict: + column_value_dict['date'] = date_dict + else: + column_value_dict['date'] = {'query': [to_date], 'range': 'ngt'} + column_value_dict['mirror_date'] = {'query': [from_date], 'range': 'nlt'} + + column_value_dict.set('resource_uid', resource_uid) + column_value_dict.set('payment_uid', payment_uid) + column_value_dict.set('project_uid', project_uid) + column_value_dict.set('funding_uid', funding_uid) + column_value_dict.set('ledger_uid', ledger_uid) + column_value_dict.set('payment_request_uid', payment_request_uid) + column_value_dict.set('function_uid', function_uid) + column_value_dict.set('section_uid', section_uid) + column_value_dict.set('node_uid', node_uid) + column_value_dict.set('mirror_node_uid', mirror_node_uid) + column_value_dict.set('mirror_section_uid', mirror_section_uid) + column_value_dict.setUIDList('resource_uid', resource) + column_value_dict.setUIDList('aggregate_uid', item) + column_value_dict.setUIDList('node_uid', node) + column_value_dict.setUIDList('payment_uid', payment) + column_value_dict.setUIDList('project_uid', project) + column_value_dict.setUIDList('funding_uid', funding) + column_value_dict.setUIDList('ledger_uid', ledger) + column_value_dict.setUIDList('payment_request_uid', payment_request) + column_value_dict.setUIDList('function_uid', function) + + sql_kw['transformed_uid'] = self._generatePropertyUidList(transformed_resource) + + column_value_dict.setUIDList('section_uid', section) + column_value_dict.setUIDList('mirror_section_uid', mirror_section) + + # Handle variation_category as variation_text + if variation_category: + if variation_text: + raise ValueError( + "Passing both variation_category and variation_text is not supported") + warn("variation_category is deprecated, please use variation_text instead", + DeprecationWarning) + if isinstance(variation_category, basestring): + variation_category = (variation_category,) + # variation text is a \n separated list of variation categories, but without + # trailing nor leading \n + variation_text = [ + "{}\n%".format(x) for x in variation_category] + [ + "%\n{}\n%".format(x) for x in variation_category] + [ + "%\n{}".format(x) for x in variation_category] + [ + "{}".format(x) for x in variation_category] + + column_value_dict.setUIDList('variation_text', variation_text, + as_text=1) + column_value_dict.setUIDList('sub_variation_text', sub_variation_text, + as_text=1) + new_kw['column_value_dict'] = column_value_dict.copy() + + related_key_dict = DictMixIn() + # category membership + related_key_dict.setUIDList('resource_category_uid', resource_category) + related_key_dict.setUIDList('node_category_uid', node_category) + related_key_dict.setUIDList('project_category_uid', project_category) + related_key_dict.setUIDList('funding_category_uid', funding_category) + related_key_dict.setUIDList('ledger_category_uid', ledger_category) + related_key_dict.setUIDList('payment_request_category_uid', payment_request_category) + related_key_dict.setUIDList('function_category_uid', function_category) + related_key_dict.setUIDList('payment_category_uid', payment_category) + related_key_dict.setUIDList('section_category_uid', section_category) + related_key_dict.setUIDList('mirror_section_category_uid', + mirror_section_category) + # category strict membership + related_key_dict.setUIDList('resource_category_strict_membership_uid', + resource_category_strict_membership) + related_key_dict.setUIDList('node_category_strict_membership_uid', + node_category_strict_membership) + related_key_dict.setUIDList('project_category_strict_membership_uid', + project_category_strict_membership) + related_key_dict.setUIDList('funding_category_strict_membership_uid', + funding_category_strict_membership) + related_key_dict.setUIDList('ledger_category_strict_membership_uid', + ledger_category_strict_membership) + related_key_dict.setUIDList('payment_request_category_strict_membership_uid', + payment_request_category_strict_membership) + related_key_dict.setUIDList('function_category_strict_membership_uid', + function_category_strict_membership) + related_key_dict.setUIDList('payment_category_strict_membership_uid', + payment_category_strict_membership) + related_key_dict.setUIDList('section_category_strict_membership_uid', + section_category_strict_membership) + related_key_dict.setUIDList( + 'mirror_section_category_strict_membership_uid', + mirror_section_category_strict_membership) + + new_kw['related_key_dict'] = related_key_dict.copy() + new_kw['related_key_dict_passthrough'] = kw + # Check we do not get a known group_by + related_group_by = [] + if group_by: + if isinstance(group_by, basestring): + group_by = (group_by,) + for value in group_by: + if value == "node_uid": + group_by_node = 1 + elif value == 'mirror_node_uid': + group_by_mirror_node = 1 + elif value == 'section_uid': + group_by_section = 1 + elif value == 'mirror_section_uid': + group_by_mirror_section = 1 + elif value == 'payment_uid': + group_by_payment = 1 + elif value == 'sub_variation_text': + group_by_sub_variation = 1 + elif value == 'variation_text': + group_by_variation = 1 + elif value == 'uid': + group_by_movement = 1 + elif value == 'resource_uid': + group_by_resource = 1 + elif value == 'project_uid': + group_by_project = 1 + elif value == 'funding_uid': + group_by_funding = 1 + elif value == 'ledger_uid': + group_by_ledger = 1 + elif value == 'payment_request_uid': + group_by_payment_request = 1 + elif value == "function_uid": + group_by_function = 1 + elif value == 'date': + group_by_date = 1 + else: + related_group_by.append(value) + if related_group_by: + new_kw['related_key_dict_passthrough']['group_by_list'] = related_group_by + + new_kw['simulation_dict'] = self._getSimulationStateDict( + simulation_state=simulation_state, + omit_transit=omit_transit, + input_simulation_state=input_simulation_state, + output_simulation_state=output_simulation_state, + transit_simulation_state=transit_simulation_state, + strict_simulation_state=strict_simulation_state, + ) + new_kw['omit_dict'] = { + 'input': omit_input, + 'output': omit_output, + 'asset_increase': omit_asset_increase, + 'asset_decrease': omit_asset_decrease, + } + if reserved_kw is not None: + if not isinstance(reserved_kw, dict): + # Not a dict when taken from URL, so, cast is needed + # to make pop method available + reserved_kw = dict(reserved_kw) + new_kw['reserved_kw'] = { + 'omit_dict': { + 'input': reserved_kw.pop('omit_input', False), + 'output': reserved_kw.pop('omit_output', False), + }, + 'simulation_dict': self._getSimulationStateDict(**reserved_kw), + } + + # build the group by expression + # if we group by a criterion, we also add this criterion to the select + # expression, unless it is already selected in Resource_zGetInventoryList + # the caller can also pass select_dict or select_list. select_expression, + # which is deprecated in ZSQLCatalog is not supported here. + select_dict = kw.get('select_dict', {}) + select_dict.update(dict.fromkeys(list(kw.pop('select_list', [])) + related_group_by)) + new_kw['select_dict'] = select_dict + related_key_select_expression_list = [] + + column_group_by_expression_list = [] + related_key_group_by_expression_list = [] + if group_by_node: + column_group_by_expression_list.append('node_uid') + if group_by_mirror_node: + column_group_by_expression_list.append('mirror_node_uid') + if group_by_section: + column_group_by_expression_list.append('section_uid') + if group_by_mirror_section: + column_group_by_expression_list.append('mirror_section_uid') + if group_by_payment: + column_group_by_expression_list.append('payment_uid') + if group_by_sub_variation: + column_group_by_expression_list.append('sub_variation_text') + if group_by_variation: + column_group_by_expression_list.append('variation_text') + if group_by_movement: + column_group_by_expression_list.append('uid') + if group_by_resource: + column_group_by_expression_list.append('resource_uid') + if group_by_project: + column_group_by_expression_list.append('project_uid') + if group_by_funding: + column_group_by_expression_list.append('funding_uid') + if group_by_ledger: + column_group_by_expression_list.append('ledger_uid') + if group_by_payment_request: + column_group_by_expression_list.append('payment_request_uid') + if group_by_function: + column_group_by_expression_list.append('function_uid') + if group_by_date: + column_group_by_expression_list.append('date') + + if column_group_by_expression_list: + new_kw['column_group_by'] = column_group_by_expression_list + + if group_by_section_category: + related_key_group_by_expression_list.append('section_category_uid') + related_key_select_expression_list.append('section_category_uid') + if group_by_section_category_strict_membership: + related_key_group_by_expression_list.append( + 'section_category_strict_membership_uid') + related_key_select_expression_list.append( + 'section_category_strict_membership_uid') + if group_by_mirror_section_category: + related_key_group_by_expression_list.append('mirror_section_category_uid') + related_key_select_expression_list.append('mirror_section_category_uid') + if group_by_mirror_section_category_strict_membership: + related_key_group_by_expression_list.append( + 'mirror_section_category_strict_membership_uid') + related_key_select_expression_list.append( + 'mirror_section_category_strict_membership_uid') + if group_by_node_category: + related_key_group_by_expression_list.append('node_category_uid') + related_key_select_expression_list.append('node_category_uid') + if group_by_node_category_strict_membership: + related_key_group_by_expression_list.append( + 'node_category_strict_membership_uid') + related_key_select_expression_list.append( + 'node_category_strict_membership_uid') + if group_by_mirror_node_category: + related_key_group_by_expression_list.append('mirror_node_category_uid') + if group_by_mirror_node_category_strict_membership: + related_key_group_by_expression_list.append( + 'mirror_node_category_strict_membership_uid') + related_key_select_expression_list.append( + 'mirror_node_category_strict_membership_uid') + if group_by_payment_category: + related_key_group_by_expression_list.append('payment_category_uid') + related_key_select_expression_list.append('payment_category_uid') + if group_by_payment_category_strict_membership: + related_key_group_by_expression_list.append( + 'payment_category_strict_membership_uid') + related_key_select_expression_list.append( + 'payment_category_strict_membership_uid') + if group_by_function_category: + related_key_group_by_expression_list.append('function_category_uid') + related_key_select_expression_list.append('function_category_uid') + if group_by_function_category_strict_membership: + related_key_group_by_expression_list.append( + 'function_category_strict_membership_uid') + related_key_select_expression_list.append( + 'function_category_strict_membership_uid') + if group_by_project_category: + related_key_group_by_expression_list.append('project_category_uid') + related_key_select_expression_list.append('project_category_uid') + if group_by_project_category_strict_membership: + related_key_group_by_expression_list.append( + 'project_category_strict_membership_uid') + related_key_select_expression_list.append( + 'project_category_strict_membership_uid') + if group_by_funding_category: + related_key_group_by_expression_list.append('funding_category_uid') + related_key_select_expression_list.append('funding_category_uid') + if group_by_funding_category_strict_membership: + related_key_group_by_expression_list.append( + 'funding_category_strict_membership_uid') + related_key_select_expression_list.append( + 'funding_category_strict_membership_uid') + if group_by_ledger_category: + related_key_group_by_expression_list.append('ledger_category_uid') + related_key_select_expression_list.append('ledger_category_uid') + if group_by_ledger_category_strict_membership: + related_key_group_by_expression_list.append( + 'ledger_category_strict_membership_uid') + related_key_select_expression_list.append( + 'ledger_category_strict_membership_uid') + if group_by_payment_category: + related_key_group_by_expression_list.append('payment_request_category_uid') + related_key_select_expression_list.append('payment_request_category_uid') + if group_by_payment_request_category_strict_membership: + related_key_group_by_expression_list.append( + 'payment_request_category_strict_membership_uid') + related_key_select_expression_list.append( + 'payment_request_category_strict_membership_uid') + + if related_key_group_by_expression_list: + new_kw['related_key_group_by'] = related_key_group_by_expression_list + if related_key_select_expression_list: + new_kw['related_key_select_expression_list'] =\ + related_key_select_expression_list + + return sql_kw, new_kw + + ####################################################### + # Inventory management + security.declareProtected(Permissions.AccessContentsInformation, + 'getInventory') + def getInventory(self, src__=0, simulation_period='', **kw): + """ + Returns an inventory of a single or multiple resources on a single or + multiple nodes as a single float value + + from_date (>=) - only take rows which date is >= from_date + + to_date (<) - only take rows which date is < to_date + + at_date (<=) - only take rows which date is <= at_date + + resource (only in generic API in simulation) + + node - only take rows in stock table which node_uid is + equivalent to node + + payment - only take rows in stock table which payment_uid is + equivalent to payment + + section - only take rows in stock table which section_uid is + equivalent to section + + mirror_section - only take rows in stock table which mirror_section_uid is + mirror_section + + resource_category - only take rows in stock table which + resource_uid is member of resource_category + + node_category - only take rows in stock table which node_uid is + member of section_category + + payment_category - only take rows in stock table which payment_uid + is in section_category + + section_category - only take rows in stock table which section_uid is + member of section_category + + mirror_section_category - only take rows in stock table which + mirror_section_uid is member of + mirror_section_category + + node_filter - only take rows in stock table which node_uid + matches node_filter + + payment_filter - only take rows in stock table which payment_uid + matches payment_filter + + section_filter - only take rows in stock table which section_uid + matches section_filter + + mirror_section_filter - only take rows in stock table which + mirror_section_uid matches mirror_section_filter + + variation_text - only take rows in stock table with specified + variation_text. + XXX this way of implementing variation selection is far + from perfect + + sub_variation_text - only take rows in stock table with specified + variation_text + + variation_category - variation or list of possible variations (it is not + a cross-search ; SQL query uses OR). + Deprecated, use variation_text. + + simulation_state - only take rows with specified simulation_state + + transit_simulation_state - specifies which states are transit states + + omit_transit - do not evaluate transit_simulation_state + + input_simulation_state - only take rows with specified simulation_state + and quantity > 0 + + output_simulation_state - only take rows with specified simulation_state + and quantity < 0 + + ignore_variation - do not take into account variation in inventory + calculation (useless on getInventory, but useful on + getInventoryList) + + standardise - provide a standard quantity rather than an SKU (XXX + not implemented yet) + + omit_simulation - doesn't take into account simulation movements + + only_accountable - Only take into account accountable movements. By + default, only movements for which isAccountable() is + true will be taken into account. + + omit_input - doesn't take into account movement with quantity > 0 + + omit_output - doesn't take into account movement with quantity < 0 + + omit_asset_increase - doesn't take into account movement with asset_price > 0 + + omit_asset_decrease - doesn't take into account movement with asset_price < 0 + + selection_domain, selection_report - see ListBox + + group_by_variation - (useless on getInventory, but useful on + getInventoryList) + + group_by_node - (useless on getInventory, but useful on + getInventoryList) + + group_by_mirror_node - (useless on getInventory, but useful on + getInventoryList) + + group_by_sub_variation - (useless on getInventory, but useful on + getInventoryList) + + group_by_movement - (useless on getInventory, but useful on + getInventoryList) + + precision - the precision used to round quantities and prices. + + metric_type - convert the results to a specific metric_type + + quantity_unit - display results using this specific quantity unit + + transformed_resource - one, or several resources. list resources that can + be produced using those resources as input in a + transformation. + relative_resource_url for each returned line will + point to the transformed resource, while the stock + will be the stock of the produced resource, + expressed in number of transformed resources. + transformed_variation_text - to be used with transformed_resource, to + to refine the transformation selection only + to those using variated resources as input. + + **kw - if we want extended selection with more keywords (but + bad performance) check what we can do with + buildSQLQuery + + NOTE: we may want to define a parameter so that we can select the kind of + inventory statistics we want to display (ex. sum, average, cost, etc.) + """ + # JPS: this is a hint for implementation of xxx_filter arguments + # node_uid_count = portal_catalog.countResults(**node_filter) + # if node_uid_count not too big: + # node_uid_list = cache(portal_catalog(**node_filter)) + # pass this list to ZSQL method + # else: + # build a table in MySQL + # and join that table with the stock table + method = getattr(self,'get%sInventoryList' % simulation_period) + kw['ignore_group_by'] = 1 + result = method(inventory_list=0, src__=src__, **kw) + if src__: + return result + + total_result = 0.0 + if len(result) > 0: + if len(result) != 1: + raise ValueError, 'Sorry we must have only one' + result = result[0] + + if hasattr(result, "converted_quantity"): + total_result = result.converted_quantity + else: + inventory = result.total_quantity + if inventory is not None: + total_result = inventory + + return total_result + + security.declareProtected(Permissions.AccessContentsInformation, + 'getCurrentInventory') + def getCurrentInventory(self, **kw): + """ + Returns current inventory + """ + return self.getInventory(simulation_period='Current', **kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getAvailableInventory') + def getAvailableInventory(self, **kw): + """ + Returns available inventory + (current inventory - reserved_inventory) + """ + return self.getInventory(simulation_period='Available', **kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getFutureInventory') + def getFutureInventory(self, **kw): + """ + Returns future inventory + """ + return self.getInventory(simulation_period='Future', **kw) + + def _getDefaultGroupByParameters(self, ignore_group_by=0, + group_by_node=0, group_by_mirror_node=0, + group_by_section=0, group_by_mirror_section=0, + group_by_payment=0, group_by_project=0, group_by_funding=0, + group_by_ledger=0, group_by_function=0, + group_by_variation=0, group_by_sub_variation=0, + group_by_movement=0, group_by_date=0, + group_by_section_category=0, + group_by_section_category_strict_membership=0, + group_by_resource=None, + group_by=None, + **ignored): + """ + Set defaults group_by parameters + + If ignore_group_by is true, this function returns an empty dict. + + If any group-by is provided, automatically group by resource aswell + unless group_by_resource is explicitely set to false. + If no group by is provided, use the default group by: movement, node and + resource. + """ + new_group_by_dict = {} + if not ignore_group_by and group_by is None: + if group_by_node or group_by_mirror_node or group_by_section or \ + group_by_project or group_by_funding or group_by_ledger or \ + group_by_function or group_by_mirror_section or group_by_payment or \ + group_by_sub_variation or group_by_variation or \ + group_by_movement or group_by_date or group_by_section_category or\ + group_by_section_category_strict_membership: + if group_by_resource is None: + group_by_resource = 1 + new_group_by_dict['group_by_resource'] = group_by_resource + elif group_by_resource is None: + new_group_by_dict['group_by_movement'] = 1 + new_group_by_dict['group_by_node'] = 1 + new_group_by_dict['group_by_resource'] = 1 + return new_group_by_dict + + security.declareProtected(Permissions.AccessContentsInformation, + 'getInventoryList') + def getInventoryList(self, src__=0, optimisation__=True, + ignore_variation=0, standardise=0, + omit_simulation=0, + only_accountable=True, + default_stock_table='stock', + statistic=0, inventory_list=1, + precision=None, connection_id=None, + **kw): + """ + Returns a list of inventories for a single or multiple + resources on a single or multiple nodes, grouped by resource, + node, section, etc. Every line defines an inventory value for + a given group of resource, node, section. + NOTE: we may want to define a parameter so that we can select + the kind of inventory statistics we want to display (ex. sum, + average, cost, etc.) + + Optimisation queries. + Optimisation of a stock lookup is done to avoid a table scan + of all lines corresponding to a given node, section or payment, + because they grow with time and query time should not. + First query: Fetch fitting full inventory dates. + For each node, section or payment, find the first anterior full + inventory. + Second query: Fetch full inventory amounts. + Fetch values of inventory identified in the first query. + Third query: Classic stock table read. + Fetch all rows in stock table which are posterior to the inventory. + Final result + Add results of the second and third queries, and return it. + + Missing optimisations: + - In a getInventory case where everything is specified for the + resource, it's not required for the inventory to be full, it + just need to be done for the right resource. + If the resource isn't completely defined, we require inventory + to be full, which is implemented. + - Querying multiple nodes/categories/payments in one call prevents + from using optimisation, it should be equivalent to multiple calls + on individual nodes/categories/payments. + - + """ + getCategory = self.getPortalObject().portal_categories.getCategoryUid + + result_column_id_dict = {} + + metric_type = kw.pop('metric_type', None) + quantity_unit = kw.pop('quantity_unit', None) + quantity_unit_uid = None + + if quantity_unit is not None: + + if isinstance(quantity_unit, str): + quantity_unit_uid = getCategory(quantity_unit, 'quantity_unit') + if quantity_unit_uid is not None: + result_column_id_dict['converted_quantity'] = None + if metric_type is None: + # use the default metric type + metric_type = quantity_unit.split("/", 1)[0] + elif isinstance(quantity_unit, (int, float)): + # quantity_unit used to be a numerical parameter.. + raise ValueError('Numeric values for quantity_unit are not supported') + + + convert_quantity_result = False + if metric_type is not None: + metric_type_uid = getCategory(metric_type, 'metric_type') + + if metric_type_uid is not None: + convert_quantity_result = True + kw['metric_type_uid'] = Query( + metric_type_uid=metric_type_uid, + table_alias_list=(("measure", "measure"),)) + + if src__: + sql_source_list = [] + # If no group at all, give a default sort group by + kw.update(self._getDefaultGroupByParameters(**kw)) + base_inventory_kw = { + 'stock_table_id': default_stock_table, + 'src__': src__, + 'ignore_variation': ignore_variation, + 'standardise': standardise, + 'omit_simulation': omit_simulation, + 'only_accountable': only_accountable, + 'precision': precision, + 'inventory_list': inventory_list, + 'connection_id': connection_id, + 'statistic': statistic, + 'convert_quantity_result': convert_quantity_result, + 'quantity_unit_uid': quantity_unit_uid, + } + # Get cached data + if getattr(self, "Resource_zGetInventoryCacheResult", None) is not None and \ + optimisation__ and (not kw.get('from_date')) and \ + 'transformed_resource' not in kw: + # Here is the different kind of date + # from_date : >= + # to_date : < + # at_date : <= + # As we just have from_date, it means that we must use + # the to_date for the cache in order to avoid double computation + # of the same line + at_date = kw.pop("at_date", None) + if at_date is None: + to_date = kw.pop("to_date", None) + else: + # add one second so that we can use to_date + to_date = at_date + MYSQL_MIN_DATETIME_RESOLUTION + try: + cached_result, cached_date = self._getCachedInventoryList( + to_date=to_date, + sql_kw=kw, + **base_inventory_kw) + except StockOptimisationError: + cached_result = [] + kw['to_date'] = to_date + else: + if src__: + sql_source_list.extend(cached_result) + # Now must generate query for date diff + kw['to_date'] = to_date + kw['from_date'] = cached_date + else: + cached_result = [] + sql_kw, new_kw = self._generateKeywordDict(**kw) + # Copy kw content as _generateSQLKeywordDictFromKeywordDict + # remove some values from it + try: + new_kw_copy = deepcopy(new_kw) + except TypeError: + # new_kw contains wrong parameters + # as optimisation has already been disable we + # do not care about the deepcopy + new_kw_copy = new_kw + stock_sql_kw = self._generateSQLKeywordDictFromKeywordDict( + table=default_stock_table, sql_kw=sql_kw, new_kw=new_kw_copy) + stock_sql_kw.update(base_inventory_kw) + delta_result = self.Resource_zGetInventoryList( + **stock_sql_kw) + if src__: + sql_source_list.append(delta_result) + result = ';\n-- NEXT QUERY\n'.join(sql_source_list) + else: + if cached_result: + result = self._addBrainResults(delta_result, cached_result, new_kw) + else: + result = delta_result + return result + + security.declareProtected(Permissions.AccessContentsInformation, + 'getInventoryCacheLag') + def getInventoryCacheLag(self): + """ + Returns a duration, in days, for stock cache management. + If data in stock cache is older than lag compared to query's date + (at_date or to_date), then it becomes a "soft miss": use found value, + but add a new entry to cache at query's date minus half the lag. + So this value should be: + - Small enough that few enough rows need to be table-scanned for + average queries (probably queries against current date). + - Large enough that few enough documents get modified past that date, + otherwise cache entries would be removed from cache all the time. + """ + return self.SimulationTool_getInventoryCacheLag() + + def _getCachedInventoryList(self, to_date, sql_kw, stock_table_id, src__=False, **kw): + """ + Try to get a cached inventory list result + If not existing, fill the cache + """ + Resource_zGetInventoryList = self.Resource_zGetInventoryList + # Generate the SQL source without date parameter + # This will be the cache key + try: + no_date_kw = deepcopy(sql_kw) + except TypeError: + LOG("SimulationTool._getCachedInventoryList", WARNING, + "Failed copying sql_kw, disabling stock cache", + error=True) + raise StockOptimisationError + no_date_sql_kw, no_date_new_kw = self._generateKeywordDict(**no_date_kw) + no_date_stock_sql_kw = self._generateSQLKeywordDictFromKeywordDict( + table=stock_table_id, sql_kw=no_date_sql_kw, + new_kw=no_date_new_kw) + kw.update(no_date_stock_sql_kw) + if src__: + sql_source_list = [] + # Generate the cache key (md5 of query source) + sql_text_hash = md5(Resource_zGetInventoryList( + stock_table_id=stock_table_id, + src__=1, + **kw)).digest() + # Try to get result from cache + Resource_zGetInventoryCacheResult = self.Resource_zGetInventoryCacheResult + inventory_cache_kw = {'query': sql_text_hash} + if to_date is not None: + inventory_cache_kw['date'] = to_date + try: + cached_sql_result = Resource_zGetInventoryCacheResult(**inventory_cache_kw) + except ProgrammingError, (code, _): + if code != NO_SUCH_TABLE: + raise + # First use of the optimisation, we need to create the table + LOG("SimulationTool._getCachedInventoryList", INFO, + "Creating inventory cache stock") + if src__: + sql_source_list.append(self.SimulationTool_zCreateInventoryCache(src__=1)) + else: + self.SimulationTool_zCreateInventoryCache() + cached_sql_result = None + + if src__: + sql_source_list.append(Resource_zGetInventoryCacheResult(src__=1, **inventory_cache_kw)) + if cached_sql_result: + brain_result = loads(cached_sql_result[0].result) + # Rebuild the brains + cached_result = Results( + (brain_result['items'], brain_result['data']), + brains=getBrain( + Resource_zGetInventoryList.class_file_, + Resource_zGetInventoryList.class_name_, + ), + parent=self, + ) + else: + cached_result = [] + cache_lag = self.getInventoryCacheLag() + if cached_sql_result and (to_date is None or (to_date - DateTime(cached_sql_result[0].date) < cache_lag)): + cached_date = DateTime(cached_sql_result[0].date) + result = cached_result + elif to_date is not None: + # Cache miss, or hit with old data: store a new entry in cache. + # Don't store it at to_date, as it risks being flushed soon (ie, when + # any document older than to_date gets reindexed in stock table). + # Don't store it at to_date - cache_lag, as it would risk expiring + # soon as we store it (except if to_date is fixed for many queries, + # which we cannot tell here). + # So store it at half the cache_lag before to_date. + cached_date = to_date - cache_lag / 2 + new_cache_kw = deepcopy(sql_kw) + if cached_result: + # We can use cached result to generate new cache result + new_cache_kw['from_date'] = DateTime(cached_sql_result[0].date) + sql_kw, new_kw = self._generateKeywordDict( + to_date=cached_date, + **new_cache_kw) + kw.update(self._generateSQLKeywordDictFromKeywordDict( + table=stock_table_id, + sql_kw=sql_kw, + new_kw=new_kw, + ) + ) + new_result = Resource_zGetInventoryList( + stock_table_id=stock_table_id, + src__=src__, + **kw) + if src__: + sql_source_list.append(new_result) + else: + result = self._addBrainResults(new_result, cached_result, new_kw) + self.Resource_zInsertInventoryCacheResult( + query=sql_text_hash, + date=cached_date, + result=dumps({ + 'items': result.__items__, + 'data': result._data, + }), + ) + else: + # Cache miss and this getInventory() not specifying to_date, + # and other getInventory() have not created usable caches. + # In such case, do not create cache, do not use cache. + result = [] + cached_date = None + if src__: + result = sql_source_list + return result, cached_date + + def _addBrainResults(self, first_result, second_result, new_kw): + """ + Build a Results which is the addition of two other result + """ + # This part defined key to group lines from different Results + group_by_id_list = [] + group_by_id_list_append = group_by_id_list.append + + for group_by_id in new_kw.get('column_group_by', []): + if group_by_id == 'uid': + group_by_id_list_append('stock_uid') + else: + group_by_id_list_append(group_by_id) + # Add related key group by + related_key_dict_passthrough = new_kw.get("related_key_dict_passthrough", {}) + group_by_list = related_key_dict_passthrough.get('group_by_list', []) + cannot_group_by = set(group_by_list).difference( + related_key_dict_passthrough.get('select_list', []), + ) + if cannot_group_by: + # XXX-Aurel : to review & change, must prevent coming here before + raise ValueError("Impossible to group by %s" % (cannot_group_by, )) + group_by_id_list += group_by_list + + if len(group_by_id_list): + def getInventoryListKey(line): + """ + Generate a key based on values used in SQL group_by + """ + return tuple([line[x] for x in group_by_id_list]) + + else: + def getInventoryListKey(line): + """ + Return a dummy key, all line will be summed + """ + return "dummy" + result_column_id_dict = { + 'inventory': None, + 'total_quantity': None, + 'total_price': None + } + def addLineValues(line_a=None, line_b=None): + """ + Add columns of 2 lines and return a line with same + schema. If one of the parameters is None, returns the + other parameters. + + Arithmetic modifications on additions: + None + x = x + None + None = None + """ + if line_a is None: + return line_b + if line_b is None: + return line_a + # Create a new Shared.DC.ZRDB.Results.Results.__class__ + # instance to add the line values. + # the logic for the 5 lines below is taken from + # Shared.DC.ZRDB.Results.Results.__getitem__ + Result = line_a.__class__ + parent = line_a.aq_parent + result = Result((), parent) + try: + # We must copy the path so that getObject works + setattr(result, 'path', line_a.path) + except ValueError: # XXX: ValueError ? really ? + # getInventory return no object, so no path available + pass + if parent is not None: + result = result.__of__(parent) + for key in line_a.__record_schema__: + value = line_a[key] + if key in result_column_id_dict: + value_b = line_b[key] + if None not in (value, value_b): + result[key] = value + value_b + elif value is not None: + result[key] = value + else: + result[key] = value_b + elif line_a[key] == line_b[key]: + result[key] = line_a[key] + elif key not in ('date', 'stock_uid', 'path'): + # There are 2 possible reasons to end up here: + # - key corresponds to a projected column for which are neither + # known aggregated columns (in which case they should be in + # result_column_id_dict) nor part of grouping columns, and the + # result happens to be unstable. There are cases in ERP5 where + # such result is suposed to be stable, for example + # group_by=('xxx_uid'), selection_list=('xxx_path') because the + # relation is bijective (although the database doesn't know it). + # These should result in stable results (but don't necessarily + # do, ex: xxx_title when object title has been changed between + # cache fill and cache lookup). + # - line_a and line_b are indeed mismatched, and code calling us + # has a bug. + LOG('InventoryTool.getInventoryList.addLineValues', + PROBLEM, + 'mismatch for %s column: %s and %s' % ( + key, line_a[key], line_b[key])) + return result + # Add lines + inventory_list_dict = {} + for line_list in (first_result, second_result): + for line in line_list: + line_key = getInventoryListKey(line) + line_a = inventory_list_dict.get(line_key) + inventory_list_dict[line_key] = addLineValues(line_a, line) + sorted_inventory_list = inventory_list_dict.values() + # Sort results manually when required + sort_on = new_kw.get('sort_on') + if sort_on: + def cmp_inventory_line(line_a, line_b): + """ + Compare 2 inventory lines and sort them according to + sort_on parameter. + """ + result = 0 + for key, sort_direction in sort_on: + try: + result = cmp(line_a[key], line_b[key]) + except KeyError: + raise Exception('Impossible to sort result since columns sort ' + 'happens on are not available in result: %r' % (key, )) + if result: + if not sort_direction.upper().startswith('A'): + # Default sort is ascending, if a sort is given and + # it does not start with an 'A' then reverse sort. + # Tedious syntax checking is MySQL's job, and + # happened when queries were executed. + result *= -1 + break + return result + sorted_inventory_list.sort(cmp_inventory_line) + # Brain is rebuild properly using tuple not r instance + column_list = first_result._searchable_result_columns() + column_name_list = [x['name'] for x in column_list] + # Rebuild a result object based on added results + Resource_zGetInventoryList = self.Resource_zGetInventoryList + return Results( + (column_list, tuple([tuple([getattr(y, x) for x in column_name_list]) \ + for y in sorted_inventory_list])), + parent=self, + brains=getBrain( + Resource_zGetInventoryList.class_file_, + Resource_zGetInventoryList.class_name_, + ), + ) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getConvertedInventoryList') + def getConvertedInventoryList(self, simulation_period='', **kw): + """ + Return list of inventory with a 'converted_quantity' additional column, + which contains the sum of measurements for the specified metric type, + expressed in the 'quantity_unit' unit. + + metric_type - category + quantity_unit - category + """ + + warn('getConvertedInventoryList is Deprecated, use ' \ + 'getInventory instead.', DeprecationWarning) + + method = getattr(self,'get%sInventoryList' % simulation_period) + + return method(**kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getAllInventoryList') + def getAllInventoryList(self, src__=0, **kw): + """ + Returns list of inventory, for all periods. + Performs 1 SQL request for each simulation state, and merge the results. + Rename relevant columns with a '${simulation}_' prefix + (ex: 'total_price' -> 'current_total_price'). + """ + columns = ('total_quantity', 'total_price', 'converted_quantity') + + # Guess the columns to use to identify each row, by looking at the GROUP + # clause. Note that the call to 'mergeZRDBResults' will crash if the GROUP + # clause contains a column not requested in the SELECT clause. + kw.update(self._getDefaultGroupByParameters(**kw), ignore_group_by=1) + group_by_list = self._generateKeywordDict(**kw)[1].get('group_by_list', ()) + + results = [] + edit_result = {} + get_false_value = lambda row, column_name: row.get(column_name) or 0 + + for simulation in 'current', 'available', 'future': + method = getattr(self, 'get%sInventoryList' % simulation.capitalize()) + rename = {'inventory': None} # inventory column is deprecated + for column in columns: + rename[column] = new_name = '%s_%s' % (simulation, column) + edit_result[new_name] = get_false_value + results += (method(src__=src__, **kw), rename), + + if src__: + return ';\n-- NEXT QUERY\n'.join(r[0] for r in results) + return mergeZRDBResults(results, group_by_list, edit_result) + + + security.declareProtected(Permissions.AccessContentsInformation, + 'getCurrentInventoryList') + def getCurrentInventoryList(self, omit_transit=1, + transit_simulation_state=None, **kw): + """ + Returns list of current inventory grouped by section or site + """ + portal = self.getPortalObject() + kw['simulation_state'] = portal.getPortalCurrentInventoryStateList() + \ + portal.getPortalTransitInventoryStateList() + if transit_simulation_state is None: + transit_simulation_state = portal.getPortalTransitInventoryStateList() + + return self.getInventoryList( + omit_transit=omit_transit, + transit_simulation_state=transit_simulation_state, + **kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getAvailableInventoryList') + def getAvailableInventoryList(self, omit_transit=1, transit_simulation_state=None, **kw): + """ + Returns list of current inventory grouped by section or site + """ + portal = self.getPortalObject() + if transit_simulation_state is None: + transit_simulation_state = portal.getPortalTransitInventoryStateList() + kw['simulation_state'] = portal.getPortalCurrentInventoryStateList() + \ + portal.getPortalTransitInventoryStateList() + reserved_kw = {'simulation_state': portal.getPortalReservedInventoryStateList(), + 'transit_simulation_state': transit_simulation_state, + 'omit_input': 1} + return self.getInventoryList(reserved_kw=reserved_kw, omit_transit=omit_transit, + transit_simulation_state=transit_simulation_state, **kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getFutureInventoryList') + def getFutureInventoryList(self, **kw): + """ + Returns list of future inventory grouped by section or site + """ + portal = self.getPortalObject() + kw['simulation_state'] = portal.getPortalFutureInventoryStateList() + \ + portal.getPortalTransitInventoryStateList() + \ + portal.getPortalReservedInventoryStateList() + \ + portal.getPortalCurrentInventoryStateList() + return self.getInventoryList(**kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getInventoryStat') + def getInventoryStat(self, simulation_period='', **kw): + """ + getInventoryStat is the pending to getInventoryList in order to + provide statistics on getInventoryList lines in ListBox such as: + total of inventories, number of variations, number of different + nodes, etc. + """ + kw['group_by_variation'] = 0 + method = getattr(self,'get%sInventoryList' % simulation_period) + return method(statistic=1, inventory_list=0, optimisation__=False, + ignore_group_by=1, **kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getCurrentInventoryStat') + def getCurrentInventoryStat(self, **kw): + """ + Returns statistics of current inventory grouped by section or site + """ + return self.getInventoryStat(simulation_period='Current', **kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getAvailableInventoryStat') + def getAvailableInventoryStat(self, **kw): + """ + Returns statistics of available inventory grouped by section or site + """ + return self.getInventoryStat(simulation_period='Available', **kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getFutureInventoryStat') + def getFutureInventoryStat(self, **kw): + """ + Returns statistics of future inventory grouped by section or site + """ + return self.getInventoryStat(simulation_period='Future', **kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getInventoryChart') + def getInventoryChart(self, src__=0, **kw): + """ + Returns a list of couples derived from getInventoryList in order + to feed a chart renderer. Each couple consist of a label + (node, section, payment, combination of node & section, etc.) + and an inventory value. + + Mostly useful for charts in ERP5 forms. + """ + result = self.getInventoryList(src__=src__, **kw) + if src__ : + return result + + return [(r.node_title, r.total_quantity) for r in result] + + security.declareProtected(Permissions.AccessContentsInformation, + 'getCurrentInventoryChart') + def getCurrentInventoryChart(self, **kw): + """ + Returns list of current inventory grouped by section or site + """ + kw['simulation_state'] = self.getPortalObject()\ + .getPortalCurrentInventoryStateList() + return self.getInventoryChart(**kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getFutureInventoryChart') + def getFutureInventoryChart(self, **kw): + """ + Returns list of future inventory grouped by section or site + """ + portal = self.getPortalObject() + kw['simulation_state'] = portal.getPortalFutureInventoryStateList() + \ + portal.getPortalTransitInventoryStateList() + \ + portal.getPortalReservedInventoryStateList() + \ + portal.getPortalCurrentInventoryStateList() + return self.getInventoryChart(**kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getInventoryAssetPrice') + def getInventoryAssetPrice(self, src__=0, + simulation_period='', + valuation_method=None, + **kw): + """ + Same thing as getInventory but returns an asset + price rather than an inventory. + + If valuation method is None, returns the sum of total prices. + + Else it should be a string, in: + Filo + Fifo + WeightedAverage + MonthlyWeightedAverage + MovingAverage + When using a specific valuation method, a resource_uid is expected + as well as one of (section_uid or node_uid). + """ + if valuation_method is None: + method = getattr(self,'get%sInventoryList' % simulation_period) + kw['ignore_group_by'] = 1 + result = method( src__=src__, inventory_list=0, **kw) + if src__ : + return result + + if len(result) == 0: + return 0.0 + + total_result = 0.0 + for result_line in result: + if result_line.total_price is not None: + total_result += result_line.total_price + + return total_result + + if valuation_method not in ('Fifo', 'Filo', 'WeightedAverage', + 'MonthlyWeightedAverage', 'MovingAverage'): + raise ValueError("Invalid valuation method: %s" % valuation_method) + + assert 'node_uid' in kw or 'section_uid' in kw + sql_kw = self._generateSQLKeywordDict(**kw) + + if 'section_uid' in kw and 'node_uid' not in kw: + # ignore internal movements if ignore node + sql_kw['where_expression'] += ' AND ' \ + 'NOT(stock.section_uid<=>stock.mirror_section_uid)' + + result = self.Resource_zGetAssetPrice( + valuation_method=valuation_method, + src__=src__, + **sql_kw) + + if src__ : + return result + + if len(result) > 0: + return result[-1].total_asset_price + + security.declareProtected(Permissions.AccessContentsInformation, + 'getCurrentInventoryAssetPrice') + def getCurrentInventoryAssetPrice(self, **kw): + """ + Returns list of current inventory grouped by section or site + """ + kw['simulation_state'] = self.getPortalCurrentInventoryStateList() + return self.getInventoryAssetPrice(simulation_period='Current',**kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getAvailableInventoryAssetPrice') + def getAvailableInventoryAssetPrice(self, **kw): + """ + Returns list of available inventory grouped by section or site + (current inventory - deliverable) + """ + portal = self.getPortalObject() + kw['simulation_state'] = portal.getPortalReservedInventoryStateList() + \ + portal.getPortalCurrentInventoryStateList() + return self.getInventoryAssetPrice(**kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getFutureInventoryAssetPrice') + def getFutureInventoryAssetPrice(self, **kw): + """ + Returns list of future inventory grouped by section or site + """ + portal = self.getPortalObject() + kw['simulation_state'] = portal.getPortalFutureInventoryStateList() + \ + portal.getPortalReservedInventoryStateList() + \ + portal.getPortalCurrentInventoryStateList() + return self.getInventoryAssetPrice(**kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getInventoryHistoryList') + def getInventoryHistoryList(self, src__=0, ignore_variation=0, + standardise=0, omit_simulation=0, + only_accountable=True, omit_input=0, + omit_output=0, precision=None, **kw): + """ + Returns a time based serie of inventory values + for a single or a group of resource, node, section, etc. This is useful + to list the evolution with time of inventory values (quantity, asset price). + + TODO: + - make sure getInventoryHistoryList can return + cumulative values calculated by SQL (JPS) + """ + sql_kw = self._generateSQLKeywordDict(**kw) + return self.Resource_getInventoryHistoryList( + src__=src__, ignore_variation=ignore_variation, + standardise=standardise, omit_simulation=omit_simulation, + only_accountable=only_accountable, + omit_input=omit_input, omit_output=omit_output, + precision=precision, + **sql_kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getInventoryHistoryChart') + def getInventoryHistoryChart(self, src__=0, ignore_variation=0, + standardise=0, omit_simulation=0, + only_accountable=True, + omit_input=0, omit_output=0, + precision=None, **kw): + """ + getInventoryHistoryChart is the pensing to getInventoryHistoryList + to ease the rendering of time based graphs which show the evolution + of one ore more inventories. Each item in the serie consists of + time, value and "colour" (multiple graphs can be drawn for example + for each variation of a resource) + """ + sql_kw = self._generateSQLKeywordDict(**kw) + + return self.Resource_getInventoryHistoryChart( + src__=src__, ignore_variation=ignore_variation, + standardise=standardise, omit_simulation=omit_simulation, + only_accountable=only_accountable, + omit_input=omit_input, omit_output=omit_output, + precision=precision, + **sql_kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getMovementHistoryList') + def getMovementHistoryList(self, src__=0, ignore_variation=0, + standardise=0, omit_simulation=0, + omit_input=0, omit_output=0, + only_accountable=True, + omit_asset_increase=0, omit_asset_decrease=0, + initial_running_total_quantity=0, + initial_running_total_price=0, precision=None, + **kw): + """Returns a list of movements which modify the inventory + for a single or a group of resource, node, section, etc. + A running total quantity and a running total price are available on + brains. The initial values can be passed, in case you want to have an + "initial summary line". + """ + # Extend select_dict by order_by_list columns. + catalog = self.getPortalObject().portal_catalog.getSQLCatalog() + kw = catalog.getCannonicalArgumentDict(kw) + extra_column_set = {i[0] for i in kw.get('order_by_list', ())} + kw.setdefault('select_dict', {}).update( + (x.replace('.', '_') + '__ext__', x) + for x in extra_column_set if not x.endswith('__score__')) + + sql_kw = self._generateSQLKeywordDict(**kw) + + return self.Resource_zGetMovementHistoryList( + src__=src__, ignore_variation=ignore_variation, + standardise=standardise, + omit_simulation=omit_simulation, + only_accountable=only_accountable, + omit_input=omit_input, omit_output=omit_output, + omit_asset_increase=omit_asset_increase, + omit_asset_decrease=omit_asset_decrease, + initial_running_total_quantity= + initial_running_total_quantity, + initial_running_total_price= + initial_running_total_price, + precision=precision, **sql_kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getMovementHistoryStat') + def getMovementHistoryStat(self, src__=0, **kw): + """ + getMovementHistoryStat is the pending to getMovementHistoryList + for ListBox stat + + supported parameters are similar to ones accepted by getInventoryList + with the exception of group_by_* + """ + inventory_list = self.getInventoryList(ignore_group_by=1, **kw) + assert len(inventory_list) == 1 + return inventory_list[0] + + security.declareProtected(Permissions.AccessContentsInformation, + 'getNextAlertInventoryDate') + def getNextAlertInventoryDate(self, reference_quantity=0, src__=0, + simulation_period='Future', from_date=None, + range='min', # pylint: disable=redefined-builtin + initial_inventory_kw=None, + inventory_list_kw=None, + **kw): + """ + Give the next date where the quantity is lower than the + reference quantity. This is calculated by first looking if inventory + right now is good or not. If not, then look at inventory list until + a movement makes the inventory like expected. + + range - either 'min' (default) or 'nlt'. With 'nlt', returns + the next date where inventory is above reference_quantity + + initial_inventory_kw - additional parameters for the initial inventory + + inventory_list_kw - additional parameters for looking at next movements + (exemple: use omit_output) + """ + result = None + # First look at current inventory, we might have already an inventory + # lower than reference_quantity + def getCheckQuantityMethod(): + if range == 'min': + return lambda x: x < reference_quantity + elif range == 'nlt': + return lambda x: x >= reference_quantity + else: + raise ValueError("Uknown range type : %s" % (range,)) + + checkQuantity = getCheckQuantityMethod() + if from_date is None: + from_date = DateTime() + def getAugmentedInventoryKeyword(additional_kw): + inventory_kw = kw + if additional_kw: + inventory_kw = kw.copy() + inventory_kw.update(additional_kw) + return inventory_kw + inventory_method = getattr(self, "get%sInventory" % simulation_period) + initial_inventory = inventory_method(at_date=from_date, + **getAugmentedInventoryKeyword(initial_inventory_kw)) + if checkQuantity(initial_inventory): + result = from_date + else: + inventory_list_method = getattr(self, + "get%sInventoryList" % simulation_period) + inventory_list = inventory_list_method(src__=src__, from_date=from_date, + sort_on = (('date', 'ascending'),), group_by_movement=1, + **getAugmentedInventoryKeyword(inventory_list_kw)) + if src__ : + return inventory_list + total_inventory = initial_inventory + for inventory in inventory_list: + if inventory['inventory'] is not None: + total_inventory += inventory['inventory'] + if checkQuantity(total_inventory): + result = inventory['date'] + break + return result + + security.declareProtected(Permissions.AccessContentsInformation, + 'getNextNegativeInventoryDate') + def getNextNegativeInventoryDate(self, **kw): + """ + Deficient Inventory with a reference_quantity of 0, so when the + stock will be negative + """ + return self.getNextAlertInventoryDate(reference_quantity=0, **kw) + + ####################################################### + # Traceability management + security.declareProtected(Permissions.AccessContentsInformation, 'getTrackingList') + def getTrackingList(self, src__=0, + strict_simulation_state=1, history=0, **kw) : + """ + Returns a list of items in the form + + uid (of item) + date + node_uid + section_uid + resource_uid + variation_text + delivery_uid + + If at_date is provided, returns the a list which answers + to the question "where are those items at this date" or + "which are those items which are there a this date" + + If at_date is not provided, returns a history of positions + which answers the question "where have those items been + between this time and this time". This will be handled by + something like getTrackingHistoryList + + This method is only suitable for singleton items (an item which can + only be at a single place at a given time). Such items include + containers, serial numbers (ex. for engine), rolls with subrolls, + + This method is not suitable for batches (ex. a coloring batch). + For such items, standard getInventoryList method is appropriate + + Parameters are the same as for getInventory. + + Default sort orders is based on dates, reverse. + + + from_date (>=) - + + to_date (<) - + + at_date (<=) - only take rows which date is <= at_date + + history (boolean) - keep history variations + + resource (only in generic API in simulation) + + node - only take rows in stock table which node_uid is equivalent to node + + section - only take rows in stock table which section_uid is equivalent to section + + resource_category - only take rows in stock table which resource_uid is in resource_category + + node_category - only take rows in stock table which node_uid is in section_category + + section_category - only take rows in stock table which section_uid is in section_category + + variation_text - only take rows in stock table with specified variation_text + XXX this way of implementing variation selection is far from perfect + + variation_category - variation or list of possible variations + Deprecated, use variation_text + + simulation_state - only take rows with specified simulation_state + + selection_domain, selection_report - see ListBox + + **kw - if we want extended selection with more keywords (but bad performance) + check what we can do with buildSQLQuery + + Extra parameters for getTrackingList : + + item + + input - if set, answers to the question "which are those items which have been + delivered for the first time after from_date". Cannot be used with output + + output - if set, answers to the question "which are those items which have been + delivered for the last time before at_date or to_date". Cannot be used with input + + """ + next_item_simulation_state = kw.pop('next_item_simulation_state', None) + new_kw = self._generateSQLKeywordDict(table='item',strict_simulation_state=strict_simulation_state,**kw) + for key in ('at_date', 'to_date'): + value = kw.get(key, None) + if value is not None: + if isinstance(value, DateTime): + value = value.toZone('UTC').ISO() + value = '%s' % (value, ) + # Do not remove dates in new_kw, they are required in + # order to do a "select item left join item on date" + new_kw[key] = value + + # Extra parameters for the SQL Method + new_kw['join_on_item'] = not history and (new_kw.get('at_date') or \ + new_kw.get('to_date') or \ + new_kw.get('input') or \ + new_kw.get('output')) + new_kw['date_condition_in_join'] = not (new_kw.get('input') or new_kw.get('output')) + + # Pass simulation state to request + if next_item_simulation_state: + new_kw['simulation_state_list'] = next_item_simulation_state + elif kw.has_key('item.simulation_state'): + new_kw['simulation_state_list'] = kw['item.simulation_state'] + else: + new_kw['simulation_state_list'] = None + + return self.Resource_zGetTrackingList(src__=src__, + **new_kw) + + security.declareProtected(Permissions.AccessContentsInformation, 'getCurrentTrackingList') + def getCurrentTrackingList(self, **kw): + """ + Returns list of current inventory grouped by section or site + """ + portal = self.getPortalObject() + kw['item.simulation_state'] = portal\ + .getPortalCurrentInventoryStateList() + kw['next_item_simulation_state'] = portal\ + .getPortalCurrentInventoryStateList() + portal\ + .getPortalTransitInventoryStateList() + return self.getTrackingList(**kw) + + security.declareProtected(Permissions.AccessContentsInformation, 'getCurrentTrackingHistoryList') + def getCurrentTrackingHistoryList(self, **kw): + """ + Returns list of current inventory grouped by section or site + """ + kw['item.simulation_state'] = self.getPortalObject()\ + .getPortalCurrentInventoryStateList() + return self.getTrackingHistoryList(**kw) + + security.declareProtected(Permissions.AccessContentsInformation, 'getTrackingHistoryList') + def getTrackingHistoryList(self, **kw): + """ + Returns history list of inventory grouped by section or site + """ + kw['history'] = 1 + return self.getTrackingList(**kw) + + security.declareProtected(Permissions.AccessContentsInformation, 'getFutureTrackingList') + def getFutureTrackingList(self, **kw): + """ + Returns list of future inventory grouped by section or site + """ + portal = self.getPortalObject() + kw['item.simulation_state'] = portal.getPortalFutureInventoryStateList() + \ + portal.getPortalTransitInventoryStateList() + \ + portal.getPortalReservedInventoryStateList() + \ + portal.getPortalCurrentInventoryStateList() + return self.getTrackingList(**kw) + + ####################################################### + # Capacity Management + security.declareProtected( Permissions.ModifyPortalContent, 'updateCapacity' ) + def updateCapacity(self, node): + capacity_item_list = [] + for o in node.contentValues(): + if o.isCapacity(): + # Do whatever is needed + capacity_item_list += o.asCapacityItemList() + # Do whatever with capacity_item_list + # and store the resulting new capacity in node + node._capacity_item_list = capacity_item_list + + security.declareProtected( Permissions.ModifyPortalContent, 'isAmountListInsideCapacity' ) + def isAmountListInsideCapacity(self, node, amount_list, + resource_aggregation_base_category=None, resource_aggregation_depth=None): + """ + Purpose: decide if a list of amounts is consistent with the capacity of a node + + If any resource in amount_list is missing in the capacity of the node, resource + aggregation is performed, based on resource_aggregation_base_category. If the + base category is not specified, it is an error (should guess instead?). The resource + aggregation is done at the level of resource_aggregation_depth in the tree + of categories. If resource_aggregation_depth is not specified, it's an error. + + Assumptions: amount_list is an association list, like ((R1 V1) (R2 V2)). + node has an attribute '_capacity_item_list' which is a list of association lists. + resource_aggregation_base_category is a Base Category object or a list of Base + Category objects or None. + resource_aggregation_depth is a strictly positive integer or None. + """ + # Make a copy of the attribute _capacity_item_list, because it may be necessary + # to modify it for resource aggregation. + capacity_item_list = node._capacity_item_list[:] + + # Make a mapping between resources and its indices. + resource_map = {} + index = 0 + for alist in capacity_item_list: + for pair in alist: + resource = pair[0] +# LOG('isAmountListInsideCapacity', 0, +# "resource is %s" % repr(resource)) + if resource not in resource_map: + resource_map[resource] = index + index += 1 + + # Build a point from the amount list. + point = zeros(index, 'd') # Fill up zeros for safety. + mask_map = {} # This is used to skip items in amount_list. + for amount in amount_list: + if amount[0] in mask_map: + continue + # This will fail, if amount_list has any different resource from the capacity. + # If it has any different point, then we should ...... + # + # There would be two possible different solutions: + # 1) If a missing resource is a meta-resource of resources supported by the capacity, + # it is possible to add the resource into the capacity by aggregation. + # 2) If a missing resource has a meta-resource as a parent and the capacity supports + # the meta-resource directly or indirectly (`indirectly' means `by aggregation'), + # it is possible to convert the missing resource into the meta-resource. + # + # However, another way has been implemented here. This does the following, if the resource + # is not present in the capacity: + # 1) If the value is zero, just ignore the resource, because zero is always acceptable. + # 2) Attempt to aggregate resources both of the capacity and of the amount list. This aggregation + # is performed at the depth of 'resource_aggregation_depth' under the base category + # 'resource_aggregation_base_category'. + # + resource = amount[0] + if resource in resource_map: + point[resource_map[amount[0]]] = amount[1] + else: + if amount[1] == 0: + # If the value is zero, no need to consider. + pass + elif resource_aggregation_base_category is None or resource_aggregation_depth is None: + # XXX use an appropriate error class + # XXX should guess a base category instead of emitting an exception + raise RuntimeError, "The resource '%s' is not found in the capacity, and the argument 'resource_aggregation_base_category' or the argument 'resource_aggregation_depth' is not specified" % resource + else: + # It is necessary to aggregate resources, to guess the capacity of this resource. + + def getAggregationResourceUrl(url, depth): + # Return a partial url of the argument 'url'. + # If 'url' is '/foo/bar/baz' and 'depth' is 2, return '/foo/bar'. + pos = 0 + for _ in range(resource_aggregation_depth): + pos = url.find('/', pos+1) + if pos < 0: + break + if pos < 0: + return None + pos = url.find('/', pos+1) + if pos < 0: + pos = len(url) + return url[:pos] + + def getAggregatedResourceList(aggregation_url, category, resource_list): + # Return a list of resources which should be aggregated. 'aggregation_url' is used + # for a top url of those resources. 'category' is a base category for the aggregation. + aggregated_resource_list = [] + for resource in resource_list: + for url in resource.getCategoryMembershipList(category, base=1): + if url.startswith(aggregation_url): + aggregated_resource_list.append(resource) + return aggregated_resource_list + + def getAggregatedItemList(item_list, resource_list, aggregation_resource): + # Return a list of association lists, which is a result of an aggregation. + # 'resource_list' is a list of resources which should be aggregated. + # 'aggregation_resource' is a category object which is a new resource created by + # this aggregation. + # 'item_list' is a list of association lists. + new_item_list = [] + for alist in item_list: + new_val = 0 + new_alist = [] + # If a resource is not a aggregated, then add it to the new alist as it is. + # Otherwise, aggregate it to a single value. + for pair in alist: + if pair[0] in resource_list: + new_val += pair[1] + else: + new_alist.append(pair) + # If it is zero, ignore this alist, as it is nonsense. + if new_val != 0: + new_alist.append([aggregation_resource, new_val]) + new_item_list.append(new_alist) + return new_item_list + + # Convert this to a string if necessary, for convenience. + if not isinstance(resource_aggregation_base_category, (tuple, list)): + resource_aggregation_base_category = (resource_aggregation_base_category,) + + done = 0 +# LOG('isAmountListInsideCapacity', 0, +# "resource_aggregation_base_category is %s" % repr(resource_aggregation_base_category)) + for category in resource_aggregation_base_category: + for resource_url in resource.getCategoryMembershipList(category, base=1): + aggregation_url = getAggregationResourceUrl(resource_url, + resource_aggregation_depth) + if aggregation_url is None: + continue + aggregated_resource_list = getAggregatedResourceList (aggregation_url, + category, + resource_map.keys()) + # If any, do the aggregation. + if len(aggregated_resource_list) > 0: + aggregation_resource = self.portal_categories.resolveCategory(aggregation_url) + # Add the resource to the mapping. + # LOG('aggregation_resource', 0, str(aggregation_resource)) + resource_map[aggregation_resource] = index + index += 1 + # Add the resource to the point. + point = resize(point, (index,)) + val = 0 + for aggregated_amount in amount_list: + for url in aggregated_amount[0].getCategoryMembershipList(category, base=1): + if url.startswith(aggregation_url): + val += aggregated_amount[1] + mask_map[aggregated_amount[0]] = None + break + point[index-1] = val + # Add capacity definitions of the resource into the capacity. + capacity_item_list += getAggregatedItemList(capacity_item_list, + aggregated_resource_list, + aggregation_resource) + done = 1 + break + if done: + break + if not done: + raise RuntimeError, "Aggregation failed" + + # Build a matrix from the capacity item list. +# LOG('resource_map', 0, str(resource_map)) + matrix = zeros((len(capacity_item_list)+1, index), 'd') + for index in range(len(capacity_item_list)): + for pair in capacity_item_list[index]: + matrix[index,resource_map[pair[0]]] = pair[1] + +# LOG('isAmountListInsideCapacity', 0, +# "matrix = %s, point = %s, capacity_item_list = %s" % (str(matrix), str(point), str(capacity_item_list))) + return solve(matrix, point) + + + # Asset Price Calculation + def updateAssetPrice(self, resource, variation_text, section_category, node_category, + strict_membership=0, simulation_state=None): + if simulation_state is None: + simulation_state = self.getPortalCurrentInventoryStateList() + # Initialize price + current_asset_price = 0.0 # Missing: initial inventory price !!! + current_inventory = 0.0 + # Parse each movement + brain_list = self.Resource_zGetMovementHistoryList(resource=[resource], + variation_text=variation_text, + section_category=section_category, + node_category=node_category, + strict_membership=strict_membership, + simulation_state=simulation_state) # strict_membership not taken into account + # We select movements related to certain nodes (ex. Stock) and sections (ex.Coramy Group) + result = [] + for b in brain_list: + m = b.getObject() + if m is not None: + previous_inventory = current_inventory + inventory_quantity = b.quantity # We should use the aggregated quantity provided by Resource_zGetMovementHistoryList + quantity = m.getQuantity() # The movement quantity is important to determine the meaning of source and destination + # Maybe we should take care of target qty in delired deliveries + if quantity is None: + quantity = 0.0 + if m.getSourceValue() is None: + # This is a production movement or an inventory movement + # Use Industrial Price + current_inventory += inventory_quantity # Update inventory + if m.getPortalType() in ('Inventory Line', 'Inventory Cell'): # XX should be replaced by isInventory ??? + asset_price = m.getPrice() + if asset_price in (0.0, None): + asset_price = current_asset_price # Use current price if no price defined + else: # this is a production + asset_price = m.getIndustrialPrice() + if asset_price is None: asset_price = current_asset_price # Use current price if no price defined + result.append((m.getRelativeUrl(), m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), + m.getQuantity(), 'Production or Inventory', 'Price: %s' % asset_price + )) + elif m.getDestinationValue() is None: + # This is a consumption movement or an inventory movement + current_inventory += inventory_quantity # Update inventory + asset_price = current_asset_price + result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), + m.getQuantity(), 'Consumption or Inventory', 'Price: %s' % asset_price + )) + elif m.getSourceValue().isAcquiredMemberOf(node_category) and m.getDestinationValue().isAcquiredMemberOf(node_category): + # This is an internal movement + current_inventory += inventory_quantity # Update inventory + asset_price = current_asset_price + result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), + m.getQuantity(), 'Internal', 'Price: %s' % asset_price + )) + elif m.getSourceValue().isAcquiredMemberOf(node_category) and quantity < 0: + # This is a physically inbound movement - try to use commercial price + if m.getSourceSectionValue() is None: + # No meaning + current_inventory += inventory_quantity # Update inventory + asset_price = current_asset_price + result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), + m.getQuantity(), 'Error', 'Price: %s' % asset_price + )) + elif m.getDestinationSectionValue() is None: + # No meaning + current_inventory += inventory_quantity # Update inventory + asset_price = current_asset_price + result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), + m.getQuantity(), 'Error', 'Price: %s' % asset_price + )) + elif m.getDestinationSectionValue().isAcquiredMemberOf(section_category): + current_inventory += inventory_quantity # Update inventory + if m.getDestinationValue().isAcquiredMemberOf('site/Piquage'): + # Production + asset_price = m.getIndustrialPrice() + if asset_price is None: asset_price = current_asset_price # Use current price if no price defined + result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), + m.getQuantity(), 'Production', 'Price: %s' % asset_price + )) + else: + # Inbound from same section + asset_price = current_asset_price + result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), + m.getQuantity(), 'Inbound same section', 'Price: %s' % asset_price + )) + else: + current_inventory += inventory_quantity # Update inventory + asset_price = m.getPrice() + result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), + m.getQuantity(), 'Inbound different section', 'Price: %s' % asset_price + )) + elif m.getDestinationValue().isAcquiredMemberOf(node_category) and quantity > 0: + # This is a physically inbound movement - try to use commercial price + if m.getSourceSectionValue() is None: + # No meaning + current_inventory += inventory_quantity # Update inventory + asset_price = current_asset_price + result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), + m.getQuantity(), 'Error', 'Price: %s' % asset_price + )) + elif m.getDestinationSectionValue() is None: + # No meaning + current_inventory += inventory_quantity # Update inventory + asset_price = current_asset_price + result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), + m.getQuantity(), 'Error', 'Price: %s' % asset_price + )) + elif m.getSourceSectionValue().isAcquiredMemberOf(section_category): + current_inventory += inventory_quantity # Update inventory + if m.getSourceValue().isAcquiredMemberOf('site/Piquage'): + # Production + asset_price = m.getIndustrialPrice() + if asset_price is None: asset_price = current_asset_price # Use current price if no price defined + result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), + m.getQuantity(), 'Production', 'Price: %s' % asset_price + )) + else: + # Inbound from same section + asset_price = current_asset_price + result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), + m.getQuantity(), 'Inbound same section', 'Price: %s' % asset_price + )) + else: + current_inventory += inventory_quantity # Update inventory + asset_price = m.getPrice() + result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), + m.getQuantity(), 'Inbound different section', 'Price: %s' % asset_price + )) + else: + # Outbound movement + current_inventory += inventory_quantity # Update inventory + asset_price = current_asset_price + result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), + m.getQuantity(), 'Outbound', 'Price: %s' % asset_price + )) + + # Update asset_price + if current_inventory > 0: + if inventory_quantity is not None: + # Update price with an average of incoming goods and current goods + current_asset_price = ( current_asset_price * previous_inventory + asset_price * inventory_quantity ) / float(current_inventory) + else: + # New price is the price of incoming goods - negative stock has no meaning for asset calculation + current_asset_price = asset_price + + result.append(('###New Asset Price', current_asset_price, 'New Inventory', current_inventory)) + + # Update Asset Price on the right side + if m.getSourceSectionValue() is not None and m.getSourceSectionValue().isAcquiredMemberOf(section_category): + # for each movement, source section is member of one and one only accounting category + # therefore there is only one and one only source asset price + m._setSourceAssetPrice(current_asset_price) + #quantity = m.getInventoriatedQuantity() + #if quantity: + # #total_asset_price = - current_asset_price * quantity + # #m.Movement_zSetSourceTotalAssetPrice(uid=m.getUid(), total_asset_price = total_asset_price) + # m._setSourceAssetPrice(current_asset_price) + if m.getDestinationSectionValue() is not None and m.getDestinationSectionValue().isMemberOf(section_category): + # for each movement, destination section is member of one and one only accounting category + # therefore there is only one and one only destination asset price + m._setDestinationAssetPrice(current_asset_price) + #quantity = m.getInventoriatedQuantity() + #if quantity: + # total_asset_price = current_asset_price * quantity + # m.Movement_zSetDestinationTotalAssetPrice(uid=m.getUid(), total_asset_price = total_asset_price) + # Global reindexing required afterwards in any case: so let us do it now + # Until we get faster methods (->reindexObject()) + m.reindexObject() + + return result + + def _findBuilderForDelivery(self, delivery, movement_portal_type): + """ + Find out the builder corresponding to a delivery by looking at the business process + """ + builder = None + portal_type = delivery.getPortalType() + for business_link in delivery.asComposedDocument().objectValues(portal_type="Business Link"): + for business_link_builder in business_link.getDeliveryBuilderValueList(): + if business_link_builder.getDeliveryPortalType() == portal_type \ + and business_link_builder.getDeliveryLinePortalType() == movement_portal_type: + builder = business_link_builder + break + if builder is not None: + break + return builder + + security.declareProtected( Permissions.ModifyPortalContent, 'mergeDeliveryList' ) + def mergeDeliveryList(self, delivery_list): + """ + Merge multiple deliveries into one delivery. + All delivery lines are merged into the first one. + The first one is therefore called main_delivery here. + The others are cancelled. + """ + # Sanity checks. + if not(len(delivery_list) >=2): + raise ValueError("Please select at least 2 deliveries") + portal= self.getPortalObject() + translateString = portal.Base_translateString + error_list = [] + if len(delivery_list) > 1: + portal_type_set = set([x.getPortalType() for x in delivery_list]) + if len(portal_type_set) != 1: + error_list.append(translateString("Please select only deliveries of same type")) + else: + allowed_state_set = set(portal.getPortalReservedInventoryStateList() + \ + portal.getPortalFutureInventoryStateList()) + found_state_set = set([x.getSimulationState() for x in delivery_list]) + if found_state_set.difference(allowed_state_set): + error_list.append(translateString("Found delivery having unexpected status for merge")) + else: + movement_portal_type_set = set() + for delivery in delivery_list: + movement_portal_type_set.update([x.getPortalType() for x in delivery.getMovementList()]) + if len(movement_portal_type_set) != 1: + error_list.append(translateString("Please Select only movement of same type")) + else: + # Allow to call a script to do custom checking conditions before merge + main_delivery = delivery_list[0] + check_merge_condition_method = main_delivery._getTypeBasedMethod("checkMergeConditionOnDeliveryList") + if check_merge_condition_method is not None: + error_list.extend(check_merge_condition_method(delivery_list=delivery_list)) + if len(error_list) == 0: + # so far so good + # in delivery_list we have list of delivery to merge + simulation_movement_list = [] + to_copy_delivery_line_list = [] # for lines not coming from upper simulation, thus + # created by hand should be manually added to main + # delivery since they are not coming from builder + for delivery in delivery_list: + line_id_to_delete_list = [] + for movement in delivery.getMovementList(): + related_simulation_movement_list = movement.getDeliveryRelatedValueList() + for simulation_movement in related_simulation_movement_list: + # if we are on a root applied rule directly, so in the case of + # a manually added line, we have to copy + # the simulation movement into to main delivery + if simulation_movement.getParentValue().getParentValue().getId() == "portal_simulation": + # For manually added lines, make sure we have only one simulation movement + assert len(related_simulation_movement_list) == 1 + if not(delivery is main_delivery): + to_copy_delivery_line_list.append(movement) + else: + simulation_movement.setDeliveryValue(None) + simulation_movement_list.append(simulation_movement) + # Since we keep the main delivery, we remove existing lines already + # coming from builder to let builder recreate them in the same time + # as other ones (to possibly merge lines also) + movement_id = movement.getId() + if delivery is main_delivery and not(movement_id in line_id_to_delete_list): + line_id_to_delete_list.append(movement.getId()) + if line_id_to_delete_list: + delivery.manage_delObjects(ids=line_id_to_delete_list) + # It is required to expand again simulation movement, because + # we unlinked them from delivery, so it is possible that some + # properties will change on simulation movement (mostly categories). + # By expanding again, we will avoid having many deliveries instead + # of one when doing "merge" + for simulation_movement in simulation_movement_list: + simulation_movement.expand(expand_policy='immediate') + + # activate builder + movement_portal_type, = movement_portal_type_set + merged_builder = self._findBuilderForDelivery(main_delivery, movement_portal_type) + if merged_builder is None: + error_list.append(translateString("Unable to find builder")) + else: + merged_builder.build(movement_relative_url_list=[q.getRelativeUrl() for q in \ + simulation_movement_list], merge_delivery=True, + delivery_relative_url_list=[main_delivery.getRelativeUrl()]) + # Finally, copy all lines that were created manually on all deliveries except + # the main one + @UnrestrictedMethod + def setMainDeliveryModifiable(delivery): + # set causality state in such way we can modify delivery + delivery.diverge() + setMainDeliveryModifiable(main_delivery) + delivery_type_list = portal.getPortalDeliveryTypeList() + for delivery_line in to_copy_delivery_line_list: + delivery = delivery_line.getParentValue() + if not(delivery.getPortalType() in delivery_type_list): + raise NotImplementedError("Merge of deliveries doe not yet handle case of cells") + copy_data = delivery.manage_copyObjects(ids=[delivery_line.getId()]) + main_delivery.manage_pasteObjects(copy_data) + main_delivery.updateCausalityState() + + # Finally do cleanup + for delivery in delivery_list[1:]: + # cancel, delete - to disallow any user related operations on those deliveries + after_merge_method = delivery._getTypeBasedMethod('cleanDeliveryAfterMerge') + if after_merge_method is not None: + after_merge_method() + else: + error_list.append(translateString("Please select at least two deliveries")) + return error_list + + ####################################################### + # Sequence + security.declareProtected(Permissions.AccessContentsInformation, + 'getSequence') + def getSequence(self, **kw): + """ + getSequence is take the same parameters as Sequence constructor, + and return a Sequence. + """ + return Sequence(**kw) + + ####################################################### + # Time Management + security.declareProtected(Permissions.AccessContentsInformation, + 'getAvailableTime') + def getAvailableTime(self, from_date=None, to_date=None, + portal_type=(), node=(), + resource=(), src__=0, **kw): + """ + Calculate available time for a node + Returns an inventory of a single or multiple resources on a single + node as a single float value + + from_date (>=) - only take rows which mirror_date is >= from_date + + to_date (<) - only take rows which date is < to_date + + node - only take rows in stock table which node_uid is + equivalent to node + + resource - only take rows in stock table which resource_uid is + equivalent to resource + + portal_type - only take rows in stock table which portal_type + is in portal_type parameter + """ + # XXX For now, consider that from_date and to_date are required + if (from_date is None) or (to_date is None): + raise NotImplementedError, \ + "getAvailableTime does not managed yet None values" + portal = self.getPortalObject() + # Calculate portal_type + if not portal_type: + portal_type = portal.getPortalCalendarPeriodTypeList() + + simulation_state = portal.getPortalCurrentInventoryStateList() + \ + portal.getPortalTransitInventoryStateList() + \ + portal.getPortalReservedInventoryStateList() + + sql_result = portal.Node_zGetAvailableTime( + from_date=from_date, + to_date=to_date, + portal_type=portal_type, + node=node, + resource=resource, + simulation_state=simulation_state, + src__=src__, **kw) + if not src__: + result = 0 + if len(sql_result) == 1: + result = sql_result[0].total_quantity + else: + result = sql_result + return result + + security.declareProtected(Permissions.AccessContentsInformation, + 'getAvailableTimeSequence') + def getAvailableTimeSequence(self, from_date, to_date, + portal_type=(), node=(), + resource=(), + src__=0, + **kw): + """ + Calculate available time for a node in multiple period of time. + Each row is the available time for a specific period + + node - only take rows in stock table which node_uid is + equivalent to node + + portal_type - only take rows in stock table which portal_type + is in portal_type parameter + + resource - only take rows in stock table which resource_uid is + equivalent to resource + + from_date (>=) - return period which start >= from_date + + to_date (<) - return period which start < to_date + + second, minute, + hour, day, + month, year - duration of each time period (cumulative) + """ + portal = self.getPortalObject() + # Calculate portal_type + if not portal_type: + portal_type = portal.getPortalCalendarPeriodTypeList() + + sequence = Sequence(from_date, to_date, **kw) + for sequence_item in sequence: + setattr(sequence_item, 'total_quantity', + self.getAvailableTime( + from_date=sequence_item.from_date, + to_date=sequence_item.to_date, + portal_type=portal_type, + node=node, + resource=resource, + src__=src__)) + return sequence + + security.declareProtected(Permissions.AccessContentsInformation, + 'getAvailableTimeMovementList') + def getAvailableTimeMovementList(self, from_date, to_date, + **kw): + """ + Calculate available time movement list by taking into account + both available time and not available time. + + Necessary parameter is at least node. + + Parameters supported by getMovementHistoryList are supported here. + + from_date (>=) - return period which start >= from_date + + to_date (<) - return period which start < to_date + """ + portal = self.getPortalObject() + if kw.get("simulation_state", None) is None: + kw["simulation_state"] = portal.getPortalCurrentInventoryStateList() + \ + portal.getPortalTransitInventoryStateList() + \ + portal.getPortalReservedInventoryStateList() + movement_list = self.getMovementHistoryList(from_date=from_date, + to_date=to_date, group_by_movement=1, + group_by_date=1, **kw) + # do import on top, but better to avoid breaking instances with older softwares + from interval import IntervalSet, Interval + # we look at all movements, and we build a set of intervals for available + # time, another for not available time, and we do substraction of both sets + assignment_interval_set = IntervalSet() + leave_interval_set = IntervalSet() + result_list = [] + + def getOrderedMovementDates(movement): + date_list = [movement.date, movement.mirror_date] + date_list.sort() + return date_list + + movement_availability_dict = {} # to later map availability intervals with their movements + for movement in movement_list: + start_date, stop_date = getOrderedMovementDates(movement) + current_interval = Interval(start_date, stop_date) + # case of available time + if movement.total_quantity > 0: + assignment_interval_set.add(current_interval) + movement_availability_dict[current_interval] = movement + # case of not available time + else: + leave_interval_set.add(current_interval) + i = 0 + # Parse all calculated availability_interval to find matching movements to + # be returned in the result. IntervalSet are already ordered + for availability_interval in (assignment_interval_set - leave_interval_set): + while True: + assignment_interval = assignment_interval_set[i] + if availability_interval in assignment_interval: + result_list.append(movement_availability_dict[assignment_interval].asContext( + start_date=availability_interval.lower_bound, + stop_date=availability_interval.upper_bound)) + break + else: + i += 1 + return result_list + + def _checkExpandAll(self, activate_kw=None): + """Check all simulation trees using AppliedRule._checkExpand + """ + if activate_kw is None: + activate_kw = {} + portal = self.getPortalObject() + active_process = portal.portal_activities.newActiveProcess().getPath() + kw = dict(priority=3, tag='checkExpand') + kw.update(group_method_cost=1, max_retry=0, + active_process=active_process, **activate_kw) + self._recurseCallMethod('_checkExpand', min_depth=1, max_depth=1, + activate_kw=kw) + return active_process + +from Products.ERP5Type.DateUtils import addToDate + +class SequenceItem: + """ + SequenceItem define a time period. + period. + """ + def __init__(self, from_date, to_date): + self.from_date = from_date + self.to_date = to_date + +class Sequence: + """ + Sequence is a iterable object, which calculate a range of time + period. + """ + def __init__(self, from_date, to_date, + second=None, minute=None, hour=None, + day=None, month=None, year=None): + """ + Calculate a list of time period. + Time period is a 2-tuple of 2 DateTime, which represent the from date + and to date of the period. + + The start date of a period is calculated with the rule + start_date of the previous + period duration + + from_date (>=) - return period which start >= from_date + + to_date (<) - return period which start < to_date + + second, minute, + hour, day, + month, year - duration of each time period (cumulative) + at least one of those parameters must be specified. + """ + if not (second or minute or hour or day or month or year): + raise ValueError('Period duration must be specified') + + self.item_list = [] + # Calculate all time period + current_from_date = from_date + while current_from_date < to_date: + current_to_date = addToDate(current_from_date, + second=second, + minute=minute, + hour=hour, + day=day, + month=month, + year=year) + self.item_list.append(SequenceItem(current_from_date, + current_to_date)) + current_from_date = current_to_date + + def __len__(self): + return len(self.item_list) + + def __getitem__(self, key): + return self.item_list[key] + + def __contains__(self, value): + return (value in self.item_list) + + def __iter__(self): + for x in self.item_list: + yield x + +InitializeClass(Sequence) +allow_class(Sequence) +InitializeClass(SequenceItem) +allow_class(SequenceItem) + +InitializeClass(SimulationTool) diff --git a/product/ERP5/bootstrap/erp5_core/ToolComponentTemplateItem/portal_components/tool.erp5.SimulationTool.xml b/product/ERP5/bootstrap/erp5_core/ToolComponentTemplateItem/portal_components/tool.erp5.SimulationTool.xml new file mode 100644 index 0000000000000000000000000000000000000000..33a359547764fe17d63e3fee83d01279ed0a1807 --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/ToolComponentTemplateItem/portal_components/tool.erp5.SimulationTool.xml @@ -0,0 +1,110 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Tool Component" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>default_reference</string> </key> + <value> <string>SimulationTool</string> </value> + </item> + <item> + <key> <string>default_source_reference</string> </key> + <value> <string>Products.ERP5.Tool.SimulationTool</string> </value> + </item> + <item> + <key> <string>description</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>tool.erp5.SimulationTool</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Tool Component</string> </value> + </item> + <item> + <key> <string>sid</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>text_content_error_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>text_content_warning_message</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>version</string> </key> + <value> <string>erp5</string> </value> + </item> + <item> + <key> <string>workflow_history</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="PersistentMapping" module="Persistence.mapping"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>data</string> </key> + <value> + <dictionary> + <item> + <key> <string>component_validation_workflow</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_log</string> </key> + <value> + <list> + <dictionary> + <item> + <key> <string>action</string> </key> + <value> <string>validate</string> </value> + </item> + <item> + <key> <string>validation_state</string> </key> + <value> <string>validated</string> </value> + </item> + </dictionary> + </list> + </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/bootstrap/erp5_core/bt/template_document_id_list b/product/ERP5/bootstrap/erp5_core/bt/template_document_id_list index 70f76cd5bf27b9a8e072ff4a2f50db9e2a8b69cf..c3e8c5ac1c4bc3b9c99f15d8e96f59e999482ea1 100644 --- a/product/ERP5/bootstrap/erp5_core/bt/template_document_id_list +++ b/product/ERP5/bootstrap/erp5_core/bt/template_document_id_list @@ -1,3 +1,4 @@ +document.erp5.AppliedRule document.erp5.Delivery document.erp5.DeliveryCell document.erp5.DeliveryLine @@ -9,4 +10,5 @@ document.erp5.InvoiceLine document.erp5.Item document.erp5.Order document.erp5.PackingList -document.erp5.ScriptConstraint \ No newline at end of file +document.erp5.ScriptConstraint +document.erp5.SimulationMovement \ No newline at end of file diff --git a/product/ERP5/bootstrap/erp5_core/bt/template_interface_id_list b/product/ERP5/bootstrap/erp5_core/bt/template_interface_id_list new file mode 100644 index 0000000000000000000000000000000000000000..ee918611f0170678de6b885cf3d489edc75051f4 --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/bt/template_interface_id_list @@ -0,0 +1,7 @@ +interface.erp5.IDivergenceController +interface.erp5.IExpandable +interface.erp5.IMovementCollection +interface.erp5.IMovementCollectionDiff +interface.erp5.IMovementCollectionUpdater +interface.erp5.IRule +interface.erp5.ISimulationMovement \ No newline at end of file diff --git a/product/ERP5/bootstrap/erp5_core/bt/template_mixin_id_list b/product/ERP5/bootstrap/erp5_core/bt/template_mixin_id_list index 3f7d83cdf8dd05615e0cf6be49892a93240bebd0..d8a18da1dbff5e6b75403600b55019d1476c10fb 100644 --- a/product/ERP5/bootstrap/erp5_core/bt/template_mixin_id_list +++ b/product/ERP5/bootstrap/erp5_core/bt/template_mixin_id_list @@ -1 +1,4 @@ +mixin.erp5.ExplainableMixin +mixin.erp5.MovementCollectionUpdaterMixin +mixin.erp5.RuleMixin mixin.erp5.SimulableMixin \ No newline at end of file diff --git a/product/ERP5/bootstrap/erp5_core/bt/template_module_component_id_list b/product/ERP5/bootstrap/erp5_core/bt/template_module_component_id_list new file mode 100644 index 0000000000000000000000000000000000000000..179c1047c6843f93d0ea5f283083130478b3851a --- /dev/null +++ b/product/ERP5/bootstrap/erp5_core/bt/template_module_component_id_list @@ -0,0 +1,3 @@ +module.erp5.ExpandPolicy +module.erp5.MovementCollectionDiff +module.erp5.MovementGroup \ No newline at end of file diff --git a/product/ERP5/bootstrap/erp5_core/bt/template_tool_component_id_list b/product/ERP5/bootstrap/erp5_core/bt/template_tool_component_id_list index d8e627b7b183ac8bbd5233a81279fbb7acd23a6c..9816bb52f551741fc3a68482861f4268139be08e 100644 --- a/product/ERP5/bootstrap/erp5_core/bt/template_tool_component_id_list +++ b/product/ERP5/bootstrap/erp5_core/bt/template_tool_component_id_list @@ -1,2 +1,3 @@ tool.erp5.CallableTool -tool.erp5.DiffTool \ No newline at end of file +tool.erp5.DiffTool +tool.erp5.SimulationTool \ No newline at end of file diff --git a/product/ERP5/dtml/explainSimulationTool.dtml b/product/ERP5/dtml/explainSimulationTool.dtml deleted file mode 100644 index 35b6e7108faacf6ffa4b86db3f82ff6e7808c69e..0000000000000000000000000000000000000000 --- a/product/ERP5/dtml/explainSimulationTool.dtml +++ /dev/null @@ -1,4 +0,0 @@ -<dtml-var manage_page_header> -<dtml-var manage_tabs> - -<dtml-var manage_page_footer> diff --git a/product/ERP5/tests/testInventoryAPI.py b/product/ERP5/tests/testInventoryAPI.py index 91230a771150273f463e8aa1d41897df7f74f9b1..aab2a61d6b04a7851bc10545fdb03aca08a1a99e 100644 --- a/product/ERP5/tests/testInventoryAPI.py +++ b/product/ERP5/tests/testInventoryAPI.py @@ -43,8 +43,6 @@ from MySQLdb import ProgrammingError from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase from Products.ERP5Type.tests.utils import reindex -from Products.ERP5.Tool.SimulationTool import MYSQL_MIN_DATETIME_RESOLUTION - class InventoryAPITestCase(ERP5TypeTestCase): """Base class for Inventory API Tests {{{ @@ -2913,6 +2911,7 @@ class TestInventoryCacheTable(InventoryAPITestCase): min_lag = cache_lag / 2 self.NOW = now = DateTime(DateTime().strftime("%Y-%m-%d %H:%M:%S UTC")) self.CACHE_DATE = cache_date = now - min_lag + from erp5.component.tool.SimulationTool import MYSQL_MIN_DATETIME_RESOLUTION self.LAST_CACHED_MOVEMENT_DATE = last_cached_movement_date = \ cache_date - MYSQL_MIN_DATETIME_RESOLUTION # First movement, won't be into cache diff --git a/product/ERP5Type/interfaces/divergence_message.py b/product/ERP5Type/interfaces/divergence_message.py index ffb8ccaf616710353e762e2987ebebdbfadbed04..8b4550aef500ef4fd340a3129ac238c7607426bc 100644 --- a/product/ERP5Type/interfaces/divergence_message.py +++ b/product/ERP5Type/interfaces/divergence_message.py @@ -27,7 +27,7 @@ # ############################################################################## """ -Products.ERP5Type.interfaces.divergence_message +erp5.component.interface.IDivergenceMessage """ from zope.interface import Interface