@flowselections/floriday-verkoop-module 1.0.52 → 1.0.53
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.
|
@@ -76,6 +76,7 @@ export async function replanScopeInternal(scope) {
|
|
|
76
76
|
// (pieces_per_package / packages_per_layer / layers_per_load_carrier) —
|
|
77
77
|
// géén voorgekauwde karrenlijst; de karindeling rekenen we zelf.
|
|
78
78
|
const floridayByOrder = new Map();
|
|
79
|
+
const loadCarrierByOrder = new Map();
|
|
79
80
|
for (const o of orderRows) {
|
|
80
81
|
const fp = (o.custom_fields ?? {})?.floriday_packing ?? null;
|
|
81
82
|
if (!fp)
|
|
@@ -83,7 +84,48 @@ export async function replanScopeInternal(scope) {
|
|
|
83
84
|
const bel = completeSource(fp.pieces_per_package, fp.packages_per_layer, fp.layers_per_load_carrier);
|
|
84
85
|
if (bel)
|
|
85
86
|
floridayByOrder.set(o.id, bel);
|
|
87
|
+
const lc = typeof fp.load_carrier === "string" ? fp.load_carrier.trim() : "";
|
|
88
|
+
if (lc)
|
|
89
|
+
loadCarrierByOrder.set(o.id, lc.toUpperCase());
|
|
86
90
|
}
|
|
91
|
+
// ── Ladingdragerprofiel ────────────────────────────────────────────────
|
|
92
|
+
// Vertrouw NIET op de kolomdefault 'trolley' van order_carts: die verbergt
|
|
93
|
+
// dat er geen profielkeuze is gemaakt. Zonder profiel plannen we niet.
|
|
94
|
+
const { data: carrierProfiles } = await supabaseAdmin
|
|
95
|
+
.from("load_carrier_types")
|
|
96
|
+
.select("id, code, name, layers_per_carrier, is_active, is_default");
|
|
97
|
+
const activeProfiles = (carrierProfiles ?? []).filter((p) => p.is_active);
|
|
98
|
+
const defaultProfile = activeProfiles.find((p) => p.is_default) ??
|
|
99
|
+
activeProfiles.find((p) => String(p.code).toLowerCase() === "trolley") ??
|
|
100
|
+
null;
|
|
101
|
+
/** Floriday-ladingdragercode → code in load_carrier_types. */
|
|
102
|
+
const CARRIER_CODE_MAP = {
|
|
103
|
+
DANISH_TROLLEY: ["danish_container", "danish_trolley"],
|
|
104
|
+
DANISH_TROLLEY_NON_CC_RFID: ["danish_container"],
|
|
105
|
+
DANISH_TROLLEY_WATER_TRAY: ["danish_container"],
|
|
106
|
+
ONE_THIRD_DANISH_TROLLEY: ["danish_container"],
|
|
107
|
+
AUCTION_TROLLEY: ["auction_trolley", "trolley"],
|
|
108
|
+
EURO_TROLLEY: ["euro_rolcontainer", "trolley"],
|
|
109
|
+
ROLL_CONTAINER: ["euro_rolcontainer"],
|
|
110
|
+
EURO_PALLET: ["euro_pallet", "pallet"],
|
|
111
|
+
HT_EURO_PALLET: ["euro_pallet", "pallet"],
|
|
112
|
+
PALLET: ["pallet"],
|
|
113
|
+
BLOCK_PALLET: ["pallet"],
|
|
114
|
+
NONE: ["loose"],
|
|
115
|
+
};
|
|
116
|
+
const profileFor = (loadCarrier) => {
|
|
117
|
+
if (!loadCarrier)
|
|
118
|
+
return defaultProfile;
|
|
119
|
+
const direct = activeProfiles.find((p) => String(p.code).toUpperCase() === loadCarrier.toUpperCase());
|
|
120
|
+
if (direct)
|
|
121
|
+
return direct;
|
|
122
|
+
for (const code of CARRIER_CODE_MAP[loadCarrier.toUpperCase()] ?? []) {
|
|
123
|
+
const hit = activeProfiles.find((p) => String(p.code).toLowerCase() === code);
|
|
124
|
+
if (hit)
|
|
125
|
+
return hit;
|
|
126
|
+
}
|
|
127
|
+
return defaultProfile;
|
|
128
|
+
};
|
|
87
129
|
let usedFloridayPacking = false;
|
|
88
130
|
const inputs = (items ?? []).map((it) => {
|
|
89
131
|
const prod = it.products ?? {};
|
|
@@ -95,6 +137,12 @@ export async function replanScopeInternal(scope) {
|
|
|
95
137
|
completeSource(prod.plants_per_crate, prod.crates_per_layer, prod.layers_per_cart);
|
|
96
138
|
if (bel && fromFloriday)
|
|
97
139
|
usedFloridayPacking = true;
|
|
140
|
+
const loadCarrier = loadCarrierByOrder.get(it.order_id) ?? null;
|
|
141
|
+
// layers_per_cart = LEAST(profiel, bron) — identiek aan logistics_plan_buffer_sql.
|
|
142
|
+
const profileLayers = pos(profileFor(loadCarrier)?.layers_per_carrier);
|
|
143
|
+
const layers = bel?.layers_per_cart != null && profileLayers != null
|
|
144
|
+
? Math.min(bel.layers_per_cart, profileLayers)
|
|
145
|
+
: (bel?.layers_per_cart ?? null);
|
|
98
146
|
return {
|
|
99
147
|
order_item_id: it.id,
|
|
100
148
|
product_id: it.product_id,
|
|
@@ -102,7 +150,8 @@ export async function replanScopeInternal(scope) {
|
|
|
102
150
|
plants: Math.max(0, Number(it.quantity ?? 0)),
|
|
103
151
|
plants_per_crate: bel?.plants_per_crate ?? null,
|
|
104
152
|
crates_per_layer: bel?.crates_per_layer ?? null,
|
|
105
|
-
layers_per_cart:
|
|
153
|
+
layers_per_cart: layers,
|
|
154
|
+
load_carrier: loadCarrier,
|
|
106
155
|
};
|
|
107
156
|
});
|
|
108
157
|
// Item → order-map voor het toewijzen van cart.order_id bij homogene karren.
|
|
@@ -111,6 +160,11 @@ export async function replanScopeInternal(scope) {
|
|
|
111
160
|
itemToOrder.set(it.id, it.order_id);
|
|
112
161
|
}
|
|
113
162
|
// Bestaande karren in de scope tellen (voor waarschuwing) en wissen.
|
|
163
|
+
// Nummering én delete-scope liggen altijd op bufferniveau als de order een
|
|
164
|
+
// buffer heeft; order-scope is uitsluitend terugval.
|
|
165
|
+
if (!bufferId) {
|
|
166
|
+
console.warn("[cart-replan] geen buffer voor order %s — nummering en delete vallen terug op order-scope", soloOrderId);
|
|
167
|
+
}
|
|
114
168
|
const cartScope = (q) => bufferId ? q.eq("buffer_id", bufferId) : q.eq("order_id", soloOrderId);
|
|
115
169
|
const { data: existingCarts } = await cartScope(supabaseAdmin.from("order_carts").select("id, source"));
|
|
116
170
|
const replacedManualCount = (existingCarts ?? []).filter((c) => c.source === "manual").length;
|
|
@@ -121,7 +175,7 @@ export async function replanScopeInternal(scope) {
|
|
|
121
175
|
// levert alleen de belading-VELDEN (pieces_per_package / packages_per_layer /
|
|
122
176
|
// layers_per_load_carrier) — de mirror schrijft nooit een voorgekauwde
|
|
123
177
|
// carts-array, dus die tak bestaat bewust niet meer.
|
|
124
|
-
|
|
178
|
+
const plannedCarts = [];
|
|
125
179
|
const warnings = [];
|
|
126
180
|
let blockedItems = [];
|
|
127
181
|
{
|
|
@@ -132,13 +186,50 @@ export async function replanScopeInternal(scope) {
|
|
|
132
186
|
const neverMix = new Set((overrides ?? [])
|
|
133
187
|
.filter((o) => o.never_mix && o.product_id)
|
|
134
188
|
.map((o) => o.product_id));
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
189
|
+
// Ladingdrager is een HARDE partitie: één planner-run per load_carrier.
|
|
190
|
+
// Items zonder load_carrier (lokale orders) vormen hun eigen partitie op
|
|
191
|
+
// het defaultprofiel.
|
|
192
|
+
const partitions = new Map();
|
|
193
|
+
for (const input of inputs) {
|
|
194
|
+
const key = input.load_carrier ?? "";
|
|
195
|
+
const list = partitions.get(key);
|
|
196
|
+
if (list)
|
|
197
|
+
list.push(input);
|
|
198
|
+
else
|
|
199
|
+
partitions.set(key, [input]);
|
|
200
|
+
}
|
|
201
|
+
const allBlocked = [];
|
|
202
|
+
let nextCartNumber = 1;
|
|
203
|
+
for (const [key, partitionInputs] of partitions) {
|
|
204
|
+
const profile = profileFor(key || null);
|
|
205
|
+
if (!profile) {
|
|
206
|
+
// Geen actief profiel én geen 'trolley': niet plannen, wel blokkeren.
|
|
207
|
+
for (const i of partitionInputs) {
|
|
208
|
+
allBlocked.push({
|
|
209
|
+
order_item_id: i.order_item_id,
|
|
210
|
+
product_id: i.product_id,
|
|
211
|
+
product_name: i.product_name,
|
|
212
|
+
reason: "geen_belading",
|
|
213
|
+
missing: ["load_carrier_profile"],
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
warnings.push(`Geen actief ladingdragerprofiel gevonden${key ? ` voor ${key}` : ""} — karindeling overgeslagen.`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const result = planCarts(partitionInputs, {
|
|
220
|
+
maxPartySharePerLayerPct: settings?.max_party_share_per_layer_pct ?? 50,
|
|
221
|
+
maxBeladingenPerLayer: settings?.max_beladingen_per_layer ?? 2,
|
|
222
|
+
neverMixProductIds: neverMix,
|
|
223
|
+
startCartNumber: nextCartNumber,
|
|
224
|
+
});
|
|
225
|
+
for (const cart of result.carts) {
|
|
226
|
+
nextCartNumber = Math.max(nextCartNumber, cart.cart_number + 1);
|
|
227
|
+
plannedCarts.push({ cart, profile });
|
|
228
|
+
}
|
|
229
|
+
warnings.push(...result.warnings);
|
|
230
|
+
allBlocked.push(...result.blocked);
|
|
231
|
+
}
|
|
232
|
+
const result = { blocked: allBlocked };
|
|
142
233
|
// Regels zonder (volledige) belading: NOOIT karren maken. Meld de
|
|
143
234
|
// blokkade aan de logistiek-engine zodat die zichtbaar wordt in
|
|
144
235
|
// logistics_plan_queue / logistics_events / logistics_decisions.
|
|
@@ -187,7 +278,7 @@ export async function replanScopeInternal(scope) {
|
|
|
187
278
|
}
|
|
188
279
|
}
|
|
189
280
|
let cartsCreated = 0;
|
|
190
|
-
for (const cart of plannedCarts) {
|
|
281
|
+
for (const { cart, profile } of plannedCarts) {
|
|
191
282
|
// Bepaal cart.order_id: null bij mix van orders, anders die ene order.
|
|
192
283
|
const orderIdsOnCart = new Set();
|
|
193
284
|
for (const i of cart.items) {
|
|
@@ -207,6 +298,10 @@ export async function replanScopeInternal(scope) {
|
|
|
207
298
|
source: usedFloridayPacking ? "floriday" : "auto",
|
|
208
299
|
occupancy_pct: cart.occupancy_pct,
|
|
209
300
|
total_layers_used: cart.total_layers_used,
|
|
301
|
+
// Profiel expliciet meegeven; nooit leunen op de kolomdefault.
|
|
302
|
+
carrier_type: profile.code,
|
|
303
|
+
carrier_type_id: profile.id,
|
|
304
|
+
max_layers: profile.layers_per_carrier ?? null,
|
|
210
305
|
algorithm_version: CART_PLANNER_VERSION,
|
|
211
306
|
})
|
|
212
307
|
.select("id")
|