General Definitions
- the simulation represents the prevision, and the delivery represents the decision. The system must allow users to make a decision that does not fit with the prevision, and then adapt the prevision
- an Order or a Delivery must have at most one causality in the simulation
- a Simulation Movement must have at most one delivery link. Multiple Simulation Movements can link to a single Delivery Movement
- once a simulation tree that starts at the top level is completely delivered, it can be deleted (this expects all movements in this simulation tree to be linked to delivered deliveries, and this is definitely no meant to be automated, but can be thought as a way to save some space in a huge database, by removing old and useless simulation movements)
Root Applied Rules
- when it is desired to have previsions generated from an Order or a Delivery, a new root_applied_rule must be created at the root of the simulation. This rule will then be linked to the Order or Delivery using the causality relation
- the purpose of the rule used as a root_applied_rule is to "bootstrap" the simulation: expanding a root_applied_rule should "copy" the movements of the Order or Delivery to the simulation, or update their counterpart in the simulation if it already exists, but it should not modify submovements of the root_applied_rule that are linked to another Delivery
- a root_applied_rule must have a causality which explains why the prevision was generated
- a root_applied_rule can modify its child movements only if they are in future states. Once they are in reserved or current states (which generally means they are linked to a confirmed delivery), solvers must be used instead.
the frozen state
- a movement is said to be frozen if it's either:
- attached to a delivery which is in a "final" state (stopped, delivered, cancelled)
- attached to a delivery which was frozen by user action (currently, this has not been used in practice yet, but it could be useful if we want to freeze movements related to a proforma invoice, for example)
- once a movement is frozen:
- expanding the parent of this movement must not modify it
a TargetSolver doing backtracking cannot modify or traverse it. The divergence must be solved at this movement's level (by creating a compensation movement, or a new branch in the simulation)
Rules
- rules are Predicates, finding rules that can be applied to a movement is done by searching for Predicates that match the simulation movement
- rules are historized. Rules have a reference and version properties. When two rules with the same reference are returned by a Predicate search, the one with the highest version must be used
if a rule has to change in time (like a VAT ratio), multiple rules have to be created, using for example start_date_range_min and start_date_range_max to determine which rule should apply, based on properties of the SimulationMovement (copied form the Order or Delivery)
- rules must be configurable:
- the test method of a rule is a ZODB python script, that returns True if the rule can be applied to a simulation movement, and False otherwise. This test method can use any part of the system it has access to to determine if the rule can be applied
the behaviour of the isDivergent method is definable at the rule level, using DivergenceTesters
the expand method can use Predicates to adapt its behaviour (as InvoiceTransactionRule)
expand
AppliedRule.expand creates or modifies child movements according to the behaviour defined in the class of Rule and expands them (until no movement is created)
SimulationMovement.expand searches for applicable rules in the context of the movement, attaches the ones that pass the test and expand them (until no rule is attached). More detailed explanation follows:
for all already applied rules if the rule does not apply anymore (test returns False)\ and none of its submovments is linked to a delivery remove the rule for all applicable rules (found by Predicate search) if no rule with the same portal type is already applied apply the rule- applied_rule.expand cannot modify a frozen movement. it must still keep the children of the applied_rule consistent with its parents (keep the rule stable), for instance by creating a new movement to compensate the difference
- expand must be stable. Calling expand multiple times or at different places must produce the same result, given that the context is the same (deliveries and rules haven't changes in between)
divergence
- isDivergent must be able to detect a divergent movement. A divergence is a difference between a delivery movement and the simulation movements attached to it
- For the divergence testers to work properly, we need to copy all properties and categories that can cause a divergence to the simulation, and not just acquire them from the order or delivery
- isDivergent must be configurable and the system must be able to tell which property is causing the divergence and propose adapted solvers
- idea: we could use Constraints (allows to reuse code, configure isDivergent, propose adapted solvers, unify the causality workflow)
- idea: stop using one workflow transaction per solver, and pull all the solver specific logic out of the workflow (transactions and scripts) and only keep a generic solver caller inside
- idea: the behaviour of isDivergent should be defined by the parent applied_rule. Logically, all movements created by one single rule can only diverge all the same way (properties we want to consider or ignore are the same)
- the isStable method must be able to detect the stability of an applied_rule. A rule is said to be stable if calling expand on it does not modify the simulation (ie: its parent movement is consistent with the "sum" of its child movements)
- while backtracking, once we get to a stable rule, it is not necessary to continue backtracking, as it is not supposed to modify anything anylonger
- the profit_quantity property allows to solve a quantity divergence locally:
- isDivergent compares simulation.corrected_quantity and delivery.quantity
- movement.corrected_quantity = movement.quantity - movement.profit_quantity
- it should only be used when the user accepts the fact that some units were definitely gained or lost during processing (delivery, production ...)
- for dates and prices, there is no equivalent, both parties have to agree on them
the delivery_error property allows to ignore the FloatRounding issue. When a solver wants to backtrack a decision by applying a ratio on the quantity of movements, it has to guess the rounding error and store it to delivery_error (so that it's accounted by isDivergent, for example)
DeliverySolver
- Role: a delivery solver has to distribute the decision on the attached simulation_movements. It does this by modifying the delivery_ratio value
- simulation.quantity = delivery.quantity * simulation.delivery_ratio (same for order and order_ratio)
- the sum of delivery_ratio for all simulation movements related to one delivery movement must be equal to 1.0
TargetSolver
- Role: a target solver has to modify the simulation movements to make them converge with the related delivery movements, and maybe backtrack the changes to the parent simulation movements
a TargetSolver must stop before a frozen movement, without modifying it. It cannot go further upwards, and must be able to solve the divergence locally:
- either by creating a new movement to compensate the difference,
- either by going up the tree and repercuting (?) the divergence up to a certain (?) point, and then creating a new root_applied_rule to solve the divergence form upwards and finally by linking the movements created by expanding this new rule to the deliveries we want to make converge
- solvers must guarantee stability. Expanding after calling a Solver should not require modifying what has just been fixed
the TargetSolver doesn't have to continue backtracking if the parent rule of the movement it just modified is still stable
- the solver has to know when to stop backtracking. This decision might be complex in some cases and could rely on customer specific scripts or user decision
Examples of properties that can cause a divergence and possible solutions
quantity & delivery_ratio
- create a compensation at the same level (split)
- accept the difference by using profit_quantity (for instance, if some resources have been "broken" during production)
- backtrack the ratio between the new and old quantities
- create a new root_applied_rule and expand it to a compensation movement, link this movement to the delivery, and adapt the delivery_ratio to make the first tree converge
start_date & stop_date
- define a error range within which we consider that the movement is not divergent
- accept decision, without backtracking (how to make the rule stable? rule.expand can accept to ignore the difference if the movement is not divergent)
- accept decision, then backtrack (for example to delay production if delivery was delayed)
- price
- accept decision, without backtracking (how to make the rule stable? rule.expand can accept to ignore the difference if the movement is not divergent)
- accept and backtrack as long as the resource is the same
resource & variation
- create one movement that sends back the wrong resource and another one that sends the right one
- accept the new resource (but then, how to make stable?)
- optional: backtrack until the resource is consumed (for production environment)
- optional: backtrack until product's delivery (we must then be able to 'reverse' the transformation to determine which resource we can/will produce from the one we consumed)
- optional: backtrack until the resource is consumed (for production environment)
- the resource is replaced by some item aggregate
- accept and backtrack as long as the resource is the same (not consumed or transformed)
source & destination
- create a movement that sends the resource from the wrong node to the right one
- accept the new source or destination and backtrack? it can cause problems to the production or delivery
- delivery movement added or deleted
- added: create a new root_applied_rule and link it to the delivery movement
- deleted: either:
- delete all simulation movements linked to it
- set quantity to 0 on simulation movements linked to it
- set simulation_state to cancelled movements linked to it