Commit a44109d1 authored by Cédric Le Ninivin's avatar Cédric Le Ninivin

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 5a2121da
......@@ -30,6 +30,7 @@
from AccessControl import ClassSecurityInfo
from Products.ERP5Type.Globals import InitializeClass
from Products.ERP5Type import Permissions, PropertySheet
from Products.ERP5Type.DateUtils import addToDate
from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5Type.Core.Predicate import Predicate
from erp5.component.document.Amount import Amount
......@@ -40,6 +41,8 @@ from erp5.component.module.ExplanationCache import _getExplanationCache
from DateTime import DateTime
from Acquisition import aq_parent, aq_inner
from math import ceil
class CollectError(Exception): pass
class MatrixError(Exception): pass
class DuplicatedPropertyDictKeysError(Exception): pass
......@@ -290,6 +293,234 @@ class BuilderMixin(XMLObject, Amount, Predicate):
break
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):
"""
Returns a list of simulation movements (or something similar to
......@@ -442,10 +673,12 @@ class BuilderMixin(XMLObject, Amount, Predicate):
Create a new delivery in case where a builder may not update
an existing one.
"""
return delivery_module.newContent(
delivery = delivery_module.newContent(
portal_type=self.getDeliveryPortalType(),
created_by_builder=1,
activate_kw=activate_kw)
created_by_builder=1)
if self.getPortalObject().portal_workflow.isTransitionPossible(delivery, "auto_plan"):
delivery.autoPlan()
return delivery
def _processDeliveryGroup(self, delivery_module, movement_group_node,
collect_order_list, movement_group_node_list=None,
......@@ -504,20 +737,32 @@ class BuilderMixin(XMLObject, Amount, Predicate):
if force_update and delivery is None and len(delivery_to_update_list):
delivery = delivery_to_update_list[0]
created = False
if delivery is None:
if not self.isDeliveryCreatable():
raise SelectMethodError('No updatable delivery found with %s for %s' \
% (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,
movement_group_node.getMovementList(),
activate_kw)
created = True
# Put properties on delivery
self._setUpdated(delivery, 'delivery')
if property_dict:
property_dict.setdefault('edit_order', ('stop_date', 'start_date'))
delivery._edit(reindex_object=1, **property_dict)
if created:
delivery.immediateReindexObject()
# Then, create delivery line
for grouped_node in movement_group_node.getGroupList():
self._processDeliveryLineGroup(
......
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