dodil/erp-brick-manufacturing
DODIL ERP brick: manufacturing
src/brick_manufacturing/domain/mrp.py
"""MRP-lite (MFG-08, MFG-17): net what is needed week by week, round the shortfall up to a lot, and say what each order needs.
Pure and deterministic - the same demand, stock and supply give the same planned orders. Demand is netted by week (Monday to Sunday):
projected stock starts at what is on hand less the safety stock; a week whose need is not covered gets one planned order, its shortfall
rounded up through product (`round_up`), and that order's quantity then covers the following weeks until the stock runs short again. A
planned order is due one lead time (a function of ITS quantity) before the first need in its week, never before today.
"""
from datetime import date, timedelta
from decimal import Decimal
from typing import Callable
def monday(d: date) -> date:
return d - timedelta(days=d.weekday())
def low_level_codes(edges: dict) -> dict:
"""product -> its low-level code: 0 for a product no BOM uses as a component, else 1 + the deepest parent's. `edges`: parent -> {components}.
Iterative (a product is final once every parent is final); a cycle cannot reach here (the BOM write refuses it)."""
parents: dict = {}
for parent, comps in edges.items():
parents.setdefault(parent, set())
for c in comps:
parents.setdefault(c, set()).add(parent)
code: dict = {}
pending = set(parents)
while pending:
progressed = False
for p in sorted(pending):
if all(q in code for q in parents[p]):
code[p] = 1 + max((code[q] for q in parents[p]), default=-1)
pending.discard(p)
progressed = True
if not progressed: # defensive: never loop forever on bad data
for p in pending:
code[p] = 0
break
return code
def plan_product(needs: list, supply: list, on_hand: Decimal, safety: Decimal, today: date, lead_days: Callable[[Decimal, date], int],
round_up: Callable[[Decimal], Decimal]) -> list:
"""needs/supply: [(date, quantity)]. Returns [{need_date, due, quantity, covers: [{week, need}], short}] in week order."""
weeks: dict = {}
for d, q in needs:
w = weeks.setdefault(monday(d), {"need": Decimal("0"), "first": d, "supply": Decimal("0")})
w["need"] += q
w["first"] = min(w["first"], d)
for d, q in supply:
w = weeks.setdefault(monday(d), {"need": Decimal("0"), "first": d, "supply": Decimal("0")})
w["supply"] += q
projected = on_hand - safety
out = []
for wk in sorted(weeks):
w = weeks[wk]
projected += w["supply"]
short = w["need"] - projected
if short > 0 and w["need"] > 0:
qty = round_up(short)
due = max(today, w["first"] - timedelta(days=lead_days(qty, w["first"])))
out.append({"need_date": due, "due": due, "first_need": w["first"], "quantity": qty, "short": short,
"week": wk, "need": w["need"]})
projected += qty
projected -= w["need"]
return out