dodil/erp-brick-inventory
DODIL ERP brick: inventory
src/brick_inventory/domain/removal.py
"""Which lot to take: first-expiring first (FEFO), oldest received first (FIFO), or the walking order (manual).
An expired lot is never proposed, picked or promised from its expiry date; a lot with less shelf life left than the customer
accepts is not proposed either (a warehouse manager may allow it, with a reason, recorded). Stock in quality, blocked,
consignment-at-a-customer or subcontractor places is never proposed.
"""
from dataclasses import dataclass
from datetime import date
from decimal import Decimal
from typing import Optional
from brick_inventory.domain.stock import expired, shelf_days_left
from brick_inventory.domain.values import ZERO
FAR = date(9999, 12, 31)
LONG_AGO = date(1900, 1, 1)
@dataclass(frozen=True)
class Candidate:
location_id: str
lot_code: str
stock_type: str
owner: str
quantity_base: Decimal
expires_on: Optional[date] = None
received_on: Optional[date] = None
pick_sequence: int = 0
def eligible(c: Candidate, today: date, min_shelf_life_days: Optional[int], allow_expired: bool = False) -> bool:
if c.stock_type != "unrestricted" or c.quantity_base <= 0:
return False
if expired(c.expires_on, today) and not allow_expired:
return False
if min_shelf_life_days and c.expires_on is not None:
left = shelf_days_left(c.expires_on, today)
if left is not None and left < min_shelf_life_days:
return False
return True
def order_key(strategy: str):
if strategy == "fefo":
return lambda c: (c.expires_on or FAR, c.received_on or LONG_AGO, c.pick_sequence, c.location_id, c.lot_code)
if strategy == "fifo":
return lambda c: (c.received_on or LONG_AGO, c.pick_sequence, c.location_id, c.lot_code)
return lambda c: (c.pick_sequence, c.location_id, c.lot_code)
def propose(candidates: list, need: Decimal, strategy: str, today: date, min_shelf_life_days: Optional[int] = None,
allow_expired: bool = False) -> tuple:
"""([(candidate, quantity)], short): the lots and bins to take `need` from, in order, and what could not be found."""
pool = sorted((c for c in candidates if eligible(c, today, min_shelf_life_days, allow_expired)), key=order_key(strategy))
out, left = [], need
for c in pool:
if left <= 0:
break
take = min(left, c.quantity_base)
out.append((c, take))
left -= take
return out, max(left, ZERO)