from typing import List, Sequence from .dates_days_between import days_between from .inventory_expiry_fefo_types import FefoPick, PickLine, StockBatch def _whole_units(name: str, value: int) -> None: if isinstance(value, bool) or not isinstance(value, int) or value < 0: raise ValueError("%s must be a whole number, not negative, received %r" % (name, value)) def fefo_pick(batches: Sequence[StockBatch], quantity: int, as_at: str, min_shelf_life_days: int) -> FefoPick: """Pick ``quantity`` units earliest expiry first, from batches with at least ``min_shelf_life_days`` of life left on ``as_at``. Short stock is a shortfall, not an error. """ _whole_units("quantity", quantity) _whole_units("minShelfLifeDays", min_shelf_life_days) dated = [] for batch in batches: _whole_units('quantity of batch "%s"' % batch.batch, batch.quantity) dated.append((days_between(as_at, batch.expiry), batch)) # sorted() is stable, so equal expiry dates keep their order. dated = sorted(dated, key=lambda pair: pair[0]) lines: List[PickLine] = [] skipped: List[str] = [] remaining = quantity for life, batch in dated: if batch.quantity == 0: continue if life < min_shelf_life_days: skipped.append(batch.batch) continue if remaining == 0: continue take = min(remaining, batch.quantity) lines.append(PickLine(batch=batch.batch, expiry=batch.expiry, quantity=take)) remaining -= take return FefoPick(lines=lines, picked=quantity - remaining, shortfall=remaining, skipped=skipped)