199 lines
8.0 KiB
Python
199 lines
8.0 KiB
Python
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
import datetime
|
|
from collections import defaultdict
|
|
|
|
from trytond.model import ModelSQL, ModelView
|
|
from trytond.pool import Pool
|
|
from trytond.tools import grouped_slice
|
|
from trytond.transaction import Transaction
|
|
|
|
|
|
class ShipmentInternal(ModelSQL, ModelView):
|
|
__name__ = 'stock.shipment.internal'
|
|
|
|
@classmethod
|
|
def generate_internal_shipment(cls, clean=True, warehouses=None):
|
|
"""
|
|
Generate internal shipments to meet order points defined on
|
|
non-warehouse location.
|
|
|
|
If clean is set, it will remove all previous requests.
|
|
"""
|
|
pool = Pool()
|
|
OrderPoint = pool.get('stock.order_point')
|
|
Location = pool.get('stock.location')
|
|
Product = pool.get('product.product')
|
|
Date = pool.get('ir.date')
|
|
User = pool.get('res.user')
|
|
Move = pool.get('stock.move')
|
|
LeadTime = pool.get('stock.location.lead_time')
|
|
company = User(Transaction().user).company
|
|
if not company:
|
|
return
|
|
|
|
implicit_location_domain = [
|
|
['OR',
|
|
('provisioning_location', '!=', None),
|
|
('overflowing_location', '!=', None),
|
|
],
|
|
('type', '=', 'storage'),
|
|
]
|
|
clean_request_domain = [
|
|
('state', '=', 'request'),
|
|
('company', '=', company.id),
|
|
]
|
|
order_point_domain = [
|
|
('type', '=', 'internal'),
|
|
('company', '=', company.id),
|
|
]
|
|
if not warehouses:
|
|
warehouses = Location.search([
|
|
('type', '=', 'warehouse'),
|
|
])
|
|
else:
|
|
implicit_location_domain.append(('parent', 'child_of', warehouses))
|
|
clean_request_domain.append(
|
|
['OR',
|
|
('to_location.parent', 'child_of', warehouses),
|
|
('from_location.parent', 'child_of', warehouses),
|
|
])
|
|
order_point_domain.append(
|
|
('location.parent', 'child_of', warehouses))
|
|
|
|
today = Date.today()
|
|
lead_time = LeadTime.get_max_lead_time()
|
|
|
|
if clean:
|
|
cls.delete(cls.search(clean_request_domain))
|
|
|
|
# fetch quantities on order points
|
|
order_points = OrderPoint.search(order_point_domain)
|
|
id2product = {}
|
|
product2op = {}
|
|
id2location = {}
|
|
for op in order_points:
|
|
id2product[op.product.id] = op.product
|
|
product2op[
|
|
(op.storage_location.id, op.product.id)
|
|
] = op
|
|
id2location[op.storage_location.id] = op.storage_location
|
|
implicit_locations = Location.search(implicit_location_domain)
|
|
id2location.update({l.id: l for l in implicit_locations})
|
|
location_ids = list(id2location.keys())
|
|
|
|
def get_pbl(start, end, grouping_filter):
|
|
with Transaction().set_context(
|
|
forecast=True, stock_date_start=start, stock_date_end=end):
|
|
return Product.products_by_location(
|
|
location_ids, with_childs=True,
|
|
grouping_filter=grouping_filter)
|
|
|
|
# ordered by ids to speedup reduce_ids in products_by_location
|
|
if implicit_locations:
|
|
products = Product.search([
|
|
('type', 'in', ['goods', 'assets']),
|
|
], order=[('id', 'ASC')])
|
|
product_ids = [p.id for p in products]
|
|
pbl = get_pbl(None, today, None)
|
|
else:
|
|
product_ids = list(id2product.keys())
|
|
product_ids.sort()
|
|
pbl = defaultdict(int)
|
|
for sub_product_ids in grouped_slice(product_ids):
|
|
pbl.update(get_pbl(None, today, (list(sub_product_ids),)))
|
|
|
|
shipments = []
|
|
date = today
|
|
end_date = date + lead_time
|
|
current_qties = pbl.copy()
|
|
while date <= end_date:
|
|
# Create a list of moves to create
|
|
moves = {}
|
|
for location in id2location.values():
|
|
for product_id in product_ids:
|
|
qty = current_qties.get((location.id, product_id), 0)
|
|
op = product2op.get((location.id, product_id))
|
|
if op:
|
|
min_qty, max_qty = op.min_quantity, op.max_quantity
|
|
target_qty = op.target_quantity
|
|
prov_location = op.provisioning_location
|
|
over_location = op.overflowing_location
|
|
elif (location
|
|
and (location.provisioning_location
|
|
or location.overflowing_location)):
|
|
target_qty = 0
|
|
min_qty = 0 if location.provisioning_location else None
|
|
max_qty = 0 if location.overflowing_location else None
|
|
prov_location = location.provisioning_location
|
|
over_location = location.overflowing_location
|
|
else:
|
|
continue
|
|
|
|
change_qty = 0
|
|
if min_qty is not None and qty < min_qty:
|
|
from_loc = prov_location.id
|
|
to_loc = location.id
|
|
change_qty = target_qty - qty
|
|
elif max_qty is not None and qty > max_qty:
|
|
from_loc = location.id
|
|
to_loc = over_location.id
|
|
change_qty = qty - target_qty
|
|
|
|
if change_qty:
|
|
key = (from_loc, to_loc, product_id)
|
|
moves[key] = change_qty
|
|
current_qties[(from_loc, product_id)] -= change_qty
|
|
current_qties[(to_loc, product_id)] += change_qty
|
|
|
|
# Group moves by {from,to}_location
|
|
to_create = {}
|
|
for key, qty in moves.items():
|
|
from_location, to_location, product = key
|
|
to_create.setdefault(
|
|
(from_location, to_location), []).append((product, qty))
|
|
# Create shipments and moves
|
|
for locations, moves in to_create.items():
|
|
from_location, to_location = locations
|
|
shipment = cls(
|
|
from_location=from_location,
|
|
to_location=to_location,
|
|
planned_date=date,
|
|
state='request',
|
|
)
|
|
shipment_moves = []
|
|
for move in moves:
|
|
product_id, qty = move
|
|
product = id2product.setdefault(
|
|
product_id, Product(product_id))
|
|
shipment_moves.append(Move(
|
|
from_location=from_location,
|
|
to_location=to_location,
|
|
planned_date=date,
|
|
product=product,
|
|
quantity=qty,
|
|
unit=product.default_uom,
|
|
company=company,
|
|
))
|
|
shipment.moves = shipment_moves
|
|
shipment.planned_start_date = (
|
|
shipment.on_change_with_planned_start_date())
|
|
shipments.append(shipment)
|
|
date += datetime.timedelta(1)
|
|
|
|
# Update quantities with next moves
|
|
if implicit_locations:
|
|
for key, qty in get_pbl(date, date, None).items():
|
|
current_qties[key] += qty
|
|
else:
|
|
for sub_product_ids in grouped_slice(product_ids):
|
|
for key, qty in get_pbl(
|
|
date, date, (list(sub_product_ids),)).items():
|
|
current_qties[key] += qty
|
|
|
|
if shipments:
|
|
cls.save(shipments)
|
|
# Split moves through transit to get accurate dates
|
|
cls._set_transit(shipments)
|
|
return shipments
|