Documentation
¶
Overview ¶
Package scheduling is the production schedule solver.
It is a pure port of the knit-scheduling script (dashboard/apps/api/src/scripts/knit-scheduling-merz.ts): no database access, no clock, no randomness. Everything it needs is loaded up front into SolverInput, and Solve returns the plan. That boundary is what makes the parity gate against the script possible — the same input must produce the same plan.
The pipeline, in order:
measure per-item run rate, cost, lot size and machine affinity from history changeover calibrate the yarn-driven setup time model demand pool finished-goods demand back onto the constraint item policy EOQ, safety stock, reorder point, ABC class levelling the capacity-levelled (s,S) sweep across the horizon explode push the constraint plan downstream through the process DAG
Determinism ¶
The script leans on JavaScript's insertion-ordered maps and stable sort. Go's map iteration is randomized, so anything that iterates a map here MUST sort first or the plan will differ between identical runs. TestSolve_Deterministic guards this.
Index ¶
- Constants
- func AnalyzeDeliveryPerformance(outcomes []DeliveryOutcome, bucketOf func(time.Time) time.Time, asOf time.Time) (buckets []DeliveryPerformance, overall DeliveryPerformance)
- func AverageInputsAdded(batches []BatchMeasurement, stepInputs map[string]map[string]bool) float64
- func CeilWeeks(weeks float64) int
- func CountLots(quantity, lotUnits float64) int
- func EarliestPromiseWeek(itemID string, quantity float64, campaigns []Campaign, firm FirmSchedule, ...) (int, bool)
- func EstimateArrival(shipBy time.Time, transit *Transit, cals Calendars) (time.Time, bool)
- func MeanFloat(values []float64) float64
- func MixedStreamShare(customers []CustomerDemand, itemPolicy string) float64
- func ParseDayMask(mask string) ([dayMaskLength]bool, error)
- func ResolveDemand(in DemandInput) ([]ItemDemand, []AppliedOverride)
- func ResolveLeadTime(in LeadTimeInput) (days int, source string, ok bool)
- func SecondsPerUnitFromLaborTime(value float64, unitAbbreviation string) float64
- func SetupCost(avgMinutes, laborRate, overheadRate float64) float64
- func SplitIntoLots(quantity, lotUnits float64) []float64
- func StdDevFloat(values []float64) float64
- func SubtractBusinessDays(from time.Time, n int) time.Time
- type AllocationResult
- type AppliedOverride
- type AtRiskOrder
- type BacklogBucket
- type BatchMeasurement
- type Calendar
- func (c Calendar) AddDays(from time.Time, n int) (time.Time, bool)
- func (c Calendar) IsOpen(d time.Time) bool
- func (c Calendar) SnapBack(d time.Time) (snapped time.Time, moved int, ok bool)
- func (c Calendar) SnapForward(d time.Time) (snapped time.Time, moved int, ok bool)
- func (c Calendar) SubtractDays(from time.Time, n int) (time.Time, bool)
- type Calendars
- type Campaign
- type Changeover
- type ClassificationInput
- type Closure
- type Commitment
- type CommitmentBasis
- type CommitmentStep
- type CustomerDemand
- type DeliveryBreakdown
- type DeliveryBreakdownKey
- type DeliveryOutcome
- type DeliveryPerformance
- type DemandInput
- type DemandOverride
- type DerivedLine
- type Diagnostics
- type ExplosionCampaign
- type ExplosionInput
- type FinishedGood
- type FinishedGoodDemand
- type FinishedPolicy
- type FinishingDiagnostics
- type FinishingInput
- type FinishingItem
- type FinishingLine
- type FinishingResult
- type FinishingStep
- type FinishingSupply
- type FirmRequirement
- type FirmSchedule
- type FulfillmentResolution
- type ItemDemand
- type ItemMeasurement
- type ItemPolicy
- type LatenessBucket
- type LeadTimeInput
- type LevellingDiagnostics
- type LevellingItem
- type LevellingResult
- type LotDefault
- type LotResolutionInput
- type Machine
- type MonthlyDemand
- type OpenOrderLine
- type OrderAllocation
- type PinnedCampaign
- type PolicyInput
- type PolicyResolutionInput
- type ProductLineLot
- type ProductLineRef
- type Recommendation
- type RecommendationThresholds
- type Settings
- type SolverInput
- type SolverOutput
- type StepEdge
- type StepInfo
- type Transit
- type UncoveredRequirement
Constants ¶
const ( // ReasonLeadTimeInfeasible means the plant cannot produce inside the window customers were promised, so the stock has to exist before the order does. ReasonLeadTimeInfeasible = "lead_time_infeasible" // ReasonNoRecentDemand means nothing has sold for long enough that holding a buffer is holding dead stock. ReasonNoRecentDemand = "no_recent_demand" // ReasonSingleCustomer means effectively one customer buys this, and that customer is served to order. ReasonSingleCustomer = "single_customer" // ReasonLumpyDemand means demand arrives rarely and in wildly different sizes, which is the shape a safety stock sizes worst. ReasonLumpyDemand = "lumpy_demand" // ReasonSlowMovingHighValue means each unit is expensive and few sell, so the buffer ties up more money than the service it buys. ReasonSlowMovingHighValue = "slow_moving_high_value" // ReasonSteadyDemand means demand is regular enough to forecast, which is what a buffer is for. ReasonSteadyDemand = "steady_demand" )
Recommendation reasons. Each names the rule that decided, so a verdict is never a bare answer.
const ( DemandBasisTrailing12 = string(constants.ScheduleDemandBasisTrailing12) DemandBasisSeasonalEMA = string(constants.ScheduleDemandBasisSeasonalEMA) )
Demand basis codes, aliased from the shared enum so the engine cannot drift from the API contract.
const ( OverrideTypeAbsolute = string(constants.DemandOverrideAdjustmentAbsolute) OverrideTypeDeltaUnits = string(constants.DemandOverrideAdjustmentDeltaUnits) OverrideTypeDeltaPercent = string(constants.DemandOverrideAdjustmentDeltaPercent) )
Demand override types, aliased from the shared enum so the engine cannot drift from the API contract.
const ( OverrideScopeItem = string(constants.DemandOverrideScopeItem) OverrideScopeProductLine = string(constants.DemandOverrideScopeProductLine) OverrideScopeAccount = string(constants.DemandOverrideScopeAccount) )
Override scopes, aliased from the shared enum so the engine cannot drift from the API contract.
const ( PolicyMakeToStock = string(constants.FulfillmentPolicyMakeToStock) PolicyMakeToOrder = string(constants.FulfillmentPolicyMakeToOrder) )
Fulfillment policies, aliased from the shared enum so the engine cannot drift from the API contract.
const ( PolicySourceItem = string(constants.FulfillmentPolicySourceItem) PolicySourceProductLine = string(constants.FulfillmentPolicySourceProductLine) PolicySourceAccountDefault = string(constants.FulfillmentPolicySourceAccountDefault) )
Policy sources, aliased from the shared enum.
const ( LeadTimeSourceCustomer = string(constants.LeadTimeSourceCustomer) LeadTimeSourceParentCustomer = string(constants.LeadTimeSourceParentCustomer) LeadTimeSourceAccountGroup = string(constants.LeadTimeSourceAccountGroup) LeadTimeSourceAccount = string(constants.LeadTimeSourceAccount) LeadTimeSourceManual = string(constants.LeadTimeSourceManual) LeadTimeSourceOrderLeadTime = string(constants.LeadTimeSourceOrderLeadTime) LeadTimeSourceOrderShipBy = string(constants.LeadTimeSourceOrderShipBy) )
Lead-time sources, aliased from the shared enum so the engine cannot drift from the API contract.
const ( CommitmentStepBasis = string(constants.CommitmentStepBasis) CommitmentStepReceiveCalendar = string(constants.CommitmentStepReceiveCalendar) CommitmentStepCarrierTransit = string(constants.CommitmentStepCarrierTransit) CommitmentStepShipCalendar = string(constants.CommitmentStepShipCalendar) CommitmentStepPickupCutoff = string(constants.CommitmentStepPickupCutoff) )
Commitment step codes, naming which rule moved a date. Aliased for the same reason as the sources.
const ( LotSourceItemOverride = string(constants.ItemLotSourceItemOverride) LotSourceProductLine = string(constants.ItemLotSourceProductLine) LotSourceDownstreamProductLine = string(constants.ItemLotSourceDownstreamProductLine) LotSourceAccountDefault = string(constants.ItemLotSourceAccountDefault) )
Lot sources, aliased from the shared enum so the engine cannot drift from the API contract.
const ( AtRiskReasonPastDue = "past_due" AtRiskReasonUndated = "undated" AtRiskReasonShort = "short" )
At-risk reasons.
const FinishingSupplyLagWeeks = 1
FinishingSupplyLagWeeks is how long stage-one output waits before stage two can work it.
One week, and the reason is arithmetic rather than physical: a campaign planned in week w is produced across that week, so treating it as available to finishing in the same week would let the plan finish greige that is still on the needles. A configurable lag was considered and dropped — it would be a second knob over a lag the week granularity already fixes.
const LotRoundingTolerance = 1e-6
LotRoundingTolerance is the remainder below which a final short lot is folded into the previous one instead of becoming a batch of its own.
Planned quantities are decimals, so a campaign that is conceptually six whole doffs can arrive as 359.9999999. Without this a release would emit a seventh batch of a millionth of a unit.
const MaxExplosionDepth = 10
MaxExplosionDepth bounds the walk downstream of the constraint. Production graphs contain rework loops, so an unbounded walk would not terminate.
const MaxLotsPerCampaign = 500
MaxLotsPerCampaign bounds how many batches one campaign can be split into.
A misconfigured lot size — one unit per doff, say — would otherwise turn a single week into tens of thousands of batch rows and a production run nobody can work. The release refuses rather than writing them, so the bad setting surfaces as an error instead of as an unusable run.
const SolverVersion = "v1"
SolverVersion is stamped onto every generated schedule so a plan can be traced back to the algorithm that produced it. Bump it whenever the output changes for the same input.
const UnassignedLabel = "Unassigned"
UnassignedLabel is what a breakdown files outcomes under when they carry no value for that dimension. They are kept rather than dropped: orders with no sales rep are exactly the ones nobody is watching.
Variables ¶
This section is empty.
Functions ¶
func AnalyzeDeliveryPerformance ¶
func AnalyzeDeliveryPerformance(outcomes []DeliveryOutcome, bucketOf func(time.Time) time.Time, asOf time.Time) (buckets []DeliveryPerformance, overall DeliveryPerformance)
AnalyzeDeliveryPerformance turns a window of commitments into delivery performance, bucketed and overall.
Only orders carrying a commitment participate. An order with no ship-by date cannot be late, and counting it as on time would inflate the rate with orders nobody promised anything about — the count of those is reported separately by the caller so the exclusion is visible.
An order due in the window that has not shipped counts against on-time rather than being held back until it does. A promise not yet met is not a promise kept, and excluding open orders would let a plant with a growing late backlog report perfect delivery.
func AverageInputsAdded ¶
func AverageInputsAdded(batches []BatchMeasurement, stepInputs map[string]map[string]bool) float64
AverageInputsAdded measures how many new inputs a typical product transition introduces, which is what calibrates the changeover slope.
Campaigns are derived per machine by run-length-encoding the scan-ordered item sequence: consecutive batches of the same item are one campaign, so one changeover. stepInputs maps a production step to the set of input items it consumes.
func CeilWeeks ¶
CeilWeeks rounds a fractional week count up to whole weeks, which is how a schedule counts: a plan is written in weeks, and half a week of finishing still occupies one.
func CountLots ¶
CountLots reports how many batches SplitIntoLots would produce, without building them. Used to validate a release before any row is written.
func EarliestPromiseWeek ¶
func EarliestPromiseWeek(itemID string, quantity float64, campaigns []Campaign, firm FirmSchedule, onHandByItem map[string]float64, horizonWeeks int) (int, bool)
EarliestPromiseWeek is the first horizon week by which the plan could supply a quantity of an item that is not already committed to something else.
Capable-to-promise: what could still be promised, as opposed to what has been. Existing commitments are consumed first, because a date offered out of stock somebody else is already owed is not a date at all.
Returns false when the horizon cannot supply it, which is the honest answer — a plan that runs thirteen weeks cannot speak for the fourteenth.
func EstimateArrival ¶
func MixedStreamShare ¶
func MixedStreamShare(customers []CustomerDemand, itemPolicy string) float64
MixedStreamShare is how much of an item's demand comes from customers whose own policy disagrees with how the item is planned.
Reported rather than acted on. Policy is resolved per SKU, so a SKU sold to both a stocking distributor and a contract customer has one policy either way; this is the number that says the choice is uncomfortable, and it is what would justify splitting demand streams later.
func ParseDayMask ¶
ParseDayMask reads a seven-character days-of-week mask, Monday first, where '1' is an open day.
func ResolveDemand ¶
func ResolveDemand(in DemandInput) ([]ItemDemand, []AppliedOverride)
ResolveDemand computes the demand picture for every item, applying overrides.
Returns items sorted by ID and the list of overrides that actually changed a number.
func ResolveLeadTime ¶
func ResolveLeadTime(in LeadTimeInput) (days int, source string, ok bool)
ResolveLeadTime picks the number of days and names the rule that produced it.
The chain, most specific first: the customer, then its parent account, then the customer's account group, then the account default. Returns false only when nothing in the chain holds a usable value, which is a misconfiguration rather than a normal state — the account default is not nullable.
The parent outranks the group because it is the narrower statement: a group is a segment somebody sorted customers into, while a parent is the head office that negotiated the terms every one of its locations buys on. Putting the group first would mean a lead time set on a head office silently failed to reach exactly the grouped locations it was set for.
A negative value is skipped rather than honoured. It can only arrive from a hand-written database row (the write path rejects it), and a commitment that falls before the order was placed is worse than falling through to the next rule.
func SecondsPerUnitFromLaborTime ¶
SecondsPerUnitFromLaborTime converts a production step's labor time to seconds.
The unit abbreviation decides the scale. An unrecognized unit is treated as seconds, matching the script — a wrong guess here silently rescales every run hour in the plan, so the recognized set is kept explicit.
func SetupCost ¶
SetupCost is what one changeover costs, and therefore the "S" in the EOQ formula.
It uses a dedicated technician rate rather than the per-machine-hour production labor allocation: a tech works the single machine through the changeover, so the thin allocated rate understates it and EOQ would come out too small — meaning too many short campaigns and more changeovers than the floor can absorb.
func SplitIntoLots ¶
SplitIntoLots breaks a planned quantity into the batch sizes the floor actually runs.
Full lots come first and the remainder, if any, trails as one short lot. That ordering matters on the floor: the short doff is the one that gets cut when a week runs late, so it belongs at the end of the run rather than buried in the middle.
A non-positive lot size means the item is not lotted, so the campaign is one batch.
func StdDevFloat ¶
StdDevFloat returns the sample standard deviation, or zero for fewer than two observations.
func SubtractBusinessDays ¶
SubtractBusinessDays walks back n weekdays from a date, skipping weekends and nothing else.
Retained as the un-configured case of Calendar.SubtractDays: it is what every account got before calendars existed, and keeping it named separately keeps that baseline behaviour pinned by its own tests.
Types ¶
type AllocationResult ¶
type AllocationResult struct {
Allocations []OrderAllocation
Uncovered []UncoveredRequirement
}
AllocationResult is the whole answer: what each campaign is for, and what is not covered.
func AllocateCampaignsToOrders ¶
func AllocateCampaignsToOrders(campaigns []Campaign, firm FirmSchedule, onHandByItem map[string]float64) AllocationResult
AllocateCampaignsToOrders decides which campaign is building which order.
Earliest promise first, and supply consumed in the order it becomes available: stock on hand, then each campaign as it lands. A requirement can only be served by supply that exists by the week it is due — a campaign in week 6 cannot fill an order the constraint owed in week 2, and pretending otherwise is exactly how a plan reports itself achievable while the floor misses dates.
What is left after the walk is what the plan does not build in time, with the amount it does build recorded alongside. A partly-covered order is not a total miss and should not read as one.
Deterministic: both sides are sorted before the walk, so the same plan and order book always produce the same links.
type AppliedOverride ¶
type AppliedOverride struct {
OverrideID string `json:"override_id"`
ItemID string `json:"item_id"`
MonthStart time.Time `json:"month_start"`
Before float64 `json:"before"`
After float64 `json:"after"`
TypeCode string `json:"adjustment"`
ReasonCode string `json:"reason"`
}
AppliedOverride records an override that actually changed a number, so the plan can explain why it differs from history.
type AtRiskOrder ¶
type AtRiskOrder struct {
SalesOrderID string `json:"sales_order_id"`
SalesOrderNumber string `json:"sales_order_number"`
ItemID string `json:"item_id"`
SKU string `json:"sku"`
Units float64 `json:"units"`
DueWeek int `json:"due_week"`
// Reason is why the promise is at risk: `past_due` (the constraint stage needed to start before the horizon), `undated` (no commitment was ever recorded), or `short` (the plan projects less stock than the order needs in its week).
Reason string `json:"reason"`
}
AtRiskOrder is a commitment the plan does not meet, with the reason it does not.
type BacklogBucket ¶
type BacklogBucket struct {
// Label names the band; MinDaysLate and MaxDaysLate bound it, with MaxDaysLate zero meaning unbounded.
Label string
MinDaysLate int
MaxDaysLate int
OrderCount int
Units float64
}
BacklogBucket is one age band of orders already past their promise and still unshipped.
func AnalyzeBacklogAging ¶
func AnalyzeBacklogAging(outcomes []DeliveryOutcome, asOf time.Time) []BacklogBucket
AnalyzeBacklogAging groups orders that are past their promise and still unshipped into age bands.
Shipped orders are excluded however late they were: this is a queue of work still owed, not a record of past misses. How late a shipped order was belongs to the on-time rate.
type BatchMeasurement ¶
type BatchMeasurement struct {
BatchID string
ItemID string
SKU string
ScannedAt time.Time
Quantity float64
ProductionStepID string
MachineID string
MachineName string
UnitCost float64
// LaborTimeValue is the production step's labor time in LaborTimeUnit per unit.
LaborTimeValue float64
LaborTimeUnit string
LaborRate float64
OverheadRate float64
// RunCreatedAt is when the production run was opened; paired with ScannedAt it gives the observed lead time for this batch.
RunCreatedAt *time.Time
}
BatchMeasurement is one historical batch, as loaded from the database.
type Calendar ¶
type Calendar struct {
// OpenDays is indexed by ISO weekday, Monday at 0 through Sunday at 6.
OpenDays [dayMaskLength]bool
// Closures are dated shutdowns — holidays, shutdown weeks — keyed on the UTC-truncated date.
Closures map[time.Time]struct{}
}
Calendar is the set of days an operation runs: which weekdays it is open, less any dated closures.
One type covers all three parties to a shipment because they differ only in their days, never in how the days are counted — the plant tenders freight Monday to Thursday, the carrier moves it Monday to Friday, the customer's dock accepts it on its own days, and each of them shuts for its own holidays.
The zero value is closed every day and is not usable. Build one with NewCalendar or DefaultCalendar.
func DefaultCalendar ¶
func DefaultCalendar() Calendar
DefaultCalendar is Monday to Friday with nothing closed.
This is what the system did before calendars existed, and it is deliberately the fallback for an account that has configured nothing: adding the feature must not move a single date until somebody says which days they actually work.
func NewCalendar ¶
NewCalendar builds a calendar from a stored day mask and closure dates.
A mask with no open day is rejected rather than stored: it describes an operation that never runs, and every date resolved against it would walk the full snap-back limit before failing. Catching it here means the failure names the calendar instead of surfacing later as an order that cannot be committed.
func (Calendar) SnapBack ¶
SnapBack moves a date to the nearest open day at or before it, and reports how many days that took.
Always backward, never forward. A date the operation cannot act on has to resolve to one it can, and resolving forward would push a shipment past the day it was promised for — the whole failure this exists to prevent. Snapping back can only make an order leave early, which is safe.
ok is false when no open day exists within the snap-back limit, which means the calendar is closed indefinitely. Callers treat that as an unresolvable commitment rather than substituting a date.
func (Calendar) SnapForward ¶
func (Calendar) SubtractDays ¶
SubtractDays walks back n open days from a date.
Carriers quote transit in the days they actually move freight: a service quoted at three days and handed over on a Thursday delivers the following Tuesday, not Sunday. Counting calendar days instead would put every ship-by date up to two days late in exactly the cases that matter, since a week has more weekday-crossing lanes than not — and the same holds for a holiday the carrier's network is down.
n <= 0 returns the starting date, so a lane with no transit means ship-by is the delivery date. The starting date is not itself snapped: callers that need it on an open day snap it first, and conflating the two would hide which rule moved the date.
type Calendars ¶
type Calendars struct {
// Receive is the days the customer's dock accepts freight.
Receive Calendar
// Carrier is the days the carrier moves freight. Transit is counted in these.
Carrier Calendar
// Ship is the days the plant tenders freight.
Ship Calendar
// ShipCutoff is the local time freight has to be tendered by, as "15:00". Empty when the plant has not set one, in which case a ship-by date carries no time of day.
ShipCutoff string
// ShipLocation is the zone ShipCutoff is read in. Nil falls back to UTC.
ShipLocation *time.Location
}
Calendars are the day-sets a commitment has to respect, plus the zones its two time-of-day boundaries are read in.
Three separate calendars because the three parties genuinely differ: a plant may tender freight Monday to Thursday, its carrier moves Monday to Friday, and a customer's dock has its own days and its own holidays. Collapsing them into one weekday rule is what produced ship-by dates nobody could ship on.
func DefaultCalendars ¶
func DefaultCalendars() Calendars
DefaultCalendars is Monday to Friday for all three parties with no closures and no cutoff, which is how every date was computed before calendars existed.
type Campaign ¶
type Campaign struct {
ItemID string
SKU string
MachineID string
WeekIndex int
Units float64
Lots int
// LotUnits is the granularity the campaign was sized at, carried onto the plan so releasing the week to the floor splits into exactly the lots that were planned.
LotUnits float64
// LotUnitID is what the lot is counted in — pairs for sock greige, eaches for armsleeve greige. Without it a 60 on the plan cannot be reconciled with a 60 on the floor.
LotUnitID string
RunHours float64
}
Campaign is one planned production block: make this item, on this machine, in this week.
type Changeover ¶
type Changeover struct {
// contains filtered or unexported fields
}
Changeover models setup time as a function of how many inputs the next product introduces. Threading one extra yarn is quick; threading eight is not, and a flat average hides exactly the sequencing decision the planner is trying to make.
minutes(added) = clamp(min + slope * added, min, max)
The slope is calibrated from history rather than configured, so the modelled average lands on the number the floor actually reports.
func CalibrateChangeover ¶
func CalibrateChangeover(minMinutes, avgMinutes, maxMinutes, avgInputsAdded float64) Changeover
CalibrateChangeover solves for the slope that makes the model reproduce avgMinutes over the observed transitions.
avgInputsAdded is the mean number of new inputs across historical product transitions. With no history (or no variation) the slope is zero and every changeover costs the minimum — deliberately optimistic, but the alternative is inventing a slope from nothing.
Script: CO_SLOPE = (CO_AVG_MIN - CO_MIN_MIN) / avgYarnsAdded.
func (Changeover) Minutes ¶
func (c Changeover) Minutes(inputsAdded int) float64
Minutes returns the modelled changeover time for a transition that introduces inputsAdded new inputs.
func (Changeover) Slope ¶
func (c Changeover) Slope() float64
Slope exposes the calibrated minutes-per-added-input for diagnostics.
type ClassificationInput ¶
type ClassificationInput struct {
ItemID string
SKU string
// Monthly is the item's demand history, one entry per month with demand. Months with none may be absent; MonthsObserved is the window they were drawn from.
Monthly []float64
MonthsObserved int
// MonthsSinceLastSale is how long since anything sold. Negative means nothing ever has.
MonthsSinceLastSale int
AnnualDemand float64
UnitCost float64
// TotalProductionLeadTimeWeeks is constraint plus finishing: how long from deciding to build to having sellable stock.
TotalProductionLeadTimeWeeks float64
Customers []CustomerDemand
// CurrentPolicy is what the item is planned as today, so the recommendation can say whether anything would change.
CurrentPolicy string
}
ClassificationInput is everything the classifier needs about one item.
type Closure ¶
Closure is one dated shutdown on a calendar.
func USFederalHolidays ¶
USFederalHolidays returns the eleven federal holidays for a year, on the dates they are observed.
Computed rather than listed because most of them float: Martin Luther King Jr. Day is the third Monday in January, Thanksgiving the fourth Thursday in November, and a hardcoded table would be wrong from the year after it was written. The four fixed-date holidays are shifted to the adjacent weekday when they fall at a weekend, which is what the observance rule does and what carriers and banks actually follow — Independence Day on a Saturday closes the Friday before.
This is a seed, not a rule. It is written into an account's calendars as ordinary editable closures, because a plant that runs through Columbus Day or shuts the whole week of Christmas needs to say so, and nothing about the federal list is binding on a private factory.
type Commitment ¶
type Commitment struct {
ShipByDate time.Time
LeadTimeDays int
Source string
// TransitDays is the carrier transit subtracted to get ShipByDate, nil when transit was unknown or did not apply.
TransitDays *int
// TransitSource names where TransitDays came from, empty when TransitDays is nil.
TransitSource string
// ShipByCutoffAt is ShipByDate at the plant's pickup cutoff, as an instant. The date alone says which day freight has to leave; this says by when, which is the deadline a shop floor actually works to. Nil when the ship calendar carries no cutoff.
ShipByCutoffAt *time.Time
// CalendarAdjustmentDays is how many days the receiving and shipping calendars pulled ShipByDate back beyond what transit alone accounted for. Zero when every date already landed on an open day, and the one number that answers "why the 24th and not the 27th".
CalendarAdjustmentDays int
// Steps is the ordered derivation, one entry per rule that touched the date. Built on the way through so a preview and the stamped commitment are produced by the same walk and cannot disagree about how a date was reached.
Steps []CommitmentStep
}
Commitment is what an order promises: the date it is due to ship, how many days that was, and which rule decided.
All of it is stamped onto the order at issue rather than derived on read. A lead time is a rule that can be renegotiated; a commitment is a fact about a moment. Deriving the date later would let a customer moving from 30 days to 21 retroactively make last month's orders late, which is the opposite of what a contract is for. Transit and the calendar adjustment are stamped for the same reason: a carrier estimate refreshed next month, or a holiday added to a calendar in March, must not silently move a date the customer already has.
func ResolveCommitment ¶
func ResolveCommitment(issuedAt time.Time, basis CommitmentBasis, in LeadTimeInput, transit *Transit, cals Calendars) (Commitment, bool)
ResolveCommitment turns an order's issue date into its ship-by commitment.
Four bases, most specific first. A pinned ship date is taken as given. A promised delivery date has the customer's receiving days, the carrier's transit and the plant's shipping days worked back through it, because the difference between when a customer wants it and when it has to leave is the whole point of tracking transit. A per-order lead time replaces the standing chain. Failing all three, the chain itself decides.
Only the promised-delivery branch subtracts transit. Every other basis already names a *ship* date or a *ship* lead time — the days a customer waits before the order leaves — so deducting the journey again would charge for it twice.
Every branch ends on a day the plant actually ships. A ship-by date on a closed Friday or inside a shutdown week is not a deadline anybody can meet, and leaving it there is what made orders late on the day they were created.
Dates are calendar days truncated to the day, because ship_by_date is a DATE column and a commitment is a day rather than an instant. The one exception is ShipByCutoffAt, which exists precisely to name a time.
Returns false when no rule produced a date, or when a calendar is closed indefinitely. Leaving the commitment unstamped is the honest outcome: an order with no ship-by date reads as uncommitted, where a fabricated one would read as a promise nobody made.
type CommitmentBasis ¶
type CommitmentBasis struct {
// PromisedAt is a promised *delivery* date. The order has to leave early enough for the carrier to cover the lane, so this is the only basis transit is subtracted from.
PromisedAt *time.Time
// LeadTimeOverrideDays is this order's own lead time in days, replacing whatever the customer chain would have resolved to.
LeadTimeOverrideDays *int
// ShipByOverrideDate pins the ship date itself, bypassing transit and the receiving calendar.
ShipByOverrideDate *time.Time
}
CommitmentBasis is the explicit input somebody pinned on one order, displacing the standing lead-time chain.
At most one field may be set. The three are alternative answers to the same question and combining them has no meaning — a delivery date and a ship date cannot both be the thing being promised — so the service layer rejects more than one rather than inventing a precedence nobody could predict. The ordering ResolveCommitment applies is a defence against a bad row, not a documented rule.
func (CommitmentBasis) Count ¶
func (b CommitmentBasis) Count() int
Count is how many bases were pinned. Anything above one is a conflict the caller must reject.
func (CommitmentBasis) IsEmpty ¶
func (b CommitmentBasis) IsEmpty() bool
IsEmpty reports whether nothing was pinned, so the standing chain applies.
type CommitmentStep ¶
type CommitmentStep struct {
// Code names the rule that applied.
Code string
// Date is where the running date stood after this rule.
Date time.Time
// DaysMoved is how far this rule pulled the date back. Zero means the rule applied and changed nothing, which is worth reporting: it says the promised date was already a day the customer receives.
DaysMoved int
// Detail carries the rule's own parameter — the transit days counted, the cutoff time applied — for a caller rendering an explanation.
Detail string
}
CommitmentStep is one rule's contribution to a ship-by date.
type CustomerDemand ¶
type CustomerDemand struct {
CustomerAccountID string
CustomerName string
Units float64
// LeadTimeDays is what this customer is committed to, resolved through the same chain an order is stamped from.
LeadTimeDays int
// FulfillmentPolicy is how this customer buys, when they say. Empty means they express no preference.
FulfillmentPolicy string
}
CustomerDemand is one customer's share of an item's demand, with the lead time that customer is promised.
type DeliveryBreakdown ¶
type DeliveryBreakdown struct {
Key string
Label string
DeliveryPerformance
}
DeliveryBreakdown is delivery performance for one slice of the order book — a customer, a customer group, a product line, a sales rep, or the rule the commitment came from.
func AnalyzeDeliveryBreakdown ¶
func AnalyzeDeliveryBreakdown( outcomes []DeliveryOutcome, asOf time.Time, keyOf func(DeliveryOutcome) []DeliveryBreakdownKey, ) []DeliveryBreakdown
AnalyzeDeliveryBreakdown slices the same outcomes by whatever dimension keyOf names.
keyOf returns every (key, label) the outcome belongs to, so a dimension an order sits in more than once — product lines — fans out rather than having to pick one arbitrarily. Returning no keys drops the outcome from that breakdown entirely.
Rows come back ordered by late count descending, then by committed count: the point of a breakdown is to put the worst offender first, and sorting alphabetically would bury it.
type DeliveryBreakdownKey ¶
DeliveryBreakdownKey is one slice an outcome belongs to.
func ByCommitmentSource ¶
func ByCommitmentSource(o DeliveryOutcome) []DeliveryBreakdownKey
ByCommitmentSource keys outcomes on which rule produced their ship-by date.
The most useful breakdown nobody asks for: it says how much of the on-time score rests on an account-wide default nobody deliberately set, versus on dates actually agreed with a customer.
func ByCustomer ¶
func ByCustomer(o DeliveryOutcome) []DeliveryBreakdownKey
ByCustomer keys outcomes on the buying account.
func ByCustomerGroup ¶
func ByCustomerGroup(o DeliveryOutcome) []DeliveryBreakdownKey
ByCustomerGroup keys outcomes on the customer's group.
func ByProductLine ¶
func ByProductLine(o DeliveryOutcome) []DeliveryBreakdownKey
ByProductLine fans an order out across every line it contains.
type DeliveryOutcome ¶
type DeliveryOutcome struct {
SalesOrderID string
SalesOrderNumber string
BuyerAccountID string
CustomerName string
// CustomerGroupID and SalesRepID are empty when the order carries no such association; a breakdown files those under "Unassigned" rather than dropping them.
CustomerGroupID string
CustomerGroupName string
SalesRepID string
// ProductLines is every distinct product line on the order. An order spanning two lines is counted under both, because a late order is late for every line on it.
ProductLines []ProductLineRef
ShipByDate time.Time
IssuedAt *time.Time
// FirstShipAt is nil for an order that has not shipped at all.
FirstShipAt *time.Time
QuantityOrdered float64
QuantityPacked float64
// CommittedLeadTimeDays is what was promised; zero when the order predates commitment tracking.
CommittedLeadTimeDays int
// CommitmentSource names which rule produced the ship-by date — an explicitly promised date, the customer's lead time, their group's, or the account default. Empty for an order stamped before the source was recorded.
CommitmentSource string
}
DeliveryOutcome is one order's commitment and what actually happened to it.
func (DeliveryOutcome) ActualLeadTimeDays ¶
func (o DeliveryOutcome) ActualLeadTimeDays() *int
ActualLeadTimeDays is how long the order actually took, or nil when it has not shipped or was never issued.
func (DeliveryOutcome) DaysLate ¶
func (o DeliveryOutcome) DaysLate(asOf time.Time) int
DaysLate is how far past the promise the first shipment went, or how far past today an unshipped order already is. Zero or negative means not late.
func (DeliveryOutcome) IsInFull ¶
func (o DeliveryOutcome) IsInFull() bool
IsInFull reports whether the whole ordered quantity has been packed.
A tolerance rather than an exact comparison: quantities are decimals and a rounding difference of a millionth of a unit is not a short shipment.
func (DeliveryOutcome) IsOnTime ¶
func (o DeliveryOutcome) IsOnTime() bool
IsOnTime reports whether the first shipment left on or before the promised date.
Measured on the first shipment rather than the last: the promise is that the order starts moving by that date, and judging on the last shipment would fail an order the customer received on time in two boxes.
func (DeliveryOutcome) IsShipped ¶
func (o DeliveryOutcome) IsShipped() bool
IsShipped reports whether anything has left for this order.
type DeliveryPerformance ¶
type DeliveryPerformance struct {
// PeriodStart is the first day of the bucket; zero for the overall summary.
PeriodStart time.Time
CommittedOrderCount int
ShippedOrderCount int
OnTimeOrderCount int
OnTimeInFullCount int
LateOrderCount int
// NotYetShippedCount is orders due in this period that have not shipped at all. They count against on-time, because a promise not yet met is not a promise kept.
NotYetShippedCount int
// Ratios are nil rather than zero when nothing was due, so a quiet week does not render as total failure.
OnTimePct *float64
OnTimeInFullPct *float64
// AverageDaysLate is measured over late orders only; nil when none were late. Averaging over every order would dilute a real problem into a number that looks fine.
AverageDaysLate *float64
// AverageLeadTimeDays is how long shipped orders actually took, and AverageCommittedLeadTimeDays what they were promised. The gap between them is what a merchant renegotiates on.
AverageLeadTimeDays *float64
AverageCommittedLeadTimeDays *float64
}
DeliveryPerformance is the delivery picture for one period, or for the whole window.
type DemandInput ¶
type DemandInput struct {
AsOf time.Time
BasisCode string
ForecastZ float64
WeeksPerYear int
// ForecastMonths is how far forward to project when using the seasonal-EMA basis.
ForecastMonths int
// MonthlyByItem is monthly demand per constraint item, already pooled from the finished goods that item becomes. Series need not be dense.
MonthlyByItem map[string][]MonthlyDemand
// DownstreamByItem is the per-finished-good series behind each constraint item, needed to compute the downstream sigma sum separately from the pooled one.
DownstreamByItem map[string][]FinishedGood
Overrides []DemandOverride
// ItemsByProductLine resolves a product-line-scoped override onto items.
ItemsByProductLine map[string][]string
}
DemandInput is everything the demand pass needs, already loaded.
type DemandOverride ¶
type DemandOverride struct {
ID string
ScopeCode string
ScopeRefID string
PeriodStart time.Time
PeriodEnd time.Time
TypeCode string
Value float64
ReasonCode string
CreatedAt time.Time
}
DemandOverride is management's adjustment to the forecast baseline. This is the only mechanism for departing from history — there is no growth multiplier.
type DerivedLine ¶
type DerivedLine struct {
SourceLineID string
ProductionStep string
DepartmentID string
ItemID string
SKU string
// WeekIndex is the constraint week plus the accumulated lead-time offset.
WeekIndex int
Quantity float64
// Depth is how many steps downstream of the constraint this work sits, which is what a readiness chip keys off. Zero is the constraint step itself.
Depth int
}
DerivedLine is work a constraint campaign implies for a department — the constraint step's own work at depth 0, and everything downstream of it below that.
func Explode ¶
func Explode(in ExplosionInput) []DerivedLine
Explode walks the production-step graph from each constraint campaign and returns the work each department has to do as a result.
Two properties matter more than the traversal itself:
Determinism. Go randomizes map iteration, so every collection is sorted before it is walked. Without that, two runs over the same plan produce the same rows in a different order, and a diff between versions becomes unreadable.
Termination. Production graphs contain rework loops — a quality step feeding back into the step before it — so the walk is bounded by depth and refuses to revisit a step it has already reached at the same depth. An unbounded walk on a real graph does not return.
type Diagnostics ¶
type Diagnostics struct {
LevellingDiagnostics
AppliedOverrides []AppliedOverride `json:"applied_overrides"`
// ChangeoverSlopeMinutes is the calibrated minutes per additional input.
ChangeoverSlopeMinutes float64 `json:"changeover_slope_minutes"`
AverageInputsAdded float64 `json:"average_inputs_added"`
// ItemsWithoutRunRate never got a measured seconds-per-unit and cannot be scheduled; they are reported rather than silently dropped.
ItemsWithoutRunRate []string `json:"items_without_run_rate"`
ExcludedItemCount int `json:"excluded_item_count"`
// FirmDemandUnits is the whole open order book expressed at the constraint. Zero means the plan is forecast-only.
FirmDemandUnits float64 `json:"firm_demand_units"`
// UndatedFirmOrderCount is how many open orders carry no ship-by commitment and were dated at the front of the horizon. A non-zero count means orders predating commitment tracking still need backfilling.
UndatedFirmOrderCount int `json:"undated_firm_order_count"`
// MakeToOrderItemCount is how many planned items are built only against the order book.
MakeToOrderItemCount int `json:"make_to_order_item_count"`
// AtRiskOrders are commitments this plan does not cover in time. This is the most actionable thing the solver produces: everything else describes what the plan does, and this says which promises it breaks.
AtRiskOrders []AtRiskOrder `json:"at_risk_orders"`
// ConstraintMachineCount is how many machines the constraint department contributed, and MeasuredBatchCount how much production history was found on them. A plan with machines but no history is empty for a reason a planner can act on — nothing has been scanned in the demand window — and saying so beats an empty grid.
ConstraintMachineCount int `json:"constraint_machine_count"`
MeasuredBatchCount int `json:"measured_batch_count"`
// MachinesWithoutStep have no production step, so their campaigns derive no downstream department work.
MachinesWithoutStep int `json:"machines_without_step"`
// Finishing is stage two's own account of itself: what it could not make, and why.
Finishing FinishingDiagnostics `json:"finishing"`
// FinishingMachineCount is how many machines the second stage was sized from. Zero means its capacity was estimated from the shift pattern alone.
FinishingMachineCount int `json:"finishing_machine_count"`
// FinishingCapacityIsEstimated says the plant has no machines outside the constraint department, so stage two was sized as a single notional resource rather than counted. A levelled plan against a guessed capacity is worth flagging as such.
FinishingCapacityIsEstimated bool `json:"finishing_capacity_is_estimated"`
}
Diagnostics is the honest account of what the solver could not do and why the plan differs from raw history. A plan that cannot explain itself will not be trusted.
type ExplosionCampaign ¶
type ExplosionCampaign struct {
LineID string
ItemID string
SKU string
MachineID string
WeekIndex int
Quantity float64
// StepID is where the campaign runs — the constraint step the walk starts from.
StepID string
}
ExplosionCampaign is one constraint campaign to explode downstream.
type ExplosionInput ¶
type ExplosionInput struct {
// Campaigns are the constraint-level lines the plan already committed to.
Campaigns []ExplosionCampaign
Edges []StepEdge
Steps map[string]StepInfo
MaxDepth int
}
ExplosionInput is everything needed to derive department work from a constraint plan.
type FinishedGood ¶
type FinishedGood struct {
ItemID string
SKU string
ProductLineID string
Monthly []MonthlyDemand
OnHand float64
}
FinishedGood is one finished SKU a constraint item becomes, carried with its identity so the pooled buffer can be decomposed back into per-SKU targets.
type FinishedGoodDemand ¶
type FinishedGoodDemand struct {
ItemID string
SKU string
ProductLineID string
AnnualDemand float64
SigmaWeekly float64
OnHand float64
}
FinishedGoodDemand is one finished SKU's measured demand, carried out of the pooling step so its own target can be computed.
type FinishedPolicy ¶
type FinishedPolicy struct {
ItemID string
SKU string
GreigeItemID string
GreigeSKU string
ProductLineID string
AnnualDemand float64
WeeklyDemand float64
SigmaWeekly float64
// Sized against the finishing lead time, not the knit lead time: this stock is replenished by finishing, not by the constraint.
SafetyStock float64
ReorderPoint float64
OnHand float64
WeeksOfCover float64
}
FinishedPolicy is one finished SKU's own inventory target.
The greige buffer is pooled across the family because one greige feeds many finished SKUs and their variability partly cancels. That pooling is what makes the buffer cheap, and it is also what makes these rows necessary: once pooled, the echelon total can no longer answer "is this SKU short".
func ComputeFinishedPolicies ¶
func ComputeFinishedPolicies(greige ItemPolicy, finished []FinishedGoodDemand, s Settings) []FinishedPolicy
ComputeFinishedPolicies decomposes one greige family's pooled buffer into a target per finished SKU.
The greige buffer is sized against the knit lead time and pooled across the family; these are sized against the finishing lead time and stand alone, because that is the stage that actually replenishes them. The two are complementary, not alternatives: the greige buffer decouples knitting from finishing, and these cover the finishing lead time in front of the customer.
type FinishingDiagnostics ¶
type FinishingDiagnostics struct {
WeeklyCapacityHours float64 `json:"weekly_capacity_hours"`
PlannedHoursByWeek []float64 `json:"planned_hours_by_week"`
// UtilisationByWeek is planned hours over capacity. Nil entries are impossible here — capacity is constant across the horizon — but a zero-capacity input yields zeroes rather than infinities.
UtilisationByWeek []float64 `json:"utilisation_by_week"`
// GreigeStarvedSKUs wanted building and had no greige to build from.
GreigeStarvedSKUs []string `json:"greige_starved_skus"`
// CapacityStarvedSKUs had greige and no hours.
CapacityStarvedSKUs []string `json:"capacity_starved_skus"`
// ItemsWithoutRunRate never got a measured finishing rate and cannot be levelled.
ItemsWithoutRunRate []string `json:"items_without_run_rate"`
// UnusedGreigeUnits is stage-one output the horizon never converts. A large number means the two stages are planned against different demand, which is worth surfacing rather than leaving as an unexplained pile of greige.
UnusedGreigeUnits float64 `json:"unused_greige_units"`
TotalPlannedUnits float64 `json:"total_planned_units"`
LineCount int `json:"line_count"`
}
FinishingDiagnostics is the honest account of what stage two could not do.
The two starvation lists are the point of the whole model. A SKU held back for want of greige is a stage-one problem — knit more, or knit it sooner — and a SKU held back for want of hours is a stage-two problem — another shift, or a different mix. Collapsing them into one "short" list would throw away the only thing a two-stage plan knows that a one-stage plan does not.
type FinishingInput ¶
type FinishingInput struct {
Items []FinishingItem
Supply []FinishingSupply
// GreigeOnHand is what is already knitted and waiting when the horizon opens. Without it the first weeks of every plan would read as starved while the greige store sat full.
GreigeOnHand map[string]float64
// WeeklyCapacityHours is the whole second stage's capacity in one week.
WeeklyCapacityHours float64
Settings Settings
}
FinishingInput is everything the second-stage sweep needs, already loaded.
type FinishingItem ¶
type FinishingItem struct {
ItemID string
SKU string
// GreigeItemID names the stage-one item this is made from. It is what ties the two schedules together: a finished SKU cannot be built in a week the greige it comes from has not arrived in.
GreigeItemID string
GreigeSKU string
ProductLineID string
// WeeklyDemand and OnHand are this SKU's own, not the family's. The pooled figures behind the knit plan cannot answer "is this colourway short", which is the whole question stage two exists to answer.
WeeklyDemand float64
OnHand float64
ReorderPoint float64
SafetyStock float64
// SecondsPerUnit is the measured finishing run rate. Without one the SKU cannot be levelled, because there is no way to know what an hour of the department buys.
SecondsPerUnit float64
LotUnits float64
LotUnitID string
UnitID string
// GreigePerUnit is how much stage-one output one finished unit consumes. One unless a yield ratio says otherwise; a finishing loss makes it greater than one.
GreigePerUnit float64
// ProductionStepID and DepartmentID are where the SKU is made. The sweep does not decide with them — the second stage is one pool — but the plan line carries them so a department rollup needs no join.
ProductionStepID string
DepartmentID string
// FirmByWeek is the order book at this SKU, by the week it is owed.
FirmByWeek []float64
IsMakeToOrder bool
}
FinishingItem is one finished SKU the second stage can make.
type FinishingLine ¶
type FinishingLine struct {
ItemID string
SKU string
GreigeItemID string
GreigeSKU string
WeekIndex int
Quantity float64
Lots int
LotUnits float64
LotUnitID string
RunHours float64
// GreigeConsumed is what this line takes out of the stage-one buffer, which is what makes the two schedules reconcilable.
GreigeConsumed float64
// FirmUnits is how much of the week's draw was an order rather than a forecast, so a planner can see which lines are promises.
FirmUnits float64
// ProductionStepID and DepartmentID name where the work runs.
ProductionStepID string
DepartmentID string
ProjectedOnHandBefore float64
ProjectedOnHandAfter float64
}
FinishingLine is one finished SKU's build in one week.
type FinishingResult ¶
type FinishingResult struct {
Lines []FinishingLine
// ProjectedOnHand[itemID][weekIndex] is the finished SKU's position at the end of that week.
ProjectedOnHand map[string][]float64
Diagnostics FinishingDiagnostics
}
FinishingResult is the second-stage plan.
func LevelFinishing ¶
func LevelFinishing(in FinishingInput) FinishingResult
LevelFinishing runs the capacity-levelled sweep over the second stage.
Each week: greige arrives, every SKU whose projected position has fallen below its trigger becomes a candidate, and candidates are served most-depleted-first out of two shared pools — the greige their family knitted, and the department's hours. What does not fit waits for the next week, which is what makes this a levelling rather than an allocation.
Determinism: SKUs are sorted before any iteration and every tie breaks on SKU, so the same plan solves to the same mix twice. Go randomizes map iteration, and a mix that wobbled between runs would make a version diff unreadable.
type FinishingStep ¶
FinishingStep is where a finished good is made, as the second stage's history records it.
type FinishingSupply ¶
FinishingSupply is stage-one output becoming available to stage two.
WeekIndex is the week the greige can be worked, not the week it was knitted — the caller applies the lag between the two, because how long greige sits before finishing can start is a property of the plant rather than of the plan.
type FirmRequirement ¶
type FirmRequirement struct {
// ItemID is the constraint item, not the finished good: the requirement has already been pooled onto whatever the plan actually builds.
ItemID string
// FinishedItemID is the finished good the order was placed for, kept so an at-risk order can name the SKU rather than the greige behind it.
FinishedItemID string
SalesOrderID string
SalesOrderNumber string
SalesOrderLineID string
// Units is the outstanding quantity in the constraint item's own unit.
Units float64
// ShipByWeek is the horizon week the finished good must exist in. Negative means the commitment is already behind the plan.
ShipByWeek int
// DueWeek is when the constraint stage must finish for the finishing lead time to still make ShipByWeek. Clamped at zero.
DueWeek int
// IsPastDue is set when the constraint stage would have had to start before the plan does. The clamp to week 0 hides that, so it is recorded rather than inferred from DueWeek.
IsPastDue bool
// IsUndated marks an order with no ship-by commitment, dated at the start of the horizon because it is issued and unshipped. Reported separately so a planner can tell a real promise from a guess.
IsUndated bool
}
FirmRequirement is one open order line's outstanding quantity, expressed against the constraint item that has to produce it.
This is demand the plan owes, as opposed to the demand it forecasts. The distinction matters because a forecast is an average the buffer absorbs, while an order is a date somebody was promised.
type FirmSchedule ¶
type FirmSchedule struct {
// ByItemWeek[itemID][weekIndex] is the firm quantity due from the constraint stage in that week.
ByItemWeek map[string][]float64
// Requirements is the flat list, sorted, for diagnostics and at-risk reporting.
Requirements []FirmRequirement
// TotalUnits is the whole order book expressed at the constraint.
TotalUnits float64
// UndatedCount is how many requirements had no ship-by commitment.
UndatedCount int
// PastDueCount is how many needed the constraint stage to start before the horizon.
PastDueCount int
}
FirmSchedule is the order book resolved into per-item, per-week quantities the sweep can draw down.
func BuildFirmSchedule ¶
func BuildFirmSchedule(lines []OpenOrderLine, horizonStart time.Time, s Settings) FirmSchedule
BuildFirmSchedule dates the open order book against the horizon and pools it onto the constraint items.
Dating walks backwards from the promise: the finished good has to exist by its ship-by week, so the constraint stage has to finish a finishing lead time earlier. A requirement whose constraint week lands before the horizon is not dropped — it is clamped to week 0 and flagged, because an order that needed to start last month is the single most useful thing the plan can tell a planner, and silently moving it forward would make the plan look achievable when it is not.
Deterministic: requirements are sorted before they are returned, so two solves over the same order book produce the same diagnostics.
func (FirmSchedule) RequirementForWeek ¶
func (f FirmSchedule) RequirementForWeek(itemID string, week int) float64
RequirementForWeek is the firm quantity due for one item in one week, safe against items with no order book at all.
type FulfillmentResolution ¶
FulfillmentResolution is a SKU's policy and the rule that produced it.
func ResolveFulfillmentPolicy ¶
func ResolveFulfillmentPolicy(itemID string, in PolicyResolutionInput) FulfillmentResolution
ResolveFulfillmentPolicy decides how one item is produced.
The chain, most specific first: an explicit item override, the item's own product line, the lines of what it becomes, then the account default.
The downstream step exists for the same reason it exists for lot sizes: greige is not sold, carries no product line of its own, and has to inherit from the finished goods it becomes. It differs in one way, and the difference is the whole design. A greige item is built to order only when **every** finished good it feeds is built to order — one stocked sibling means the greige is still forecast-driven, because that sibling's buffer has to come from somewhere. This is what makes a two-value enum sufficient: the pooled greige buffer shrinks in proportion to how much of its family went make-to-order, with no third policy to name.
type ItemDemand ¶
type ItemDemand struct {
ItemID string
// AnnualDemand is the forward-looking annual run rate the policy uses.
AnnualDemand float64
// TrailingAnnual is the last twelve complete months, for comparison.
TrailingAnnual float64
// SigmaWeeklyPooled is the pooled weekly standard deviation at the constraint. Pooled as sqrt(sum of squares) because one constraint item feeds many finished SKUs whose variability partially cancels.
SigmaWeeklyPooled float64
// FinishedGoods is the per-SKU detail behind the pooled figures above.
FinishedGoods []FinishedGoodDemand
// SigmaDownstreamSum is the plain sum of downstream sigmas, used for the finished goods buffer where the risk does not pool.
SigmaDownstreamSum float64
DownstreamCount int
}
ItemDemand is the resolved demand picture for one constraint item.
type ItemMeasurement ¶
type ItemMeasurement struct {
ItemID string
SKU string
// SecondsPerUnit is the run rate: how long one unit occupies the constraint.
SecondsPerUnit float64
UnitCost float64
// OverheadRate is the production step's machine burden. Production labor is deliberately NOT included: a changeover is worked by a technician on the single machine, so setup cost uses the dedicated changeover labor rate from settings instead of the thin per-machine-hour production allocation. Including both would double-count labor and inflate EOQ.
OverheadRate float64
// LotCount is how many batches were produced; the script treats one batch as one lot regardless of its quantity, and so does this.
LotCount int
Quantity float64
// EligibleMachineID is the set of machines that have actually run this item. Empty means unconstrained.
EligibleMachineID map[string]bool
// MeasuredLeadTimeWeeks is the mean observed run-open to scan time. Zero when no usable samples existed, in which case the settings default applies.
MeasuredLeadTimeWeeks float64
LeadTimeSampleCount int
ProductionStepID string
}
ItemMeasurement is what history tells us about one constraint item.
func MeasureItems ¶
func MeasureItems(batches []BatchMeasurement) []ItemMeasurement
MeasureItems aggregates batch history into per-item measurements.
Determinism: the result is sorted by SKU. Callers iterate it directly, so returning a map would reintroduce the nondeterminism the port exists to remove.
type ItemPolicy ¶
type ItemPolicy struct {
ItemID string
SKU string
// UnitID is what every quantity in this policy is counted in. A reorder point of 2,508 is uninterpretable without it, and the policy exists whether or not the item won a slot this horizon — so it carries its own unit rather than borrowing one from a campaign that may not exist.
UnitID string
AnnualDemand float64
WeeklyDemand float64
SecondsPerUnit float64
UnitCost float64
SetupCost float64
HoldingCost float64
EOQUnits float64
// Lead times, in weeks.
ConstraintLeadTimeWeeks float64
FinishLeadTimeWeeks float64
// Two-echelon safety stock. The buffer is pooled at the constraint item because one greige feeds many finished SKUs, so their variability partially cancels (sigma_pooled = sqrt(sum of squares), not the sum). Finished goods keep a smaller per-SKU buffer for service.
SigmaWeeklyPooled float64
SigmaDownstreamSum float64
SafetyStockPrimary float64
SafetyStockDownstream float64
ReorderPoint float64
OrderUpTo float64
// FulfillmentPolicy is how this item is produced, and PolicySource which rule decided. A make-to-order item holds no buffer and is built only against the order book.
FulfillmentPolicy string
PolicySource string
// FirmDemandUnits is what the order book already owes over the horizon; ForecastDemandUnits is what the forecast projects for the same window. Split so a planner can see which drove a campaign.
FirmDemandUnits float64
ForecastDemandUnits float64
OnHandEchelon float64
// OnHandGreige is the constraint stage on its own. The echelon figure is what the build decision is made against; this is what is actually in the greige store, which a pooled total cannot be decomposed back into.
OnHandGreige float64
// Greige-stage holding: the buffer plus half a campaign on average, and a whole campaign at the peak. Finished safety stock is held as finished goods and counted on the finished policies, so the two stages sum to network storage without double-counting.
AverageGreigeInventory float64
MaxGreigeInventory float64
WeeksOfCover float64
ABCClass string
}
ItemPolicy is the computed inventory policy for one constraint item.
func ClassifyABC ¶
func ClassifyABC(policies []ItemPolicy) []ItemPolicy
ClassifyABC assigns A/B/C by cumulative share of annual run hours: A up to 80%, B to 95%, C the tail. Mutates in place and returns the slice sorted by run hours descending, which is also the order the caller wants for display.
Ties are broken by SKU so the classification is stable — without it, two items with identical run hours could swap classes between runs.
Known edge case, preserved from the script for parity: the share is cumulative INCLUSIVE of the current item, so a portfolio dominated by one item classifies that item as C rather than A (its own share already exceeds 95%). With a realistic spread of SKUs this never triggers. Do not "fix" it without re-running the parity gate — the classification feeds display and reporting, not the plan itself.
func ComputePolicy ¶
func ComputePolicy(in PolicyInput, s Settings) ItemPolicy
ComputePolicy derives the inventory policy for one item.
EOQ = sqrt(2 * D * S / H) classic economic order quantity SS_primary = z * sigma_pooled * sqrt(constraint LT) SS_downstream = z * sum(sigma_fg) * sqrt(finish LT) ROP = weekly * (constraint LT + finish LT) + SS_primary + SS_downstream
The reorder point covers demand over the whole pipeline — constraint plus finishing — because that is how long it takes for a decision made today to become sellable stock.
func (ItemPolicy) AnnualRunHours ¶
func (p ItemPolicy) AnnualRunHours() float64
AnnualRunHours is the machine time this item's yearly demand consumes. ABC is classified on this rather than on revenue: the constraint is machine time, so the A items are the ones that eat the schedule.
func (ItemPolicy) IsMakeToOrder ¶
func (p ItemPolicy) IsMakeToOrder() bool
IsMakeToOrder reports whether this item is built only against the order book.
type LatenessBucket ¶
type LatenessBucket struct {
Label string
MinDaysLate int
MaxDaysLate int
OrderCount int
// ShippedCount is how many of these have since shipped. The remainder are still owed, and are the same orders the backlog aging counts.
ShippedCount int
// Units is what was still unpacked across the band's orders.
Units float64
}
LatenessBucket is how many of the window's misses fell in one lateness band.
Reported alongside the average because an average is the one number that cannot distinguish "everything slips a day" from "most orders are fine and four are two months late", and those are opposite problems with opposite fixes.
func AnalyzeLatenessDistribution ¶
func AnalyzeLatenessDistribution(outcomes []DeliveryOutcome, asOf time.Time) []LatenessBucket
AnalyzeLatenessDistribution bands every order that missed its date by how far it missed by.
Unlike backlog aging this counts shipped orders too: the question is how badly the period's promises were missed, not what is still owed. `ShippedCount` separates the two inside each band.
type LeadTimeInput ¶
type LeadTimeInput struct {
// CustomerLeadTimeDays is the customer's own commitment, from account_relation.
CustomerLeadTimeDays *int
// ParentCustomerLeadTimeDays is the parent account's commitment, inherited by every child that has not set its own. One level: the parent's own parent is not consulted, matching how a child inherits its parent's prices.
ParentCustomerLeadTimeDays *int
// AccountGroupLeadTimeDays is inherited by every customer in the group that has not set its own.
AccountGroupLeadTimeDays *int
// AccountLeadTimeDays is the account-wide default, the last fallback.
AccountLeadTimeDays *int
}
LeadTimeInput is the standing chain, gathered once. Nil means "not set at this level, keep looking"; a configured zero is a real answer and means same-day.
type LevellingDiagnostics ¶
type LevellingDiagnostics struct {
// EOQCappedSKUs had their economic lot size reduced to fit one machine-week, meaning shorter and more frequent campaigns than the policy would prefer.
EOQCappedSKUs []string `json:"eoq_capped_skus"`
// UnschedulableSKUs cannot fit even a single minimum lot into a machine-week. They are never scheduled, so expect stockouts.
UnschedulableSKUs []string `json:"unschedulable_skus"`
// CapacityStarvedSKUs are below their reorder point but never won a slot in the horizon. This is the honest signal that the plant is short of capacity.
CapacityStarvedSKUs []string `json:"capacity_starved_skus"`
}
LevellingDiagnostics records what the solver could not do. These are the numbers a planner needs in order to trust or challenge the plan, so they are part of the output rather than log lines.
type LevellingItem ¶
type LevellingItem struct {
Policy ItemPolicy
EligibleMachineID map[string]bool
// LotUnits is the rounding granularity for this item (a doff, a pallet).
LotUnits float64
// LotUnitID is what that granularity is counted in.
LotUnitID string
// FirmByWeek is the dated order book for this item, indexed by week. Nil when nothing is on order, which is the case the plan behaved as before this existed.
FirmByWeek []float64
}
itemEligibility restricts an item to the machines that have historically run it. Empty means any machine.
type LevellingResult ¶
type LevellingResult struct {
Campaigns []Campaign
Diagnostics LevellingDiagnostics
// ProjectedOnHand[itemID][weekIndex] is the position at the END of that week, after that week's campaigns land and that week's demand is drawn down.
ProjectedOnHand map[string][]float64
}
LevellingResult is the plan plus the per-item projected stock position.
func Level ¶
func Level(items []LevellingItem, machines []Machine, s Settings, pinned []PinnedCampaign) LevellingResult
Level runs the capacity-levelled (s,S) sweep across the horizon.
Each week, every item whose projected position has fallen below its trigger is a candidate. Candidates are served most-depleted-first and placed on the least-loaded eligible machine that still has room. Anything that does not fit waits for the next week.
Pinned campaigns are applied to each week before its candidates are chosen: their inflow and capacity use are facts of the plan, not proposals, so everything the sweep derives already accounts for them. Pins are not re-emitted as campaigns — they exist as lines already.
Determinism: items are sorted by SKU and machines by name before any iteration, and the due-set sort breaks ties by SKU. Iterating the maps directly would produce a different plan on every run.
type LotDefault ¶
type LotDefault struct {
Quantity float64
UnitID string
// Source explains which rule produced this lot, so a plan can say why it is knitting in sixties. Values: item_override, product_line, downstream_product_line, account_default.
Source string
// ProductLineID is the line the convention came from, empty for the account default.
ProductLineID string
}
LotDefault is the lot an item is made in: how many, counted in what.
The unit is not decoration. A doff of sock greige is 60 pairs and a doff of armsleeve greige is 60 eaches; the number is the same and the quantities are not, so a lot size without a unit cannot be reconciled with anything downstream of it.
func ResolveLotDefault ¶
func ResolveLotDefault(itemID string, in LotResolutionInput) (LotDefault, bool)
ResolveLotDefault decides what lot one item is made in.
The chain, most specific first:
- A per-item override, which changes the size but keeps the item's own unit.
- The item's own product line, for anything that is itself sellable.
- The product lines of what the item becomes. Greige has no product line of its own — it is not sold — so a doff of sock greige takes its lot from the sock line. Where a greige feeds several lines, the one carrying the most demand wins.
- The account default, counted in the item's own unit.
Returns false when nothing in the chain yields a usable lot, which means the item is planned unlotted rather than in a lot of some guessed size.
type LotResolutionInput ¶
type LotResolutionInput struct {
// ItemOverrides are per-item lot sizes set by hand. They carry no unit — an override changes how big a lot is, not what it is counted in.
ItemOverrides map[string]float64
// ProductLineByItem maps an item to the line it sells under. Intermediate items — greige — are absent, which is what makes downstream inheritance necessary.
ProductLineByItem map[string]string
// LotByProductLine is the configured convention per line.
LotByProductLine map[string]ProductLineLot
// DownstreamByItem is what each planned item becomes, used to inherit a lot for greige that has no product line of its own.
DownstreamByItem map[string][]FinishedGood
// AccountLotUnits is the account-wide fallback size.
AccountLotUnits float64
// UnitByItem is each item's own counting unit, used for the account fallback where there is no product line to take a unit from.
UnitByItem map[string]string
}
LotResolutionInput is everything the chain needs, gathered once.
type MonthlyDemand ¶
MonthlyDemand is one month of demand for one item.
type OpenOrderLine ¶
type OpenOrderLine struct {
SalesOrderID string
SalesOrderNumber string
// SalesOrderLineID is the line the quantity is owed against, which is what a shipment is measured against later.
SalesOrderLineID string
// FinishedItemID is the item the order was placed for.
FinishedItemID string
// ConstraintItemID is the item in the plan that produces it. Empty means nothing planned produces this order, and the line is dropped.
ConstraintItemID string
// Units is already scaled into the constraint item's unit by the caller, the same way pooled historical demand is.
Units float64
// ShipByDate is nil for an order issued before commitments were tracked.
ShipByDate *time.Time
}
OpenOrderLine is one outstanding order line as loaded, before it is dated or pooled.
type OrderAllocation ¶
type OrderAllocation struct {
ItemID string
// CampaignWeek and CampaignMachineID identify the campaign; the caller maps them back onto the line it wrote.
CampaignWeek int
CampaignMachineID string
SalesOrderID string
SalesOrderNumber string
SalesOrderLineID string
Units float64
}
OrderAllocation is one campaign's contribution to one order.
type PinnedCampaign ¶
PinnedCampaign is a hand-edited campaign the sweep must plan around rather than re-derive. Its units raise the item's projected position in its week and its run time consumes that machine's capacity, so the rest of the plan responds to the hand edit: build something sooner and the solver builds less of it later; trim a campaign and the solver replenishes earlier.
type PolicyInput ¶
type PolicyInput struct {
ItemID string
SKU string
AnnualDemand float64
SecondsPerUnit float64
UnitCost float64
// OverheadRate is the step's machine burden. Setup cost adds the dedicated changeover labor rate from settings on top; production labor is excluded.
OverheadRate float64
// MeasuredLeadTimeWeeks is the observed constraint-step lead time; zero means unmeasured, in which case the settings default applies.
MeasuredLeadTimeWeeks float64
SigmaWeeklyPooled float64
SigmaDownstreamSum float64
OnHandEchelon float64
OnHandGreige float64
// FulfillmentPolicy decides whether this item carries a statistical buffer at all. Empty means make-to-stock, so a caller that has not adopted policies gets the behaviour it had before they existed.
FulfillmentPolicy string
PolicySource string
// FirmDemandUnits and ForecastDemandUnits are carried through onto the policy for reporting; neither changes the arithmetic.
FirmDemandUnits float64
ForecastDemandUnits float64
}
PolicyInput is one item's measured facts, before policy is applied.
type PolicyResolutionInput ¶
type PolicyResolutionInput struct {
// ItemOverrides are explicit per-item policies.
ItemOverrides map[string]string
// ProductLineByItem maps an item to the line it sells under. Intermediate items are absent.
ProductLineByItem map[string]string
// PolicyByProductLine is the default configured on each line.
PolicyByProductLine map[string]string
// AccountDefault is the last resort; empty means make-to-stock.
AccountDefault string
// DownstreamByItem is what each planned item becomes, so an intermediate can inherit the policy of the finished goods it turns into.
DownstreamByItem map[string][]FinishedGood
}
PolicyResolutionInput is everything the chain needs, gathered once.
Customer and account-group policies are deliberately absent. Policy is resolved per SKU, and a SKU sold to both a stocking distributor and a contract customer cannot take its policy from whichever customer is being looked at — those settings drive the recommendation instead, which is a different question asked at a different time.
type ProductLineLot ¶
ProductLineLot is one line's configured lot convention.
type ProductLineRef ¶
ProductLineRef is one product line an order touches.
type Recommendation ¶
type Recommendation struct {
ItemID string
SKU string
CurrentPolicy string
RecommendedPolicy string
// Reason is the rule that decided. Exactly one, because the rules are ordered and the first match wins — listing every rule that happened to agree would obscure which one actually drove it.
Reason string
// The measurements behind the verdict, reported so a planner can disagree with the rule rather than only with the answer.
AverageDemandInterval float64
CoefficientOfVariation float64
TopCustomerName string
// DemandWeightedLeadTimeDays is what customers are promised on average, weighted by how much they buy.
DemandWeightedLeadTimeDays float64
AnnualCOGS float64
MonthsSinceLastSale int
}
Recommendation is what the engine thinks an item should be, and why.
func RecommendPolicy ¶
func RecommendPolicy(in ClassificationInput, t RecommendationThresholds) Recommendation
RecommendPolicy decides whether an item should be built to stock or to order, and names the rule that decided.
The rules are ordered and the first match wins:
- lead_time_infeasible — customers are promised less time than the plant needs. Checked first because it is a *necessary* condition rather than a preference: if you cannot produce inside the window you promised, stocking it is not a choice you get to make.
- no_recent_demand — nothing has sold in the dormant window. A buffer here is dead stock.
- single_customer — effectively one customer buys it and that customer is served to order.
- lumpy_demand — demand is both intermittent and highly variable, which is the shape a statistical safety stock sizes worst. Uses the Syntetos-Boylan cut points.
- slow_moving_high_value — expensive units, few sold. The buffer costs more than the service it buys.
- otherwise steady_demand — regular enough to forecast, which is what stocking is for.
A recommendation is advice, never applied on its own. Reclassifying a SKU silently would change what the plant builds without anyone deciding to.
func (Recommendation) Changes ¶
func (r Recommendation) Changes() bool
Changes reports whether adopting this recommendation would actually change how the item is planned.
type RecommendationThresholds ¶
type RecommendationThresholds struct {
// DormantMonths is how long without a sale before an item counts as dead.
DormantMonths int
// ConcentrationPct is the share of demand one customer must hold to count as the only customer.
ConcentrationPct float64
// ADIThreshold and CV2Threshold are the Syntetos-Boylan cut points separating smooth demand from lumpy.
ADIThreshold float64
CV2Threshold float64
// SlowMoverCOGS is the annual cost of goods below which an item is a slow mover.
SlowMoverCOGS float64
// HighValueUnitCost is the unit cost above which holding stock is expensive.
HighValueUnitCost float64
}
RecommendationThresholds are the merchant-editable cut points the classifier draws against.
func DefaultRecommendationThresholds ¶
func DefaultRecommendationThresholds() RecommendationThresholds
DefaultRecommendationThresholds mirrors the schema defaults so a caller that has never configured them still classifies.
type Settings ¶
type Settings struct {
// Horizon
HorizonWeeks int `json:"horizon_weeks"` // HORIZON_WEEKS = 13
FrozenWeeks int `json:"frozen_weeks"`
// WeekStartDay is the weekday a horizon week begins on, 0 = Sunday through 6 = Saturday.
WeekStartDay int `json:"week_start_day"`
// Capacity
ShiftsPerDay int `json:"shifts_per_day"` // SHIFTS = 2
HoursPerShift float64 `json:"hours_per_shift"` // HOURS_PER_SHIFT = 7
WorkDaysPerWeek int `json:"work_days_per_week"` // WORK_DAYS_WK = 5
WeeksPerYear int `json:"weeks_per_year"` // WEEKS_YR = 52
CapacityHeadroomPct float64 `json:"capacity_headroom_pct"` // the 0.9 headroom reserved for changeover
DefaultLotUnits float64 `json:"default_lot_units"` // AVG_DOFF = 60
// Changeover
ChangeoverAvgMinutes float64 `json:"changeover_avg_minutes"` // CO_AVG_MIN = 30
ChangeoverMinMinutes float64 `json:"changeover_min_minutes"` // CO_MIN_MIN = 15
ChangeoverMaxMinutes float64 `json:"changeover_max_minutes"` // CO_MAX_MIN = 90
ChangeoverLaborRate float64 `json:"changeover_labor_rate"` // CHANGEOVER_LABOR_RATE = 20
// Inventory policy
HoldingRatePct float64 `json:"holding_rate_pct"` // HOLDING_RATE_PCT = 0.25
ServiceLevelZ float64 `json:"service_level_z"` // SERVICE_Z = 1.645
FinishLeadTimeWeeks float64 `json:"finish_lead_time_weeks"` // FINISH_LT_WEEKS = 6
DefaultConstraintLeadTimeWeeks float64 `json:"default_constraint_lead_time_weeks"` // KNIT_LT_WEEKS_DEFAULT = 1.3
MaxWeeksSupply float64 `json:"max_weeks_supply"` // MAX_WEEKS_SUPPLY = 12
MaxFlowDepth int `json:"max_flow_depth"` // MAX_FLOW_DEPTH = 10
// Fulfillment commitments
//
// DefaultCustomerLeadTimeDays is the last fallback in the ship-by chain, behind the customer and its account group. It lives with the planning assumptions because it is what a make-to-order promise is measured against: the date it produces has to be the same date the plan is solved to.
DefaultCustomerLeadTimeDays int `json:"default_customer_lead_time_days"`
}
Settings are the planning assumptions, already resolved to effective values by the caller. Every field was a hardcoded constant in the script; the mapping is noted so a parity failure can be traced back to a specific line.
func DefaultSettings ¶
func DefaultSettings() Settings
DefaultSettings mirrors the script's constants, with one deliberate exception: there is no growth multiplier. The script's GROWTH_MULT defaulted to 2, silently doubling all demand; that intent is now expressed as a demand override, which carries a reason and an author.
func (Settings) MachineWeeklyCapacityHours ¶
MachineWeeklyCapacityHours is the hours one machine can be planned for in a week.
The headroom factor is not slack: changeovers are not scheduled as explicit blocks, so the reserve is what stops a plan that is 100% run time and therefore impossible. Script: SHIFTS * HOURS_PER_SHIFT * WORK_DAYS_WK * 0.9.
func (Settings) MaxFlowDepthOrDefault ¶
MaxFlowDepthOrDefault bounds the genealogy walk so a rework cycle cannot loop forever.
type SolverInput ¶
type SolverInput struct {
AccountID string
PlanningAsOf time.Time
Settings Settings
Machines []Machine
Batches []BatchMeasurement
// StepInputs maps a production step to the input items it consumes, which drives the changeover model.
StepInputs map[string]map[string]bool
// MonthlyByItem is demand pooled onto each constraint item from the finished goods it becomes; DownstreamByItem keeps the per-finished-good detail so the two safety-stock echelons can be computed separately and so each finished SKU's own target can be reported rather than only its contribution to the pool.
MonthlyByItem map[string][]MonthlyDemand
DownstreamByItem map[string][]FinishedGood
// OnHandByItem is echelon stock — the constraint item plus everything downstream of it — which is what the build decision is made against. GreigeOnHandByItem is the constraint stage on its own, which the echelon total cannot be decomposed back into once summed.
OnHandByItem map[string]float64
GreigeOnHandByItem map[string]float64
// MachinesWithoutStep is how many machines in the constraint department have no production step, and therefore derive no downstream work.
MachinesWithoutStep int
Overrides []DemandOverride
ItemsByProductLine map[string][]string
// Fulfillment policy resolution. All empty means every item is make-to-stock, which is how the plan behaved before policies existed.
ItemPolicyOverrides map[string]string
ProductLineByItem map[string]string
PolicyByProductLine map[string]string
DefaultFulfillmentPolicy string
// OpenOrders is the order book the plan owes: outstanding quantities already pooled onto the constraint items that produce them. Empty means the plan is driven purely by forecast, which is how it behaved before the order book was read.
OpenOrders []OpenOrderLine
// HorizonStart is the first day of week 0, used to date the order book against the horizon.
HorizonStart time.Time
// ItemLotUnits overrides the default lot size for specific items.
ItemLotUnits map[string]float64
// LotDefaultByItem is the resolved lot each item is made in, size and unit together. Populated by the input load, which runs the whole precedence chain once.
LotDefaultByItem map[string]LotDefault
// ExcludedItemIDs are items the merchant has taken out of planning.
ExcludedItemIDs map[string]bool
// PinnedCampaigns are hand-edited campaigns already on the plan; the sweep plans around them rather than re-deriving them.
PinnedCampaigns []PinnedCampaign
DemandBasisCode string
ForecastZ float64
ForecastMonths int
// Stage two: the rest of the factory. Empty means the plan stops at the constraint, which is how it behaved before the finishing stage existed.
//
// FinishingMachines is every machine outside the constraint department; their count is what the second stage's weekly capacity is derived from, so hiring a shift onto a new machine changes the plan the way it changes the plant.
FinishingMachines []Machine
// FinishingBatches is production history for the finished goods, measured anywhere outside the constraint department. It is where the second stage's run rates come from.
FinishingBatches []BatchMeasurement
// FinishingRateScaleByItem converts a finished good's measured seconds-per-unit into the unit the plan is denominated in — the greige's. A sock scanned in eaches and knitted in pairs finishes at twice its per-each rate per planned unit.
FinishingRateScaleByItem map[string]float64
// FinishingStepByItem is where each finished good is made, denormalized onto its plan line so a department rollup needs no join.
FinishingStepByItem map[string]FinishingStep
// FinishingLotByItem is the lot each finished good is made in. Absent means the SKU is planned unlotted.
FinishingLotByItem map[string]LotDefault
}
SolverInput is everything the solver needs. It is loaded up front by the repository so this package stays pure: same input, same plan, no database, no clock.
type SolverOutput ¶
type SolverOutput struct {
SolverVersion string
PlanningAsOf time.Time
Policies []ItemPolicy
// FinishedPolicies is the per-finished-SKU decomposition of the pooled greige buffers in Policies. The two stages together are the whole inventory picture and do not overlap: greige holds its own buffer, finished goods hold theirs.
FinishedPolicies []FinishedPolicy
Campaigns []Campaign
// ProjectedOnHand[itemID][weekIndex] is the position at the end of that week.
ProjectedOnHand map[string][]float64
// Allocations say which campaign is building which order. Written alongside the plan so "what is this campaign for" and "is my order covered" are two readings of one answer.
Allocations []OrderAllocation
// FinishingLines are stage two: how many of which finished good to make from the knitted parts, week by week. Empty when the plant has no second stage configured.
FinishingLines []FinishingLine
// FinishingProjectedOnHand[itemID][weekIndex] is a finished SKU's own position at the end of that week, which the pooled greige projection cannot answer.
FinishingProjectedOnHand map[string][]float64
Diagnostics Diagnostics
}
SolverOutput is the plan plus everything needed to explain it.
func Solve ¶
func Solve(in SolverInput) SolverOutput
Solve produces the production plan.
Deterministic by construction: every collection is sorted before iteration, so the same input yields byte-identical output. See TestSolve_Deterministic.
type StepEdge ¶
StepEdge is one directed link in the production-step graph, oriented upstream → downstream. The storage table spells this (A=downstream, B=upstream); the confusion stops at the query, and everything in here is already the right way round.
type StepInfo ¶
type StepInfo struct {
StepID string
DepartmentID string
Name string
// LeadTimeOffsetWeeks is how far after the constraint campaign this step runs. It is a whole number of weeks because a schedule is planned in weeks; sub-week precision here would imply a resolution the plan does not have.
LeadTimeOffsetWeeks int
// YieldRatio is units of this step's output per unit of its input. 1 means no gain or loss; 0.95 means a 5% loss at this step.
YieldRatio float64
}
StepInfo is what the explosion needs to know about one production step.
type Transit ¶
Transit is how long the carrier takes to cover an order's lane, in the days it moves freight, and where that number came from.
type UncoveredRequirement ¶
type UncoveredRequirement struct {
ItemID string
SalesOrderID string
SalesOrderNumber string
DueWeek int
ShortUnits float64
// CoveredUnits is how much of it the plan does build, so a partly-covered order reads as partly covered rather than as a total miss.
CoveredUnits float64
}
UncoveredRequirement is an order the plan does not build in time, and by how much.