@flowselections/logistiek-engine 1.0.203 → 1.0.209
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/db/schema/logistiek-engine.sql +1333 -0
- package/dist-lib/components/BeladingIndelingCard.d.ts.map +1 -1
- package/dist-lib/components/BeladingIndelingCard.js +14 -2
- package/dist-lib/components/LadingdragersPage.d.ts.map +1 -1
- package/dist-lib/components/LadingdragersPage.js +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +498 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/floridayPush.server.d.ts.map +1 -1
- package/dist-lib/lib/floridayPush.server.js +3 -2
- package/dist-lib/lib/hoofdverpakking.d.ts +7 -0
- package/dist-lib/lib/hoofdverpakking.d.ts.map +1 -0
- package/dist-lib/lib/hoofdverpakking.js +19 -0
- package/dist-lib/lib/loadCarrierPlanner.d.ts +80 -1
- package/dist-lib/lib/loadCarrierPlanner.d.ts.map +1 -1
- package/dist-lib/lib/loadCarrierPlanner.js +353 -280
- package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
- package/dist-lib/lib/loadCarrierPlanning.server.js +39 -5
- package/dist-lib/lib/moduleVersion.d.ts +1 -1
- package/dist-lib/lib/moduleVersion.js +1 -1
- package/dist-lib/lib/quarantaine.d.ts +9 -0
- package/dist-lib/lib/quarantaine.d.ts.map +1 -0
- package/dist-lib/lib/quarantaine.js +13 -0
- package/package.json +1 -1
|
@@ -9,22 +9,126 @@
|
|
|
9
9
|
// - verticaal : totalHeightPct <= 100
|
|
10
10
|
// - horizontaal: crateShare per laag <= 1
|
|
11
11
|
// ============================================================================
|
|
12
|
-
export const LOAD_CARRIER_PLANNER_VERSION =
|
|
12
|
+
export const LOAD_CARRIER_PLANNER_VERSION = 5;
|
|
13
|
+
/** Grens vanaf waar "beladingen per laag" als onbeperkt geldt (instelling 999 = onbeperkt). */
|
|
14
|
+
export const BELADINGEN_ONBEPERKT = 999;
|
|
15
|
+
/**
|
|
16
|
+
* Leidt de menging per laag af uit de werkwijze.
|
|
17
|
+
* Niet vrij als: geen optimale vulling, maximaal N beladingen per laag (N < 999),
|
|
18
|
+
* maximaal N klanten per laag, of één klant per laag.
|
|
19
|
+
*/
|
|
20
|
+
export function bepaalLaagMenging(w) {
|
|
21
|
+
if (w.optimalFill === false)
|
|
22
|
+
return { menging: "per_regel", groepen: { klantenPerLaag: null, beladingenPerLaag: null }, reden: "optimale vulling staat uit" };
|
|
23
|
+
const b = Number(w.maxBeladingenPerLayer);
|
|
24
|
+
const bel = w.maxBeladingenPerLayer != null && Number.isFinite(b) && b > 0 && b < BELADINGEN_ONBEPERKT ? Math.round(b) : null;
|
|
25
|
+
const k = Number(w.maxCustomersPerLayer);
|
|
26
|
+
const kl = w.oneCustomerPerLayer ? 1 : w.maxCustomersPerLayer != null && Number.isFinite(k) && k > 0 ? Math.round(k) : null;
|
|
27
|
+
const delen = [];
|
|
28
|
+
if (kl != null)
|
|
29
|
+
delen.push(`maximaal ${kl} klant(en) per laag`);
|
|
30
|
+
if (bel != null)
|
|
31
|
+
delen.push(`maximaal ${bel} beladingen per laag binnen een klant`);
|
|
32
|
+
return {
|
|
33
|
+
menging: "vrij",
|
|
34
|
+
groepen: { klantenPerLaag: kl, beladingenPerLaag: bel },
|
|
35
|
+
reden: delen.length ? delen.join("; ") : "mengen per laag toegestaan",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 1.0.205: lagen op een kar volgens de mengregels.
|
|
40
|
+
* - "per_regel" (optimale vulling uit): Σ ceil(fust ÷ fusten per laag) per regel.
|
|
41
|
+
* - anders: regels vormen groepen die een laag mogen delen (zelfde klant; klanten in
|
|
42
|
+
* groepen van N; binnen een klant hoogstens M regels). Eén groep op de kar → fractie.
|
|
43
|
+
* Meer groepen → elke groep telt in hele lagen: Σ ceil(groep).
|
|
44
|
+
*/
|
|
45
|
+
export function karLagen(regels, menging = "vrij", groepen) {
|
|
46
|
+
const f = (r) => r.crates / Math.max(1, r.cpl);
|
|
47
|
+
if (menging === "per_regel")
|
|
48
|
+
return regels.reduce((s, r) => s + Math.ceil(f(r) - 1e-9), 0);
|
|
49
|
+
const N = groepen?.klantenPerLaag ?? null;
|
|
50
|
+
const M = groepen?.beladingenPerLaag ?? null;
|
|
51
|
+
if (N == null && M == null)
|
|
52
|
+
return regels.reduce((s, r) => s + f(r), 0);
|
|
53
|
+
// Per klant, vaste volgorde.
|
|
54
|
+
const perKlant = new Map();
|
|
55
|
+
for (const r of regels) {
|
|
56
|
+
const k = r.klantId ?? `regel:${r.regelId}`;
|
|
57
|
+
const l = perKlant.get(k) ?? [];
|
|
58
|
+
l.push(r);
|
|
59
|
+
perKlant.set(k, l);
|
|
60
|
+
}
|
|
61
|
+
const klanten = [...perKlant.keys()].sort();
|
|
62
|
+
const units = [];
|
|
63
|
+
if (M != null) {
|
|
64
|
+
// Binnen een klant groepen van M regels; klanten delen niet als N gezet is.
|
|
65
|
+
for (const k of klanten) {
|
|
66
|
+
const l = perKlant.get(k).sort((a, b) => a.regelId.localeCompare(b.regelId));
|
|
67
|
+
for (let i = 0; i < l.length; i += M)
|
|
68
|
+
units.push(l.slice(i, i + M).reduce((s, r) => s + f(r), 0));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
for (let i = 0; i < klanten.length; i += N) {
|
|
73
|
+
units.push(klanten.slice(i, i + N).reduce((s, k) => s + perKlant.get(k).reduce((t, r) => t + f(r), 0), 0));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (units.length <= 1)
|
|
77
|
+
return units.reduce((s, u) => s + u, 0);
|
|
78
|
+
return units.reduce((s, u) => s + Math.ceil(u - 1e-9), 0);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 1.0.208: vulgraad van een kar zoals Floriday (fractie, 1 = vol):
|
|
82
|
+
* Σ per regel (lagen van de regel ÷ lagen per kar van die regel).
|
|
83
|
+
* Mengregels blijven: een afgeronde laag (tussen groepen die geen laag mogen delen)
|
|
84
|
+
* telt met de kleinste lagen-per-kar binnen die groep.
|
|
85
|
+
*/
|
|
86
|
+
export function karVulgraadFractie(regels, menging = "vrij", groepen) {
|
|
87
|
+
const f = (r) => r.crates / Math.max(1, r.cpl);
|
|
88
|
+
const l = (r) => Math.max(1, Number(r.lpc) || 1);
|
|
89
|
+
if (menging === "per_regel")
|
|
90
|
+
return regels.reduce((s, r) => s + Math.ceil(f(r) - 1e-9) / l(r), 0);
|
|
91
|
+
const N = groepen?.klantenPerLaag ?? null;
|
|
92
|
+
const M = groepen?.beladingenPerLaag ?? null;
|
|
93
|
+
if (N == null && M == null)
|
|
94
|
+
return regels.reduce((s, r) => s + f(r) / l(r), 0);
|
|
95
|
+
const perKlant = new Map();
|
|
96
|
+
for (const r of regels) {
|
|
97
|
+
const k = r.klantId ?? `regel:${r.regelId}`;
|
|
98
|
+
const lijst = perKlant.get(k) ?? [];
|
|
99
|
+
lijst.push(r);
|
|
100
|
+
perKlant.set(k, lijst);
|
|
101
|
+
}
|
|
102
|
+
const klanten = [...perKlant.keys()].sort();
|
|
103
|
+
const units = [];
|
|
104
|
+
if (M != null) {
|
|
105
|
+
for (const k of klanten) {
|
|
106
|
+
const lijst = perKlant.get(k).sort((a, b) => a.regelId.localeCompare(b.regelId));
|
|
107
|
+
for (let i = 0; i < lijst.length; i += M)
|
|
108
|
+
units.push(lijst.slice(i, i + M));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
for (let i = 0; i < klanten.length; i += N)
|
|
113
|
+
units.push(klanten.slice(i, i + N).flatMap((k) => perKlant.get(k)));
|
|
114
|
+
}
|
|
115
|
+
if (units.length <= 1)
|
|
116
|
+
return regels.reduce((s, r) => s + f(r) / l(r), 0);
|
|
117
|
+
return units.reduce((s, u) => {
|
|
118
|
+
const lagen = u.reduce((t, r) => t + f(r), 0);
|
|
119
|
+
return s + Math.ceil(lagen - 1e-9) / Math.min(...u.map(l));
|
|
120
|
+
}, 0);
|
|
121
|
+
}
|
|
122
|
+
/** Lagen van één regel op een kar volgens de menging per laag. */
|
|
123
|
+
export function regelLagen(crates, cpl, menging = "vrij") {
|
|
124
|
+
const f = crates / Math.max(1, cpl);
|
|
125
|
+
return menging === "per_regel" ? Math.ceil(f - 1e-9) : f;
|
|
126
|
+
}
|
|
13
127
|
const EPS = 1e-9;
|
|
14
128
|
function clampInt(value, min) {
|
|
15
129
|
const n = typeof value === "string" ? Number(value) : value;
|
|
16
130
|
return Math.max(min, Math.round(Number.isFinite(n) ? n : min));
|
|
17
131
|
}
|
|
18
|
-
/** Effectieve lagen: het minimum van de productbelading en de ladingdrager. */
|
|
19
|
-
function effectiveLayers(item, carrier) {
|
|
20
|
-
const carrierLayers = Math.max(1, Math.round(carrier.layers_per_carrier || 1));
|
|
21
|
-
if (carrier.capacity_model === "single" || carrier.capacity_model === "none")
|
|
22
|
-
return 1;
|
|
23
|
-
// Floriday-verpakking is leidend en wordt niet afgeknepen op het profiel.
|
|
24
|
-
if (item.packing_authoritative)
|
|
25
|
-
return Math.max(1, Math.round(item.layers_per_cart));
|
|
26
|
-
return Math.max(1, Math.min(item.layers_per_cart, carrierLayers));
|
|
27
|
-
}
|
|
28
132
|
/** Effectieve fusten per laag: begrensd door de ladingdrager. */
|
|
29
133
|
function effectiveCratesPerLayer(item, carrier) {
|
|
30
134
|
const cap = carrier.max_crates_per_layer;
|
|
@@ -33,197 +137,6 @@ function effectiveCratesPerLayer(item, carrier) {
|
|
|
33
137
|
return Math.max(1, Math.min(base, Math.round(cap)));
|
|
34
138
|
return base;
|
|
35
139
|
}
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
// Plaatsingsregels
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
function placeAsFreshLayer(cart, batch, batchIdx, crates) {
|
|
40
|
-
if (cart.totalHeightPct + batch.layerHeightPct > 100 + EPS)
|
|
41
|
-
return false;
|
|
42
|
-
cart.layers.push({
|
|
43
|
-
heightPct: batch.layerHeightPct,
|
|
44
|
-
crateShare: crates / batch.cratesPerLayer,
|
|
45
|
-
entries: [{ batchIdx, crates }],
|
|
46
|
-
customerIds: new Set(batch.input.customer_id ? [batch.input.customer_id] : []),
|
|
47
|
-
});
|
|
48
|
-
cart.totalHeightPct += batch.layerHeightPct;
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
function placeInLayer(cart, batch, batchIdx, crates, r) {
|
|
52
|
-
const partyShare = crates / batch.cratesPerLayer;
|
|
53
|
-
const maxShare = r.opts.maxPartySharePerLayerPct / 100;
|
|
54
|
-
// REGEL 1 — één partij mag nooit meer dan X% van een laag beslaan
|
|
55
|
-
if (partyShare > maxShare + EPS)
|
|
56
|
-
return false;
|
|
57
|
-
// REGEL 2 — never-mix producten en niet-mengbare ladingdragers
|
|
58
|
-
const neverMix = r.opts.carrier.allow_mixing === false ||
|
|
59
|
-
(batch.input.product_id ? r.opts.neverMixProductIds.has(batch.input.product_id) : false);
|
|
60
|
-
if (neverMix)
|
|
61
|
-
return false;
|
|
62
|
-
for (const layer of cart.layers) {
|
|
63
|
-
// REGEL 3 — max aantal verschillende beladingen per laag
|
|
64
|
-
if (layer.entries.length >= r.opts.maxBeladingenPerLayer)
|
|
65
|
-
continue;
|
|
66
|
-
// REGEL 4 — dezelfde partij niet twee keer in dezelfde laag
|
|
67
|
-
if (layer.entries.some((e) => e.batchIdx === batchIdx))
|
|
68
|
-
continue;
|
|
69
|
-
// REGEL 4b — maximaal aantal verschillende klanten op één laag
|
|
70
|
-
const layerCustomerLimit = r.opts.oneCustomerPerLayer
|
|
71
|
-
? 1
|
|
72
|
-
: r.opts.maxCustomersPerLayer && r.opts.maxCustomersPerLayer > 0
|
|
73
|
-
? r.opts.maxCustomersPerLayer
|
|
74
|
-
: null;
|
|
75
|
-
const customerId = batch.input.customer_id ?? null;
|
|
76
|
-
if (layerCustomerLimit != null &&
|
|
77
|
-
customerId &&
|
|
78
|
-
!layer.customerIds.has(customerId) &&
|
|
79
|
-
layer.customerIds.size >= layerCustomerLimit) {
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
// REGEL 5 — vloeroppervlak van de laag mag niet over 100%
|
|
83
|
-
if (layer.crateShare + partyShare > 1 + EPS)
|
|
84
|
-
continue;
|
|
85
|
-
// REGEL 6 — hoogte: bij mengen telt de HOOGSTE partij
|
|
86
|
-
const newHeight = Math.max(layer.heightPct, batch.layerHeightPct);
|
|
87
|
-
const heightDelta = newHeight - layer.heightPct;
|
|
88
|
-
if (cart.totalHeightPct + heightDelta > 100 + EPS)
|
|
89
|
-
continue;
|
|
90
|
-
layer.entries.push({ batchIdx, crates });
|
|
91
|
-
if (customerId)
|
|
92
|
-
layer.customerIds.add(customerId);
|
|
93
|
-
layer.crateShare += partyShare;
|
|
94
|
-
layer.heightPct = newHeight;
|
|
95
|
-
cart.totalHeightPct += heightDelta;
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
/** Extra grenzen die niet over hoogte of oppervlak gaan. */
|
|
101
|
-
function acceptsBatch(cart, batch, crates, r) {
|
|
102
|
-
const carrier = r.opts.carrier;
|
|
103
|
-
const orderId = batch.input.order_id ?? null;
|
|
104
|
-
if (orderId && !cart.orderIds.has(orderId) && r.opts.maxOrders && cart.orderIds.size >= r.opts.maxOrders) {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
const customerId = batch.input.customer_id ?? null;
|
|
108
|
-
if (customerId &&
|
|
109
|
-
!cart.customerIds.has(customerId) &&
|
|
110
|
-
r.opts.maxCustomers &&
|
|
111
|
-
cart.customerIds.size >= r.opts.maxCustomers) {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
if (typeof carrier.max_boxes === "number" && carrier.max_boxes > 0) {
|
|
115
|
-
if (cart.boxes + crates > carrier.max_boxes)
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
if (typeof carrier.max_weight_kg === "number" && carrier.max_weight_kg > 0) {
|
|
119
|
-
const perCrate = Number(batch.input.weight_per_crate ?? 0) || 0;
|
|
120
|
-
if (perCrate > 0 && cart.weightKg + perCrate * crates > carrier.max_weight_kg + EPS)
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
function registerPlacement(cart, batch, crates) {
|
|
126
|
-
if (batch.input.order_id)
|
|
127
|
-
cart.orderIds.add(batch.input.order_id);
|
|
128
|
-
if (batch.input.customer_id)
|
|
129
|
-
cart.customerIds.add(batch.input.customer_id);
|
|
130
|
-
cart.boxes += crates;
|
|
131
|
-
cart.weightKg += (Number(batch.input.weight_per_crate ?? 0) || 0) * crates;
|
|
132
|
-
}
|
|
133
|
-
function tryPlaceBatch(cart, batch, batchIdx, r) {
|
|
134
|
-
const cpl = batch.cratesPerLayer;
|
|
135
|
-
let placed = 0;
|
|
136
|
-
let remaining = batch.cratesRemaining;
|
|
137
|
-
// 1) zo veel mogelijk VOLLE eigen lagen
|
|
138
|
-
while (remaining >= cpl && cart.totalHeightPct + batch.layerHeightPct <= 100 + EPS) {
|
|
139
|
-
if (!acceptsBatch(cart, batch, cpl, r))
|
|
140
|
-
break;
|
|
141
|
-
if (!placeAsFreshLayer(cart, batch, batchIdx, cpl))
|
|
142
|
-
break;
|
|
143
|
-
registerPlacement(cart, batch, cpl);
|
|
144
|
-
remaining -= cpl;
|
|
145
|
-
placed += cpl;
|
|
146
|
-
}
|
|
147
|
-
// 2) het restant: eerst bijmengen, anders eigen (deels lege) laag
|
|
148
|
-
if (remaining > 0) {
|
|
149
|
-
const partial = Math.min(remaining, Math.ceil(cpl * 0.5));
|
|
150
|
-
if (acceptsBatch(cart, batch, partial, r) && placeInLayer(cart, batch, batchIdx, partial, r)) {
|
|
151
|
-
registerPlacement(cart, batch, partial);
|
|
152
|
-
remaining -= partial;
|
|
153
|
-
placed += partial;
|
|
154
|
-
}
|
|
155
|
-
else if (acceptsBatch(cart, batch, remaining, r) && placeAsFreshLayer(cart, batch, batchIdx, remaining)) {
|
|
156
|
-
registerPlacement(cart, batch, remaining);
|
|
157
|
-
placed += remaining;
|
|
158
|
-
remaining = 0;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return placed;
|
|
162
|
-
}
|
|
163
|
-
// ---------------------------------------------------------------------------
|
|
164
|
-
// Samenvatten
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
function summarizeCart(cart, rests, cartNumber, r) {
|
|
167
|
-
const byBatch = new Map();
|
|
168
|
-
for (const layer of cart.layers) {
|
|
169
|
-
const shareSum = layer.entries.reduce((s, e) => s + e.crates, 0);
|
|
170
|
-
for (const e of layer.entries) {
|
|
171
|
-
const cur = byBatch.get(e.batchIdx) ?? { crates: 0, layerFraction: 0, heightPct: 0 };
|
|
172
|
-
const ratio = shareSum > 0 ? e.crates / shareSum : 0;
|
|
173
|
-
cur.crates += e.crates;
|
|
174
|
-
cur.layerFraction += ratio;
|
|
175
|
-
cur.heightPct += layer.heightPct * ratio;
|
|
176
|
-
byBatch.set(e.batchIdx, cur);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
const totalHeight = cart.totalHeightPct;
|
|
180
|
-
const items = [];
|
|
181
|
-
let weight = 0;
|
|
182
|
-
for (const [batchIdx, agg] of byBatch) {
|
|
183
|
-
const input = rests[batchIdx].input;
|
|
184
|
-
const perCrate = Number(input.weight_per_crate ?? 0) || 0;
|
|
185
|
-
weight += perCrate * agg.crates;
|
|
186
|
-
items.push({
|
|
187
|
-
order_item_id: input.order_item_id,
|
|
188
|
-
order_id: input.order_id ?? null,
|
|
189
|
-
product_id: input.product_id,
|
|
190
|
-
product_name: input.product_name,
|
|
191
|
-
crates: agg.crates,
|
|
192
|
-
plants: agg.crates * input.plants_per_crate,
|
|
193
|
-
boxes: agg.crates,
|
|
194
|
-
weight_kg: perCrate > 0 ? Math.round(perCrate * agg.crates * 100) / 100 : null,
|
|
195
|
-
plants_per_crate: input.plants_per_crate,
|
|
196
|
-
crates_per_layer: rests[batchIdx].cratesPerLayer,
|
|
197
|
-
layers_per_cart: Math.max(1, Math.round(100 / rests[batchIdx].layerHeightPct)),
|
|
198
|
-
// 1.0.190: layer_fraction = vulling per laag (0–1); lagen = fusten ÷ fusten per laag.
|
|
199
|
-
layer_fraction: Math.min(1, Math.round((agg.crates / Math.max(1, rests[batchIdx].cratesPerLayer)) * 1000) / 1000),
|
|
200
|
-
lagen: Math.round((agg.crates / Math.max(1, rests[batchIdx].cratesPerLayer)) * 1000) / 1000,
|
|
201
|
-
layer_share_pct: agg.heightPct,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
return {
|
|
205
|
-
cart_number: cartNumber,
|
|
206
|
-
carrier_type: r.opts.carrier.code,
|
|
207
|
-
carrier_type_id: r.opts.carrier.id ?? null,
|
|
208
|
-
source: "auto",
|
|
209
|
-
is_complete: Math.abs(cart.totalHeightPct - 100) < 1e-6,
|
|
210
|
-
occupancy_pct: Math.round(cart.totalHeightPct * 100) / 100,
|
|
211
|
-
total_layers_used: cart.layers.length,
|
|
212
|
-
max_layers: Math.max(1, Math.round(r.opts.carrier.layers_per_carrier || 1)),
|
|
213
|
-
weight_kg: weight > 0 ? Math.round(weight * 100) / 100 : null,
|
|
214
|
-
items,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
function newCart() {
|
|
218
|
-
return {
|
|
219
|
-
layers: [],
|
|
220
|
-
totalHeightPct: 0,
|
|
221
|
-
boxes: 0,
|
|
222
|
-
weightKg: 0,
|
|
223
|
-
orderIds: new Set(),
|
|
224
|
-
customerIds: new Set(),
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
140
|
function fullCart(input, crates, cratesPerLayer, layers, cartNumber, r) {
|
|
228
141
|
const perCrate = Number(input.weight_per_crate ?? 0) || 0;
|
|
229
142
|
return {
|
|
@@ -232,9 +145,9 @@ function fullCart(input, crates, cratesPerLayer, layers, cartNumber, r) {
|
|
|
232
145
|
carrier_type_id: r.opts.carrier.id ?? null,
|
|
233
146
|
source: "auto",
|
|
234
147
|
is_complete: true,
|
|
235
|
-
occupancy_pct: 100,
|
|
236
|
-
total_layers_used:
|
|
237
|
-
max_layers:
|
|
148
|
+
occupancy_pct: Math.round((10000 * crates) / Math.max(1, cratesPerLayer * layers)) / 100,
|
|
149
|
+
total_layers_used: Math.round((crates / Math.max(1, cratesPerLayer)) * 100) / 100,
|
|
150
|
+
max_layers: layers,
|
|
238
151
|
weight_kg: perCrate > 0 ? Math.round(perCrate * crates * 100) / 100 : null,
|
|
239
152
|
items: [
|
|
240
153
|
{
|
|
@@ -272,8 +185,188 @@ function sortInputs(items, sortOrder) {
|
|
|
272
185
|
}
|
|
273
186
|
}
|
|
274
187
|
// ---------------------------------------------------------------------------
|
|
275
|
-
//
|
|
188
|
+
// 1.0.204: capaciteit per kar (zelfde regel als Floriday)
|
|
276
189
|
// ---------------------------------------------------------------------------
|
|
190
|
+
// lagen op de kar = Σ (fust ÷ fusten per laag) per regel
|
|
191
|
+
// capaciteit = hoogste "lagen per kar" van de producten op de kar
|
|
192
|
+
// vol = lagen ≥ capaciteit (− 0,001)
|
|
193
|
+
// verdelen = vul een kar tot de capaciteit, rest naar de volgende kar
|
|
194
|
+
// Voegt een product met meer lagen per kar toe, dan stijgt de capaciteit.
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
export const LAGEN_MARGE = 0.001;
|
|
197
|
+
/** Capaciteit van een kar: hoogste lagen-per-kar van de producten; anders de ladingdrager. */
|
|
198
|
+
export function karCapaciteit(productLagen, ladingdragerLagen) {
|
|
199
|
+
const geldig = productLagen.map(Number).filter((n) => Number.isFinite(n) && n > 0);
|
|
200
|
+
if (geldig.length > 0)
|
|
201
|
+
return { capaciteit: Math.max(...geldig), bron: "product" };
|
|
202
|
+
const l = Number(ladingdragerLagen);
|
|
203
|
+
if (ladingdragerLagen != null && Number.isFinite(l) && l > 0)
|
|
204
|
+
return { capaciteit: l, bron: "ladingdrager" };
|
|
205
|
+
return { capaciteit: null, bron: null };
|
|
206
|
+
}
|
|
207
|
+
/** Vulgraad in procenten (heel getal), of null als de capaciteit onbekend is. */
|
|
208
|
+
export function karVulgraad(lagen, capaciteit) {
|
|
209
|
+
if (capaciteit == null || !(capaciteit > 0) || !Number.isFinite(lagen))
|
|
210
|
+
return null;
|
|
211
|
+
return Math.round((100 * lagen) / capaciteit);
|
|
212
|
+
}
|
|
213
|
+
function productLagen(item, carrier) {
|
|
214
|
+
if (carrier.capacity_model === "single")
|
|
215
|
+
return 1;
|
|
216
|
+
let l = Math.max(1, Math.round(item.layers_per_cart));
|
|
217
|
+
const grens = carrier.werkwijze_max_lagen;
|
|
218
|
+
if (typeof grens === "number" && grens > 0 && !item.packing_authoritative)
|
|
219
|
+
l = Math.min(l, Math.round(grens));
|
|
220
|
+
return l;
|
|
221
|
+
}
|
|
222
|
+
function accepteert(kar, rest, crates, r) {
|
|
223
|
+
const c = r.opts.carrier;
|
|
224
|
+
const orderId = rest.input.order_id ?? null;
|
|
225
|
+
if (orderId && !kar.orderIds.has(orderId) && r.opts.maxOrders && kar.orderIds.size >= r.opts.maxOrders)
|
|
226
|
+
return false;
|
|
227
|
+
const klant = rest.input.customer_id ?? null;
|
|
228
|
+
if (klant && !kar.customerIds.has(klant) && r.opts.maxCustomers && kar.customerIds.size >= r.opts.maxCustomers)
|
|
229
|
+
return false;
|
|
230
|
+
if (typeof c.max_boxes === "number" && c.max_boxes > 0 && kar.boxes + crates > c.max_boxes)
|
|
231
|
+
return false;
|
|
232
|
+
if (typeof c.max_weight_kg === "number" && c.max_weight_kg > 0) {
|
|
233
|
+
const pc = Number(rest.input.weight_per_crate ?? 0) || 0;
|
|
234
|
+
if (pc > 0 && kar.weightKg + pc * crates > c.max_weight_kg + EPS)
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
/** Hoeveel fusten van deze rest passen nog op de kar (0 = niets). */
|
|
240
|
+
/** Lagen van een kar volgens de mengregels, optioneel met extra fusten van een rest. */
|
|
241
|
+
function regelsVan(kar, rests, extra) {
|
|
242
|
+
const per = new Map();
|
|
243
|
+
for (const e of kar.entries)
|
|
244
|
+
per.set(e.restIdx, (per.get(e.restIdx) ?? 0) + e.crates);
|
|
245
|
+
if (extra)
|
|
246
|
+
per.set(extra.restIdx, (per.get(extra.restIdx) ?? 0) + extra.crates);
|
|
247
|
+
return [...per.entries()].map(([i, crates]) => ({
|
|
248
|
+
regelId: rests[i].input.order_item_id,
|
|
249
|
+
klantId: rests[i].input.customer_id ?? rests[i].input.order_id ?? null,
|
|
250
|
+
crates,
|
|
251
|
+
cpl: rests[i].cpl,
|
|
252
|
+
lpc: rests[i].lpc,
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
function vulVan(kar, rests, r, extra) {
|
|
256
|
+
return karVulgraadFractie(regelsVan(kar, rests, extra), r.opts.laagMenging, r.opts.laagGroepen);
|
|
257
|
+
}
|
|
258
|
+
function lagenVan(kar, rests, r, extra) {
|
|
259
|
+
const per = new Map();
|
|
260
|
+
for (const e of kar.entries)
|
|
261
|
+
per.set(e.restIdx, (per.get(e.restIdx) ?? 0) + e.crates);
|
|
262
|
+
if (extra)
|
|
263
|
+
per.set(extra.restIdx, (per.get(extra.restIdx) ?? 0) + extra.crates);
|
|
264
|
+
const regels = [...per.entries()].map(([i, crates]) => ({
|
|
265
|
+
regelId: rests[i].input.order_item_id,
|
|
266
|
+
klantId: rests[i].input.customer_id ?? rests[i].input.order_id ?? null,
|
|
267
|
+
crates,
|
|
268
|
+
cpl: rests[i].cpl,
|
|
269
|
+
}));
|
|
270
|
+
return karLagen(regels, r.opts.laagMenging, r.opts.laagGroepen);
|
|
271
|
+
}
|
|
272
|
+
function pastErop(kar, rest, r, rests, restIdx) {
|
|
273
|
+
const neverMix = r.opts.carrier.allow_mixing === false ||
|
|
274
|
+
(rest.input.product_id ? r.opts.neverMixProductIds.has(rest.input.product_id) : false);
|
|
275
|
+
if (kar.entries.length > 0 && (neverMix || kar.neverMix))
|
|
276
|
+
return 0;
|
|
277
|
+
// 1.0.208: vol = vulgraad 100 % (Σ lagen van regel ÷ lagen per kar van die regel).
|
|
278
|
+
const vrij = 1 - kar.vul;
|
|
279
|
+
if (vrij <= LAGEN_MARGE)
|
|
280
|
+
return 0;
|
|
281
|
+
let n = Math.min(rest.crates, Math.floor((vrij + LAGEN_MARGE) * rest.lpc * rest.cpl + 1e-9));
|
|
282
|
+
const c = r.opts.carrier;
|
|
283
|
+
if (typeof c.max_boxes === "number" && c.max_boxes > 0)
|
|
284
|
+
n = Math.min(n, c.max_boxes - kar.boxes);
|
|
285
|
+
if (typeof c.max_weight_kg === "number" && c.max_weight_kg > 0) {
|
|
286
|
+
const pc = Number(rest.input.weight_per_crate ?? 0) || 0;
|
|
287
|
+
if (pc > 0)
|
|
288
|
+
n = Math.min(n, Math.floor((c.max_weight_kg - kar.weightKg + EPS) / pc));
|
|
289
|
+
}
|
|
290
|
+
// Mengregels: grootste n waarbij de kar binnen de capaciteit blijft (monotoon → binair zoeken).
|
|
291
|
+
const past = (m) => vulVan(kar, rests, r, { restIdx, crates: m }) <= 1 + LAGEN_MARGE;
|
|
292
|
+
if (n > 0 && !past(n)) {
|
|
293
|
+
let lo = 0, hi = n;
|
|
294
|
+
while (lo < hi) {
|
|
295
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
296
|
+
if (past(mid))
|
|
297
|
+
lo = mid;
|
|
298
|
+
else
|
|
299
|
+
hi = mid - 1;
|
|
300
|
+
}
|
|
301
|
+
n = lo;
|
|
302
|
+
}
|
|
303
|
+
if (n <= 0 || !accepteert(kar, rest, n, r))
|
|
304
|
+
return 0;
|
|
305
|
+
return n;
|
|
306
|
+
}
|
|
307
|
+
function zetErop(kar, rest, restIdx, crates, r, rests) {
|
|
308
|
+
kar.entries.push({ restIdx, crates });
|
|
309
|
+
kar.capaciteit = Math.max(kar.capaciteit, rest.lpc);
|
|
310
|
+
kar.boxes += crates;
|
|
311
|
+
kar.weightKg += (Number(rest.input.weight_per_crate ?? 0) || 0) * crates;
|
|
312
|
+
if (rest.input.order_id)
|
|
313
|
+
kar.orderIds.add(rest.input.order_id);
|
|
314
|
+
if (rest.input.customer_id)
|
|
315
|
+
kar.customerIds.add(rest.input.customer_id);
|
|
316
|
+
if (r.opts.carrier.allow_mixing === false ||
|
|
317
|
+
(rest.input.product_id ? r.opts.neverMixProductIds.has(rest.input.product_id) : false))
|
|
318
|
+
kar.neverMix = true;
|
|
319
|
+
rest.crates -= crates;
|
|
320
|
+
kar.lagen = lagenVan(kar, rests, r);
|
|
321
|
+
kar.vul = vulVan(kar, rests, r);
|
|
322
|
+
}
|
|
323
|
+
function nieuweKar() {
|
|
324
|
+
return { entries: [], lagen: 0, vul: 0, capaciteit: 0, boxes: 0, weightKg: 0, orderIds: new Set(), customerIds: new Set(), neverMix: false, eigen: false };
|
|
325
|
+
}
|
|
326
|
+
function samenvatten(kar, rests, nr, r) {
|
|
327
|
+
const per = new Map();
|
|
328
|
+
for (const e of kar.entries)
|
|
329
|
+
per.set(e.restIdx, (per.get(e.restIdx) ?? 0) + e.crates);
|
|
330
|
+
const items = [];
|
|
331
|
+
let weight = 0;
|
|
332
|
+
const sleutel = (i) => `${rests[i].input.product_name}\u0000${rests[i].input.order_item_id}`;
|
|
333
|
+
for (const [idx, crates] of [...per.entries()].sort((a, b) => sleutel(a[0]).localeCompare(sleutel(b[0])))) {
|
|
334
|
+
const rest = rests[idx];
|
|
335
|
+
const input = rest.input;
|
|
336
|
+
const pc = Number(input.weight_per_crate ?? 0) || 0;
|
|
337
|
+
weight += pc * crates;
|
|
338
|
+
const lagen = regelLagen(crates, rest.cpl, r.opts.laagMenging);
|
|
339
|
+
items.push({
|
|
340
|
+
order_item_id: input.order_item_id,
|
|
341
|
+
order_id: input.order_id ?? null,
|
|
342
|
+
product_id: input.product_id,
|
|
343
|
+
product_name: input.product_name,
|
|
344
|
+
crates,
|
|
345
|
+
plants: crates * input.plants_per_crate,
|
|
346
|
+
boxes: crates,
|
|
347
|
+
weight_kg: pc > 0 ? Math.round(pc * crates * 100) / 100 : null,
|
|
348
|
+
plants_per_crate: input.plants_per_crate,
|
|
349
|
+
crates_per_layer: rest.cpl,
|
|
350
|
+
layers_per_cart: rest.lpc,
|
|
351
|
+
layer_fraction: Math.min(1, Math.round(lagen * 1000) / 1000),
|
|
352
|
+
lagen: Math.round(lagen * 1000) / 1000,
|
|
353
|
+
layer_share_pct: Math.round((10000 * lagen) / kar.capaciteit) / 100,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
const lagen = Math.round(kar.lagen * 100) / 100;
|
|
357
|
+
return {
|
|
358
|
+
cart_number: nr,
|
|
359
|
+
carrier_type: r.opts.carrier.code,
|
|
360
|
+
carrier_type_id: r.opts.carrier.id ?? null,
|
|
361
|
+
source: "auto",
|
|
362
|
+
is_complete: kar.vul >= 1 - LAGEN_MARGE,
|
|
363
|
+
occupancy_pct: Math.round(10000 * kar.vul) / 100,
|
|
364
|
+
total_layers_used: lagen,
|
|
365
|
+
max_layers: kar.capaciteit,
|
|
366
|
+
weight_kg: weight > 0 ? Math.round(weight * 100) / 100 : null,
|
|
367
|
+
items,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
277
370
|
export function planLoadUnits(inputs, options) {
|
|
278
371
|
const carrier = options.carrier;
|
|
279
372
|
const r = {
|
|
@@ -288,6 +381,8 @@ export function planLoadUnits(inputs, options) {
|
|
|
288
381
|
oneCustomerPerLayer: options.oneCustomerPerLayer ?? false,
|
|
289
382
|
ownCarrierFromLayers: options.ownCarrierFromLayers ?? null,
|
|
290
383
|
sortOrder: options.sortOrder ?? "none",
|
|
384
|
+
laagMenging: options.laagMenging ?? "vrij",
|
|
385
|
+
laagGroepen: options.laagGroepen ?? null,
|
|
291
386
|
},
|
|
292
387
|
};
|
|
293
388
|
const warnings = [];
|
|
@@ -301,7 +396,6 @@ export function planLoadUnits(inputs, options) {
|
|
|
301
396
|
layers_per_cart: clampInt(i.layers_per_cart, 1),
|
|
302
397
|
}))
|
|
303
398
|
.filter((i) => i.plants > 0), r.opts.sortOrder);
|
|
304
|
-
// --- Model "none": elke partij is één losse eenheid, geen menging ---------
|
|
305
399
|
if (carrier.capacity_model === "none") {
|
|
306
400
|
let n = 0;
|
|
307
401
|
for (const it of items) {
|
|
@@ -310,91 +404,70 @@ export function planLoadUnits(inputs, options) {
|
|
|
310
404
|
}
|
|
311
405
|
return { carts, warnings };
|
|
312
406
|
}
|
|
407
|
+
// STAP A: volle karren per product (capaciteit = lagen per kar van dat product).
|
|
313
408
|
const rests = [];
|
|
314
|
-
let cartNumber = 0;
|
|
315
|
-
// --- STAP A: volledige ladingdragers per artikel --------------------------
|
|
316
409
|
for (const it of items) {
|
|
317
|
-
const
|
|
410
|
+
const lpc = productLagen(it, carrier);
|
|
318
411
|
const cpl = effectiveCratesPerLayer(it, carrier);
|
|
319
|
-
let
|
|
320
|
-
if (typeof carrier.max_boxes === "number" && carrier.max_boxes > 0)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
// 1.0.190: die drager blijft open voor restfusten (hele fusten, eigen laag
|
|
331
|
-
// of bijmengen volgens de laagregels), zodat er geen extra kar ontstaat.
|
|
332
|
-
const own = r.opts.ownCarrierFromLayers;
|
|
333
|
-
const ownCarrier = Boolean(own && own > 0 && remainderCrates >= cpl * (own ?? 0));
|
|
334
|
-
if (remainderCrates > 0) {
|
|
335
|
-
rests.push({
|
|
336
|
-
input: it,
|
|
337
|
-
cratesRemaining: remainderCrates,
|
|
338
|
-
layerHeightPct: 100 / layers,
|
|
339
|
-
cratesPerLayer: cpl,
|
|
340
|
-
beladingKey: `${it.plants_per_crate}x${cpl}x${layers}`,
|
|
341
|
-
ownCarrier,
|
|
342
|
-
});
|
|
412
|
+
let perKar = cpl * lpc;
|
|
413
|
+
if (typeof carrier.max_boxes === "number" && carrier.max_boxes > 0)
|
|
414
|
+
perKar = Math.min(perKar, carrier.max_boxes);
|
|
415
|
+
const totaal = Math.ceil(it.plants / it.plants_per_crate);
|
|
416
|
+
const vol = Math.floor(totaal / perKar);
|
|
417
|
+
for (let c = 0; c < vol; c++)
|
|
418
|
+
carts.push(fullCart(it, perKar, cpl, lpc, carts.length + 1, r));
|
|
419
|
+
const rest = totaal - vol * perKar;
|
|
420
|
+
if (rest > 0) {
|
|
421
|
+
const own = r.opts.ownCarrierFromLayers;
|
|
422
|
+
rests.push({ input: it, crates: rest, cpl, lpc, ownCarrier: Boolean(own && own > 0 && rest >= cpl * own) });
|
|
343
423
|
}
|
|
344
424
|
}
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
425
|
+
// STAP B: restpartijen. Vaste volgorde (deterministisch): eigen drager eerst,
|
|
426
|
+
// dan hoogste capaciteit, dan meeste lagen, dan productnaam en regel-id.
|
|
427
|
+
const volgorde = rests
|
|
428
|
+
.map((rest, idx) => ({ rest, idx }))
|
|
429
|
+
.sort((a, b) => {
|
|
430
|
+
if (a.rest.ownCarrier !== b.rest.ownCarrier)
|
|
431
|
+
return a.rest.ownCarrier ? -1 : 1;
|
|
432
|
+
if (a.rest.lpc !== b.rest.lpc)
|
|
433
|
+
return b.rest.lpc - a.rest.lpc;
|
|
434
|
+
const la = a.rest.crates / a.rest.cpl, lb = b.rest.crates / b.rest.cpl;
|
|
435
|
+
if (Math.abs(la - lb) > 1e-9)
|
|
436
|
+
return lb - la;
|
|
437
|
+
return (a.rest.input.product_name.localeCompare(b.rest.input.product_name) ||
|
|
438
|
+
a.rest.input.order_item_id.localeCompare(b.rest.input.order_item_id));
|
|
352
439
|
});
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
for (let bi = 0; bi < rests.length; bi++) {
|
|
356
|
-
const batch = rests[bi];
|
|
440
|
+
const open = [];
|
|
441
|
+
for (const { rest, idx } of volgorde) {
|
|
357
442
|
let guard = 0;
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
batch.cratesRemaining -= placed;
|
|
443
|
+
while (rest.crates > 0 && guard++ < 10000) {
|
|
444
|
+
let geplaatst = false;
|
|
445
|
+
const kandidaten = rest.ownCarrier ? [] : open;
|
|
446
|
+
for (const kar of kandidaten) {
|
|
447
|
+
const n = pastErop(kar, rest, r, rests, idx);
|
|
448
|
+
if (n > 0) {
|
|
449
|
+
zetErop(kar, rest, idx, n, r, rests);
|
|
450
|
+
geplaatst = true;
|
|
367
451
|
break;
|
|
368
452
|
}
|
|
369
453
|
}
|
|
370
|
-
if (
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
crateShare: 1,
|
|
378
|
-
entries: [{ batchIdx: bi, crates: batch.cratesRemaining }],
|
|
379
|
-
customerIds: new Set(batch.input.customer_id ? [batch.input.customer_id] : []),
|
|
380
|
-
});
|
|
381
|
-
cart.totalHeightPct = 100;
|
|
382
|
-
registerPlacement(cart, batch, batch.cratesRemaining);
|
|
383
|
-
placed = batch.cratesRemaining;
|
|
384
|
-
}
|
|
385
|
-
openCarts.push(cart);
|
|
386
|
-
if (batch.ownCarrier && !eigenDrager)
|
|
387
|
-
eigenDrager = cart;
|
|
388
|
-
batch.cratesRemaining -= placed;
|
|
454
|
+
if (geplaatst)
|
|
455
|
+
continue;
|
|
456
|
+
const kar = nieuweKar();
|
|
457
|
+
kar.eigen = rest.ownCarrier;
|
|
458
|
+
const n = pastErop(kar, rest, r, rests, idx);
|
|
459
|
+
if (n > 0) {
|
|
460
|
+
zetErop(kar, rest, idx, n, r, rests);
|
|
389
461
|
}
|
|
462
|
+
else {
|
|
463
|
+
warnings.push(`Partij "${rest.input.product_name}" past niet volgens de grenzen van ${carrier.name} — geplaatst als losse ladingdrager.`);
|
|
464
|
+
zetErop(kar, rest, idx, rest.crates, r, rests);
|
|
465
|
+
}
|
|
466
|
+
open.push(kar);
|
|
390
467
|
}
|
|
391
468
|
}
|
|
392
|
-
for (const
|
|
393
|
-
carts.push(
|
|
394
|
-
}
|
|
395
|
-
// Doornummeren zodat de volgorde altijd 1..n is.
|
|
396
|
-
carts.forEach((c, idx) => {
|
|
397
|
-
c.cart_number = idx + 1;
|
|
398
|
-
});
|
|
469
|
+
for (const kar of open)
|
|
470
|
+
carts.push(samenvatten(kar, rests, carts.length + 1, r));
|
|
471
|
+
carts.forEach((c, i) => { c.cart_number = i + 1; });
|
|
399
472
|
return { carts, warnings };
|
|
400
473
|
}
|