Commit 89ee1adf authored by Jean-Paul Smets's avatar Jean-Paul Smets

Refactored interfaces related to business process management and simulation....

Refactored interfaces related to business process management and simulation. Nexedi refactoring should include "6th element" idea if possible, once it is better understood. 

git-svn-id: https://svn.erp5.org/repos/public/erp5/sandbox/amount_generator@35543 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent b470122c
...@@ -91,8 +91,6 @@ class BusinessPath(Path, Predicate): ...@@ -91,8 +91,6 @@ class BusinessPath(Path, Predicate):
zope.interface.implements(interfaces.ICategoryAccessProvider, zope.interface.implements(interfaces.ICategoryAccessProvider,
interfaces.IArrowBase, interfaces.IArrowBase,
interfaces.IBusinessPath, interfaces.IBusinessPath,
interfaces.IBusinessBuildable,
interfaces.IBusinessCompletable,
interfaces.IPredicate, interfaces.IPredicate,
) )
......
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved.
# Jean-Paul Smets-Solanes <jp@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.
#
##############################################################################
"""
Products.ERP5.interfaces.business_buildable
"""
from zope.interface import Interface
class IBusinessBuildable(Interface):
"""Business Buildable interface specification
This interface is implemented by Business Path as part
of Business Process management. It can be used to
check which path in a business process can be built
and to trigger the build process for a given path.
"""
def isBuildable(explanation):
"""Returns True if any of the related Simulation Movement
is buildable and if the predecessor state is completed.
'explanation' is the Order or Item or Document which is the
cause of a root applied rule in the simulation
"""
def isPartiallyBuildable(explanation):
"""Returns True if any of the related Simulation Movement
is buildable and if the predecessor state is partially completed.
'explanation' is the Order or Item or Document which is the
cause of a root applied rule in the simulation
NOTE (JPS): It is not sure whether this method will ever
be used.
"""
def build(explanation):
"""Builds all related movements in the simulation
using the builders defined on the Business Path
'explanation' is the Order or Item or Document which is the
cause of a root applied rule in the simulation
"""
# -*- coding: utf-8 -*-
############################################################################## ##############################################################################
# #
# Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved. # Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved.
...@@ -29,49 +30,81 @@ ...@@ -29,49 +30,81 @@
Products.ERP5.interfaces.business_path Products.ERP5.interfaces.business_path
""" """
from Products.ERP5.interfaces.business_completable import IBusinessCompletable from zope.interface import Interface
from Products.ERP5.interfaces.business_buildable import IBusinessBuildable
class IBusinessPath(IBusinessCompletable, IBusinessBuildable): class IBusinessPath(Interface):
"""Business Path interface specification """Business Path interface specification
IBusinessPath provides a method to calculate the completion
date of existing movements based on business path properties.
It also provides methods to determine whether all related simulation
movements related to a given explanation are completed, partially
completed or frozen. Finally, it provides a method to invoke
delivery builders for all movements related to a given explanation.
""" """
def getExpectedStartDate(task, predecessor_date=None):
"""Returns the expected start date for this
path based on the task and provided predecessor_date.
'task' is a document which follows the ITaskGetter interface def getMovementCompletionDate(self, movement):
(getStartDate, getStopDate) and defined the reference dates """Returns the date of completion of the movemnet
for the business process execution based on paremeters of the business path. This complete date can be
the start date, the stop date, the date of a given workflow transition
on the explaining delivery, etc.
'predecessor_date' can be provided as predecessor date and movement -- a Simulation Movement
to override the date provided in the task
""" """
def getExpectedStopDate(task, predecessor_date=None): def isCompleted(explanation):
"""Returns the expected stop date for this """returns True if all related simulation movements for this explanation
path based on the task and provided predecessor_date. document are in a simulation state which is considered as completed
according to the configuration of the current business path.
Completed means that it is possible to move to next step
of Business Process. This method does not check however whether previous
trade states of a given business process are completed or not.
Use instead IBusinessPathProcess.isBusinessPathCompleted for this purpose.
'task' is a document which follows the ITaskGetter interface explanation -- the Order, Order Line, Delivery or Delivery Line which
(getStartDate, getStopDate) and defined the reference dates implicitely defines a simulation subtree and a union
for the business process execution business process.
'predecessor_date' can be provided as predecessor date and NOTE: simulation movements can be completed (ex. in 'started' state) but
to override the date provided in the task not yet frozen (ex. in 'delivered' state).
""" """
def getRelatedSimulationMovementValueList(explanation): def isPartiallyCompleted(explanation):
"""Returns list of values of Simulation Movements related to self """returns True if some related simulation movements for this explanation
and delivery document are in a simulation state which is considered as completed
according to the configuration of the current business path.
Completed means that it is possible to move to next step
of Business Process. This method does not check however whether previous
trade states of a given business process are completed or not.
Use instead IBusinessPathProcess.isBusinessPathCompleted for this purpose.
explanation - any document related to business path - which bootstraped explanation -- the Order, Order Line, Delivery or Delivery Line which
process or is related to build of one paths implicitely defines a simulation subtree and a union
business process.
""" """
def isMovementRelatedWithMovement(movement_value_a, movement_value_b): def isFrozen(explanation):
"""Checks if self is parent or children to movement_value """returns True if all related simulation movements for this explanation
document are in a simulation state which is considered as frozen
according to the configuration of the current business path.
Frozen means that simulation movement cannot be modified.
This method does not check however whether previous
trade states of a given business process are completed or not.
Use instead IBusinessPathProcess.isBusinessPathCompleted for this purpose.
explanation -- the Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree and a union
business process.
NOTE: simulation movements can be frozen (ex. in 'stopped' state) but
not yet completed (ex. in 'delivered' state).
"""
This logic is Business Process specific for Simulation Movements, as def build(explanation):
sequence of Business Process is not related appearance of Simulation Tree """Builds all related movements in the simulation using the builders
defined on the Business Path
movement_value_a, movement_value_b - movements to check relation between explanation -- the Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree and a union
business process.
""" """
\ No newline at end of file
# -*- coding: utf-8 -*-
############################################################################## ##############################################################################
# #
# Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved. # Copyright (c) 2009-2010 Nexedi SA and Contributors. All Rights Reserved.
# Jean-Paul Smets-Solanes <jp@nexedi.com> # Jean-Paul Smets-Solanes <jp@nexedi.com>
# #
# WARNING: This program as such is intended to be used by professional # WARNING: This program as such is intended to be used by professional
...@@ -29,66 +30,316 @@ ...@@ -29,66 +30,316 @@
Products.ERP5.interfaces.business_process Products.ERP5.interfaces.business_process
""" """
from Products.ERP5.interfaces.business_completable import IBusinessCompletable from zope.interface import Interface
from Products.ERP5.interfaces.business_buildable import IBusinessBuildable
class IBusinessProcess(IBusinessCompletable, IBusinessBuildable): class IBusinessPathProcess(Interface):
"""Business Process interface specification """Business Path Process interface specification
IBusinessPathProcess defines Business Process APIs related
to Business Path completion status and expected completion dates.
"""
def getBusinessPathValueList(trade_phase=None, predecessor=None, successor=None):
"""Returns the list of contained Business Path documents
trade_phase -- filter by trade phase
predecessor -- filter by trade state predecessor
successor -- filter by trade state successor
"""
def isBusinessPathCompleted(explanation, business_path):
"""Returns True if given Business Path document
is completed in the context of provided explanation.
explanation -- an Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree
business_path -- a Business Path document
"""
def getExpectedBusinessPathCompletionDate(explanation, business_path):
"""Returns the expected completion date of given Business Path document
in the context of provided explanation.
explanation -- an Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree
business_path -- a Business Path document
""" """
def getBuildablePathValueList(explanation):
def getExpectedBusinessPathStartAndStopDate(explanation, business_path):
"""Returns the expected start and stop dates of given Business Path
document in the context of provided explanation.
explanation -- an Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree
business_path -- a Business Path document
"""
class IBuildableBusinessPathProcess(Interface):
"""Buildable Business Path Process interface specification
IBuildableBusinessPathProcess defines an API to build
simulation movements related to business pathj in the context
of a given explanation.
"""
def getBuildableBusinessPathValueList(explanation):
"""Returns the list of Business Path which are buildable """Returns the list of Business Path which are buildable
by taking into account trade state dependencies between
Business Path.
'explanation' is the Order or Item or Document which is the explanation -- an Order, Order Line, Delivery or Delivery Line which
cause of a root applied rule in the simulation implicitely defines a simulation subtree
""" """
def getCompletedStateValueList(explanation): def isBusinessPathBuildable(explanation, business_path):
"""Returns the list of Business States which are completed """Returns True if any of the related Simulation Movement
is buildable and if the predecessor trade state is completed.
'explanation' is the Order or Item or Document which is the explanation -- an Order, Order Line, Delivery or Delivery Line which
cause of a root applied rule in the simulation implicitely defines a simulation subtree
business_path -- a Business Path document
"""
def isBusinessPatPartiallyBuildable(explanation, business_path):
"""Returns True if any of the related Simulation Movement
is buildable and if the predecessor trade state is partially completed.
explanation -- an Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree
business_path -- a Business Path document
"""
class ITradeStateProcess(Interface):
"""Trade State Process interface specification
ITradeStateProcess defines Business Process APIs related
to Trade State completion status and expected completion dates.
ITradeStateProcess APIs recursively browse trade states and business
path to provide completion status and expected completion dates.
For example, a complete trade state is a trade state for
which all predecessor trade states are completed and for
which all business path applicable to the given explanation
are also completed.
"""
def getTradeStateList():
"""Returns list of all trade_state of this Business Process
by looking at successor and predecessor values of contained
Business Path.
explanation -- an Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree
"""
def getSuccessorTradeStateList(explanation, trade_state):
"""Returns the list of successor states in the
context of given explanation. This list is built by looking
at all successor of business path involved in given explanation
and which predecessor is the given trade_phase.
explanation -- an Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree
trade_state -- a Trade State category
""" """
def getPartiallyCompletedStateValueList(explanation): def getPredecessorTradeStateList(explanation, trade_state):
"""Returns the list of Business States which are partially """Returns the list of predecessor states in the
completed context of given explanation. This list is built by looking
at all predecessor of business path involved in given explanation
and which sucessor is the given trade_phase.
explanation -- an Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree
'explanation' is the Order or Item or Document which is the trade_state -- a Trade State category
cause of a root applied rule in the simulation
""" """
def getLatestCompletedStateValue(explanation): def getCompletedTradeStateList(explanation):
"""Returns a completed Business State with no succeeding """Returns the list of Trade States which are completed
completed Business Path in the context of given explanation.
'explanation' is the Order or Item or Document which is the explanation -- an Order, Order Line, Delivery or Delivery Line which
cause of a root applied rule in the simulation implicitely defines a simulation subtree
""" """
def getLatestPartiallyCompletedStateValue(explanation): def getPartiallyCompletedTradeStateList(explanation):
"""Returns a partially completed Business State with no """Returns the list of Trade States which are partially
succeeding partially completed Business Path completed in the context of given explanation.
'explanation' is the Order or Item or Document which is the explanation -- an Order, Order Line, Delivery or Delivery Line which
cause of a root applied rule in the simulation implicitely defines a simulation subtree
""" """
def getLatestCompletedStateValueList(explanation): def getLatestCompletedTradeStateList(explanation):
"""Returns all completed Business State with no succeeding """Returns the list of completed trade states which predecessor
completed Business Path states are completed and for which no successor state
is completed in the context of given explanation.
'explanation' is the Order or Item or Document which is the explanation -- an Order, Order Line, Delivery or Delivery Line which
cause of a root applied rule in the simulation implicitely defines a simulation subtree
""" """
def getLatestPartiallyCompletedStateValueList(explanation): def getLatestPartiallyCompletedTradeState(explanation):
"""Returns all partially completed Business State with no """Returns the list of completed trade states which predecessor
succeeding partially completed Business Path states are completed and for which no successor state
is partially completed in the context of given explanation.
'explanation' is the Order or Item or Document which is the explanation -- an Order, Order Line, Delivery or Delivery Line which
cause of a root applied rule in the simulation implicitely defines a simulation subtree
"""
def isTradeStateCompleted(explanation, trade_state):
"""Returns True if all predecessor trade states are
completed and if no successor trade state is completed
in the context of given explanation.
explanation -- an Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree
trade_state -- a Trade State category
"""
def isTradeStatePartiallyCompleted(explanation, trade_state):
"""Returns True if all predecessor trade states are
completed and if no successor trade state is partially completed
in the context of given explanation.
explanation -- an Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree
trade_state -- a Trade State category
"""
def getExpectedTradeStateCompletionDate(explanation, trade_state):
"""Returns the date at which the give trade state is expected
to be completed in the context of given explanation.
explanation -- an Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree
trade_state -- a Trade State category
"""
class ITradePhaseProcess(Interface):
"""Trade Phase Process interface specification
ITradePhaseProcess defines Business Process APIs related
to Trade Phase completion status and expected completion dates.
Unlike ITradeStateProcess, ITradePhaseProcess APIs related to completion
do not take into account relations between trade states and
business path.
For example, a completed trade phase is a trade phase for which all
business path applicable to the given explanation are completed.
It does not matter whether the predecessor trade state of related
business path is completed or not.
""" """
def getTradePhaseList(): def getTradePhaseList():
"""Returns list of all trade_phase of this Business Process """Returns list of all trade_phase of this Business Process
by looking at trade_phase values of contained Business Path.
"""
def getCompletedTradePhaseList(explanation):
"""Returns the list of Trade Phases which are completed
in the context of given explanation.
explanation -- an Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree
"""
def getPartiallyCompletedTradePhaseList(explanation):
"""Returns the list of Trade Phases which are partially completed
in the context of given explanation.
explanation -- an Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree
"""
def isTradePhaseCompleted(explanation, trade_phase):
"""Returns True all business path with given trade_phase
applicable to given explanation are completed.
explanation -- an Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree
trade_phase -- a Trade Phase category
"""
def isTradePhasePartiallyCompleted(explanation, trade_phase):
"""Returns True at least one business path with given trade_phase
applicable to given explanation is partially completed
or completed.
explanation -- an Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree
trade_phase -- a Trade Phase category
"""
def getExpectedTradePhaseCompletionDate(explanation, trade_phase):
"""Returns the date at which the give trade phase is expected
to be completed in the context of given explanation, taking
into account the graph of date constraints defined by business path
and business states.
explanation -- an Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree
trade_phase -- a Trade Phase category
"""
def getRemainingTradePhaseList(business_path, trade_phase_list=None):
"""Returns the list of remaining trade phases which to be achieved
as part of a business process. This list is calculated by analysing
the graph of business path and trade states, starting from a given
business path. The result if filtered by a list of trade phases. This
method is useful mostly for production and MRP to manage a distributed
supply and production chain.
business_path -- a Business Path document
trade_phase_list -- if provided, the result is filtered by it after
being collected - ???? useful ? XXX-JPS ?
NOTE: explanation is not involved here because we consider here that
self is the result of asUnionBusinessProcess and thus only contains
applicable Business Path to a given simulation subtree. Since the list
of remaining trade phases does not depend on exact values in the
simulation, we did not include the explanation. However, this makes the
API less uniform.
"""
class IBusinessProcess(IBusinessPathProcess, IBuildableBusinessPathProcess,
ITradeStateProcess, ITradePhaseProcess, ):
"""Business Process interface specification.
Business Process APIs are used to manage the completion status,
the completion dates, the start date and stop date, and trigger
build process of a complex simulation process in ERP5.
"""
def isBusinessProcessCompleted(explanation):
"""Returns True is all applicable Trade States and Trade Phases
are completed in the context of given explanation.
explanation -- an Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree
"""
def getExpectedCompletionDate(explanation):
"""Returns the expected date at which all applicable Trade States and
Trade Phases are completed in the context of given explanation.
explanation -- an Order, Order Line, Delivery or Delivery Line which
implicitely defines a simulation subtree
""" """
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################## ##############################################################################
# #
# Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved. # Copyright (c) 2010 Nexedi SA and Contributors. All Rights Reserved.
# Jean-Paul Smets-Solanes <jp@nexedi.com> # Jean-Paul Smets-Solanes <jp@nexedi.com>
# #
# WARNING: This program as such is intended to be used by professional # WARNING: This program as such is intended to be used by professional
...@@ -27,66 +27,66 @@ ...@@ -27,66 +27,66 @@
# #
############################################################################## ##############################################################################
""" """
Products.ERP5.interfaces.business_completable Products.ERP5.interfaces.business_path
""" """
from zope.interface import Interface from zope.interface import Interface
class IBusinessCompletable(Interface): class IExplainable(Interface):
"""Business Completable interface specification """Explainable interface specification
This interface is implemented by Business Path and Business IExplainable defines the notion of Explanation in ERP5 simulation.
States as part of Business Process Management. It can be Explanation was initially introduced to provide better indexing of
used to check whether a path or a state is completed, or movements in stock and movement tables. Thanks to explanation, it is
partially completed. possible to relate unbuilt simulation movements (ex. planned sourcing)
to a root explanation (ex. a production order). This is used
in the inventory browser user interface to provide an explanation
for each simulated movement which is not yet built. Explanation of
simulation movements are sometimes used to calculate efficiently aggregated
quantities and prices of all simulation movements which are part of the
same simulation tree.
TODO: make sure interface can support simulation movements IExplainable is implemented by all simulation movements.
"""
def isCompleted(explanation):
"""True if all related simulation movements for this explanation
document are delivered and in a simulation state which is considered
as finished.
Completed means that it is possible to move to next step of Business Process Explanations in ERP5 are also used in another meaning, as a way to calculate
efficiently aggregated quantities and prices of movements in a Delivery.
The current interface is unrelated to this meaning.
""" """
def getExplanationValueList():
def isPartiallyCompleted(explanation): """Returns the list of deliveries of parent simulation
"""True if some related simulation movements for this explanation movements. The first item in the list is the immediate
document are delivered and in simulation state which is considered explanation value. The last item in the list is the root
as finished. explanation.
""" """
def isFrozen(explanation): def getRootExplanationValue():
"""True if all related simulation movements for this explanation """Returns the delivery of the root simulation
are frozen. movement.
Frozen means that simulation movement cannot be modified.
NOTE: simulation movements can be frozen (ex. in stopped state) but
not yet completed (ex. in delivered state).
""" """
def getExpectedCompletionDate(task): def getImmediateExplanationValue():
"""Returns the date at which the given state is expected to """Returns the delivery of the first parent simulation
be completed, based on the start_date and stop_date of which has a delivery.
the given task document.
'task' is a document which follows the ITaskGetter interface
(getStartDate, getStopDate)
""" """
def getExpectedCompletionDuration(task): def getExplanationLineValueList():
"""Returns the duration at which the state is expected to be completed, """Returns the list of delivery lines of parent simulation
based on the start_date and stop_date of the explanation document. movements. The first item in the list is the immediate
explanation value. The last item in the list is the root
explanation.
"""
'task' is a document which follows the ITaskGetter interface def getRootExplanationLineValue():
(getStartDate, getStopDate) """Returns the delivery line of the root simulation
movement.
""" """
def getRemainingTradePhaseList(explanation, trade_phase_list=None): def getImmediateExplanationLineValue():
"""Returns the list of remaining trade phases which to be done on the """Returns the delivery line of the first parent simulation
explanation. which has a delivery.
"""
trade_phase_list -- if provided, the result is filtered by it after # Compatibility API
being collected def getExplanationUid():
"""Returns the UID of the root explanation
""" """
\ No newline at end of file
...@@ -33,9 +33,9 @@ Products.ERP5.interfaces.simulation_movement ...@@ -33,9 +33,9 @@ Products.ERP5.interfaces.simulation_movement
from Products.ERP5.interfaces.property_recordable import IPropertyRecordable from Products.ERP5.interfaces.property_recordable import IPropertyRecordable
from Products.ERP5.interfaces.movement import IMovement from Products.ERP5.interfaces.movement import IMovement
from Products.ERP5.interfaces.divergence_controller import IDivergenceController from Products.ERP5.interfaces.divergence_controller import IDivergenceController
from Products.ERP5.interfaces.business_completable import IBusinessCompletable from Products.ERP5.interfaces.explainable import IExplainable
class ISimulationMovement(IMovement, IPropertyRecordable, IDivergenceController, IBusinessCompletable): class ISimulationMovement(IMovement, IPropertyRecordable, IDivergenceController, IExplainable):
"""Simulation Movement interface specification """Simulation Movement interface specification
The ISimulationMovement interface introduces in addition The ISimulationMovement interface introduces in addition
...@@ -82,15 +82,42 @@ class ISimulationMovement(IMovement, IPropertyRecordable, IDivergenceController, ...@@ -82,15 +82,42 @@ class ISimulationMovement(IMovement, IPropertyRecordable, IDivergenceController,
""" """
def getDeliveryQuantity(): def getDeliveryQuantity():
""" """ Returns quantity which was actually shipped, taking
Returns quantity which was actually shipped, taking
into account the errors of the simulation fixed by into account the errors of the simulation fixed by
the delivery: the delivery:
quantity + delivery_error quantity + delivery_error
""" """
def isDeletable(): def isDeletable():
"""Returns True if this simulation movement can be deleted, False
else. A simulation movement can be deleted if all its children
can be deleted of if it has no child.
"""
def isCompleted():
"""Returns True if the simulation state of this simulation movement
is considered as completed by the business path which this simulation
movement relates to through causality base category.
NOTE: simulation movements can be completed (ex. in started state) but
not yet frozen (ex. in delivered state). This is the case for example
of accounting movements which are completed as soon as they are posted
(to allow next steps in the business process) but can still be modified
are thus not yet frozen.
""" """
Returns True is this simumlation can be deleted, False
else. def isFrozen():
"""Returns True if the simulation state of this simulation movement
is considered as frozen by the business path which this simulation
movement relates to through causality base category.
Frozen means that simulation movement cannot be modified anylonger.
NOTE: simulation movements can be frozen (ex. in stopped state) but
not yet completed (ex. in delivered state). This is the case of
sales purchase movements which are frozen as soon they are received
because they should not be modified any longer but are only completed
once some extra steps bring them to delivered state, thus allowing the
generation of planned purchase invoice.
""" """
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment