Commit b5aa3d1a authored by Cédric Le Ninivin's avatar Cédric Le Ninivin Committed by Your Name

builder: Add generateMovementListForStockOptimisationFromSupplyDefinition

buidler: Add tmp fix/workaround on movement order

builder: Use periodicity to determinate start date

builder mixin: Take into account supply date range before create movement

builder: Hackish immediate reindex of order to work for parallel build

builder: use dedicated script to evaluate min stock

builder: autoPlan only if possible

builder: Reference of order line is reference of resource line

builder: include order delay and effective date

builder: fixup add missing part for effective date

builder: Tweak and fix supply builder

builder: Fixup supply builder

builder: immediate reindex delivery on creation

builder: use _edit on delivery to only reindex modification

builder: Supply builder work with period

builder: Supply builder improved

builder: use flow unit for min stock calculation

builder:  Supply Builder Fix future inventory for parts not activated yet

builder: Supply builder do not create movement for resources that won't be consumed in the future
parent ff9c75fd
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from Products.ERP5Type.Globals import InitializeClass from Products.ERP5Type.Globals import InitializeClass
from Products.ERP5Type import Permissions, PropertySheet from Products.ERP5Type import Permissions, PropertySheet
from Products.ERP5Type.DateUtils import addToDate
from Products.ERP5Type.XMLObject import XMLObject from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5Type.Core.Predicate import Predicate from Products.ERP5Type.Core.Predicate import Predicate
from Products.ERP5.Document.Amount import Amount from Products.ERP5.Document.Amount import Amount
...@@ -40,6 +41,8 @@ from Products.ERP5.ExplanationCache import _getExplanationCache ...@@ -40,6 +41,8 @@ from Products.ERP5.ExplanationCache import _getExplanationCache
from DateTime import DateTime from DateTime import DateTime
from Acquisition import aq_parent, aq_inner from Acquisition import aq_parent, aq_inner
from math import ceil
class CollectError(Exception): pass class CollectError(Exception): pass
class MatrixError(Exception): pass class MatrixError(Exception): pass
class DuplicatedPropertyDictKeysError(Exception): pass class DuplicatedPropertyDictKeysError(Exception): pass
...@@ -285,6 +288,234 @@ class BuilderMixin(XMLObject, Amount, Predicate): ...@@ -285,6 +288,234 @@ class BuilderMixin(XMLObject, Amount, Predicate):
break break
return movement_list return movement_list
def generateMovementListForStockOptimisationFromSupplyDefinition(
self, supply,
from_date=None,
group_by_node=1, allow_intermediate_negative_stock=True,
**kw):
from Products.ERP5Type.Document import newTempMovement
portal = self.getPortalObject()
from_date = DateTime().earliestTime()
# Initiate Conditions taken from Yusei T. Original Script
# XXX to be cleaned
min_inventory = supply.getMinStock()
resource_value = supply.getResourceValue()
default_quantity_unit_value = resource_value.getDefaultQuantityUnitValue()
order_quantity_unit_value = supply.getOrderQuantityUnitValue()
flow_quantity_unit_value = supply.getFlowQuantityUnitValue()
time_quantity_unit_value = supply.getTimeQuantityUnitValue()
time_second_ratio = resource_value.getQuantityUnitDefinitionRatio(portal.portal_categories.quantity_unit.time.second)
min_delay = supply.getMinDelay()
max_delay = supply.getMaxDelay()
min_order_delay = supply.getMinOrderDelay()
max_order_delay = supply.getMaxOrderDelay()
min_order = supply.getMinOrderQuantity()
max_order = supply.getMaxOrderQuantity()
# Initiate conversions
# XXX To be cleaned
min_delay_second = 0
max_delay_second = 0
min_order_delay_second = 0
max_order_delay_second = 0
min_order_in_default_quantity_unit = 0
max_order_in_default_quantity_unit = float('inf')
default_quantity_unit_relative_url = default_quantity_unit_value.getCategoryRelativeUrl()
if time_quantity_unit_value is not None:
time_second_conversion_ratio = resource_value.getQuantityUnitDefinitionRatio(time_quantity_unit_value) / time_second_ratio
if min_delay:
min_delay_second = min_delay * time_second_conversion_ratio
if max_delay:
max_delay_second = max_delay * time_second_conversion_ratio
if min_order_delay:
min_order_delay_second = min_order_delay * time_second_conversion_ratio
if max_delay:
max_order_delay_second = max_order_delay * time_second_conversion_ratio
if order_quantity_unit_value is not None:
order_quantity_unit_relative_url = order_quantity_unit_value.getCategoryRelativeUrl()
order_quantity_unit_default_quantity_unit_conversion_ratio = resource_value.convertQuantity(1, order_quantity_unit_relative_url, default_quantity_unit_relative_url)
if min_order:
min_order_in_default_quantity_unit = min_order * order_quantity_unit_default_quantity_unit_conversion_ratio
if max_order:
max_order_in_default_quantity_unit = max_order * order_quantity_unit_default_quantity_unit_conversion_ratio
default_quantity_unit_order_quantity_unit_conversion_ratio = 1 / order_quantity_unit_default_quantity_unit_conversion_ratio
else:
default_quantity_unit_order_quantity_unit_conversion_ratio = 1
if flow_quantity_unit_value is not None:
flow_quantity_unit_relative_url = flow_quantity_unit_value.getCategoryRelativeUrl()
flow_quantity_unit_default_quantity_unit_conversion_ratio = resource_value.convertQuantity(1, flow_quantity_unit_relative_url, default_quantity_unit_relative_url)
default_quantity_unit_flow_quantity_unit_conversion_ratio = 1 / flow_quantity_unit_default_quantity_unit_conversion_ratio
else:
default_quantity_unit_flow_quantity_unit_conversion_ratio = 1
def getPreviousValidDate(date):
# We suppose the system has not been configured to handled per hour commands
return DateTime(supply.getNextPeriodicalDate(
date.earliestTime(),
# Hackish and dangerous
next_start_date=date.earliestTime(),
factor=-1)).earliestTime()
# Function to define the minimal quantity to be ordered
def minimalQuantity(quantity, date):
# Initiate variables to match original script from Yusei T.
# XXX To be cleaned
conversion_ratio = default_quantity_unit_order_quantity_unit_conversion_ratio
delay_second = max_delay_second or min_delay_second or 0
limit_date = getPreviousValidDate(date)
start_date = getPreviousValidDate(
addToDate(limit_date, second=-delay_second)
)
stop_date = addToDate(start_date, second=delay_second).earliestTime()
order_delay_second = max_order_delay_second or min_order_delay_second or 0
effective_date = addToDate(start_date, second=-order_delay_second)
order_quantity = ceil(quantity * conversion_ratio)
quantity = order_quantity / conversion_ratio
return order_quantity, order_quantity_unit_value, effective_date, start_date, stop_date, quantity
resource_portal_type = resource_value.getPortalType()
def newMovement(effective_date, start_date, stop_date, quantity, quantity_unit):
# Create temporary movement
# Do not handle variation and Item for now
movement = newTempMovement(portal, "temp")
movement.edit(
resource_value=resource_value,
destination_value=supply.getDestinationValue(),
destination_section_value=supply.getDestinationSectionValue(),
source_value=supply.getSourceValue(),
source_section_value=supply.getSourceSectionValue(),
resource_portal_type=resource_portal_type,
quantity_unit_value=quantity_unit,
quantity=quantity,
effective_date=effective_date,
start_date=start_date,
stop_date=stop_date,
reference=resource_value.getReference(),
)
return movement
movement_list = []
# Remove the Past
previous_movement_list = portal.portal_catalog(
portal_type=self.getDeliveryLinePortalType(),
simulation_state="auto_planned",
strict_resource_uid=supply.getResourceUid(),
#strict_source_uid=supply.getSourceUid(),
#strict_source_section_uid=supply.getSourceSectionUid(),
#strict_destination_uid=supply.getDestinationUid(),
#strict_destination_section_uid=supply.getDestinationSectionUid(),
parent_delivery_start_date={'query': (supply.getStartDateRangeMin(), supply.getStartDateRangeMax()),
'range':"minmax"},
)
#previous_movement_list= []
for brain in previous_movement_list:
brain.getObject().setQuantity(0)
# Prepare history list to work with
history_list = resource_value.Resource_getInventoryHistoryList(
from_date=from_date,
node_uid=supply.getDestinationUid()
# XXX This should be bound to a stard and stop date
)
#self.log("history_list len: %s" % len(history_list))
# We only consider resources that have consumption movements in
# the future, for those who don't we do not build anything.
has_consumption_movement = False
for date, inventory, quantity, portal_type in history_list:
if quantity < 0:
has_consumption_movement = True
break
if not has_consumption_movement:
return []
# evaluate future inventory at date
future_inventory_to_date = portal.portal_simulation.getFutureInventory(
to_date=from_date,
resource_uid=resource_value.getUid(),
node_uid=supply.getDestinationUid(),
)
# Prepare period_list
limit_date = getPreviousValidDate(from_date)
limit_date_list = []
last_date = getPreviousValidDate(history_list[-1][0])
while limit_date <= last_date:
limit_date_list.append(limit_date)
limit_date = DateTime(supply.getNextPeriodicalDate(
limit_date,
# Hackish and dangerous
next_start_date=limit_date)).earliestTime()
#self.log(limit_date_list)
# Create a movement per period
for period_start_date in limit_date_list:
# Prepare history list of the current period
next_period_start_date = DateTime(supply.getNextPeriodicalDate(
period_start_date,
next_start_date=period_start_date)).earliestTime()
period_history_list = []
while history_list and history_list[0][0] < next_period_start_date:
period_history_list.append(history_list.pop(0))
# Using period history list calculate min stock
min_inventory = self.Base_evaluateMinInventoryForSupplyAtDate(
supply=supply,
history_list=period_history_list,
at_date=period_start_date,
conversion_ratio=default_quantity_unit_flow_quantity_unit_conversion_ratio,
)
quantity = 0
if future_inventory_to_date < min_inventory: # SKU
quantity = min_inventory - future_inventory_to_date
ordered_quantity, ordered_unit, effective_date, start_date, delivery_date, quantity = minimalQuantity(quantity, period_start_date)
# XXX CLN This is very naive, it has to be optimized
if start_date > supply.getStartDateRangeMax():
break
if start_date < supply.getStartDateRangeMin():
# As we are going to need to go further in time to check if new Movements are needed
# we need to keep inventory correct
future_inventory_to_date += quantity
for date, total_inventory, quantity, portal_type in period_history_list:
future_inventory_to_date += quantity
continue
#self.log("at %s min: %s, inventory:%s, quantity:%s" % (period_start_date, min_inventory, future_inventory_to_date, quantity))
if quantity != 0:
self.log("Week %s Will order %s at %s for period %s" % (delivery_date.week(), quantity, delivery_date, period_start_date))
movement_list.append(
newMovement(
effective_date,
start_date,
delivery_date,
ordered_quantity,
ordered_unit
)
)
# calculate inventory at the end of the period
future_inventory_to_date += quantity
for date, total_inventory, quantity, portal_type in period_history_list:
future_inventory_to_date += quantity
#return []
return movement_list
def _searchMovementList(self, **kw): def _searchMovementList(self, **kw):
""" """
Returns a list of simulation movements (or something similar to Returns a list of simulation movements (or something similar to
...@@ -434,10 +665,12 @@ class BuilderMixin(XMLObject, Amount, Predicate): ...@@ -434,10 +665,12 @@ class BuilderMixin(XMLObject, Amount, Predicate):
Create a new delivery in case where a builder may not update Create a new delivery in case where a builder may not update
an existing one. an existing one.
""" """
return delivery_module.newContent( delivery = delivery_module.newContent(
portal_type=self.getDeliveryPortalType(), portal_type=self.getDeliveryPortalType(),
created_by_builder=1, created_by_builder=1)
activate_kw=activate_kw) if self.getPortalObject().portal_workflow.isTransitionPossible(delivery, "auto_plan"):
delivery.autoPlan()
return delivery
def _processDeliveryGroup(self, delivery_module, movement_group_node, def _processDeliveryGroup(self, delivery_module, movement_group_node,
collect_order_list, movement_group_node_list=None, collect_order_list, movement_group_node_list=None,
...@@ -488,20 +721,32 @@ class BuilderMixin(XMLObject, Amount, Predicate): ...@@ -488,20 +721,32 @@ class BuilderMixin(XMLObject, Amount, Predicate):
if force_update and delivery is None and len(delivery_to_update_list): if force_update and delivery is None and len(delivery_to_update_list):
delivery = delivery_to_update_list[0] delivery = delivery_to_update_list[0]
created = False
if delivery is None: if delivery is None:
if not self.isDeliveryCreatable(): if not self.isDeliveryCreatable():
raise SelectMethodError('No updatable delivery found with %s for %s' \ raise SelectMethodError('No updatable delivery found with %s for %s' \
% (self.getPath(), movement_group_node_list)) % (self.getPath(), movement_group_node_list))
# if total quantity is 0 no need to create anything
total_quantity = 0
for movement in movement_group_node.getMovementList():
total_quantity += movement.getQuantity()
if total_quantity == 0:
return delivery_list
delivery = self._createDelivery(delivery_module, delivery = self._createDelivery(delivery_module,
movement_group_node.getMovementList(), movement_group_node.getMovementList(),
activate_kw) activate_kw)
created = True
# Put properties on delivery # Put properties on delivery
self._setUpdated(delivery, 'delivery') self._setUpdated(delivery, 'delivery')
if property_dict: if property_dict:
property_dict.setdefault('edit_order', ('stop_date', 'start_date')) property_dict.setdefault('edit_order', ('stop_date', 'start_date'))
delivery._edit(reindex_object=1, **property_dict) delivery._edit(reindex_object=1, **property_dict)
if created:
delivery.immediateReindexObject()
# Then, create delivery line # Then, create delivery line
for grouped_node in movement_group_node.getGroupList(): for grouped_node in movement_group_node.getGroupList():
self._processDeliveryLineGroup( self._processDeliveryLineGroup(
......
...@@ -542,7 +542,10 @@ class SimulableMixin(Base): ...@@ -542,7 +542,10 @@ class SimulableMixin(Base):
if applied_rule is None: if applied_rule is None:
applied_rule = self._createRootAppliedRule() applied_rule = self._createRootAppliedRule()
expand_root = applied_rule is not None expand_root = applied_rule is not None
activate_kw = {'tag': 'build:'+self.getPath()} activate_kw = {
'priority': 4,
'tag': 'build:'+self.getPath(),
}
if expand_root: if expand_root:
applied_rule.expand(activate_kw=activate_kw) applied_rule.expand(activate_kw=activate_kw)
else: else:
......
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