@operato/twin-kernel 0.4.1 → 0.4.3
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/dist/capability.js +10 -5
- package/dist/contract.d.ts +78 -0
- package/dist/contract.js +14 -1
- package/dist/flow-engine.d.ts +21 -0
- package/dist/flow-engine.js +60 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -0
- package/dist/kernel.d.ts +35 -0
- package/dist/kernel.js +148 -1
- package/dist/make-to-order.d.ts +58 -0
- package/dist/make-to-order.js +118 -0
- package/dist/mes-kernel.d.ts +3 -16
- package/dist/mes-kernel.js +40 -21
- package/dist/observed-reducer.d.ts +10 -0
- package/dist/observed-reducer.js +11 -1
- package/dist/scenario-validate.d.ts +15 -0
- package/dist/scenario-validate.js +72 -0
- package/dist/wms-profile.d.ts +12 -1
- package/dist/wms-profile.js +17 -3
- package/dist-cjs/index.cjs +313 -39
- package/package.json +1 -1
package/dist-cjs/index.cjs
CHANGED
|
@@ -118,6 +118,7 @@ __export(index_exports, {
|
|
|
118
118
|
transformationEvent: () => transformationEvent,
|
|
119
119
|
validateDomainDefinition: () => validateDomainDefinition,
|
|
120
120
|
validateEpcisEvent: () => validateEpcisEvent,
|
|
121
|
+
validateScenario: () => validateScenario,
|
|
121
122
|
weekdayAt: () => weekdayAt,
|
|
122
123
|
workingTimeOfWeek: () => workingTimeOfWeek
|
|
123
124
|
});
|
|
@@ -390,8 +391,21 @@ var OP_EVENT = {
|
|
|
390
391
|
/** 물리 자산 상태 전이 — 어디 있나·무엇을 싣고 있나(빈 팔레트인가). */
|
|
391
392
|
asset: "asset.status",
|
|
392
393
|
order: "order.status",
|
|
393
|
-
quality: "quality.output"
|
|
394
|
+
quality: "quality.output",
|
|
394
395
|
// 품질 산출(양품/불량) — OEE quality 입력. live 누적기가 이걸로 good/scrap 정확 추적.
|
|
396
|
+
/**
|
|
397
|
+
* 주목 신호 확인(ack) — **사람이 한 행위**라 파생될 수 없다.
|
|
398
|
+
*
|
|
399
|
+
* 다른 파생 상태는 상태에서 다시 계산된다(주목 신호 자체가 그렇다). 그런데 "누가 이것을 봤다" 는
|
|
400
|
+
* 계산으로 되살릴 수 없다. 저널에 남기지 않으면 재기동하면 확인해 둔 신호가 다시 빨개지고,
|
|
401
|
+
* 과거를 되짚어도 그때 무엇을 확인했는지 알 수 없다 — 저널이 현실을 불완전하게 담는 자리였다.
|
|
402
|
+
*/
|
|
403
|
+
/*
|
|
404
|
+
* 값이 커맨드(`CMD.attentionAck='attention.ack'`)와 겹치지 않게 **과거형**으로 둔다 — 커맨드는
|
|
405
|
+
* "확인해라"(요청)이고 이벤트는 "확인했다"(사실)다. 같은 문자열을 쓰면 저널에서 요청과 사실이
|
|
406
|
+
* 구별되지 않는다.
|
|
407
|
+
*/
|
|
408
|
+
attentionAck: "attention.acked"
|
|
395
409
|
};
|
|
396
410
|
var CMD = {
|
|
397
411
|
orderHold: "order.hold",
|
|
@@ -433,6 +447,50 @@ function readBoardAssets(def) {
|
|
|
433
447
|
return list.map((a) => normalizeHomeLocation(a));
|
|
434
448
|
}
|
|
435
449
|
|
|
450
|
+
// src/scenario-validate.ts
|
|
451
|
+
var DISTRIBUTIONS = /* @__PURE__ */ new Set(["poisson", "uniform", "constant", "profile"]);
|
|
452
|
+
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
453
|
+
var bad = (errorCode, errorParams) => ({ ok: false, errorCode, errorParams });
|
|
454
|
+
function validateGenerator(g, at) {
|
|
455
|
+
if (!g || typeof g !== "object") return bad("scenario-generator-invalid", { at });
|
|
456
|
+
if (!g.kind || typeof g.kind !== "string") return bad("scenario-generator-kind-required", { at });
|
|
457
|
+
const rate = g.rate;
|
|
458
|
+
if (!rate || typeof rate !== "object") return bad("scenario-rate-required", { at, kind: g.kind });
|
|
459
|
+
if (!isNum(rate.meanPerHour) || rate.meanPerHour < 0) return bad("scenario-rate-mean-invalid", { at, kind: g.kind });
|
|
460
|
+
if (!DISTRIBUTIONS.has(rate.distribution)) return bad("scenario-rate-distribution-invalid", { at, kind: g.kind, distribution: String(rate.distribution ?? "") });
|
|
461
|
+
if (rate.distribution === "profile" && !Array.isArray(rate.profile)) return bad("scenario-rate-profile-required", { at, kind: g.kind });
|
|
462
|
+
const content = g.content;
|
|
463
|
+
if (!content || typeof content !== "object") return bad("scenario-content-required", { at, kind: g.kind });
|
|
464
|
+
if (!Array.isArray(content.skuMix) || content.skuMix.length === 0) return bad("scenario-sku-mix-required", { at, kind: g.kind });
|
|
465
|
+
for (const s of content.skuMix) {
|
|
466
|
+
if (!s?.gtin || typeof s.gtin !== "string") return bad("scenario-sku-gtin-required", { at, kind: g.kind });
|
|
467
|
+
if (!isNum(s.weight) || s.weight <= 0) return bad("scenario-sku-weight-invalid", { at, kind: g.kind, gtin: String(s.gtin) });
|
|
468
|
+
}
|
|
469
|
+
const q = content.qtyPerLine;
|
|
470
|
+
if (!q || !isNum(q.min) || !isNum(q.max)) return bad("scenario-qty-required", { at, kind: g.kind });
|
|
471
|
+
if (q.min < 0 || q.max < q.min) return bad("scenario-qty-range-invalid", { at, kind: g.kind, min: q.min, max: q.max });
|
|
472
|
+
const l = content.linesPerOrder;
|
|
473
|
+
if (l && (!isNum(l.min) || !isNum(l.max) || l.min < 1 || l.max < l.min)) return bad("scenario-lines-range-invalid", { at, kind: g.kind });
|
|
474
|
+
if (g.stimulus !== void 0 && g.stimulus !== "arrival" && g.stimulus !== "order") {
|
|
475
|
+
return bad("scenario-stimulus-invalid", { at, kind: g.kind, stimulus: String(g.stimulus) });
|
|
476
|
+
}
|
|
477
|
+
return { ok: true };
|
|
478
|
+
}
|
|
479
|
+
function validateScenario(def) {
|
|
480
|
+
if (!def || typeof def !== "object") return bad("scenario-invalid");
|
|
481
|
+
if (def.seed !== void 0 && !isNum(def.seed)) return bad("scenario-seed-invalid");
|
|
482
|
+
if (def.speed !== void 0 && (!isNum(def.speed) || def.speed <= 0)) return bad("scenario-speed-invalid");
|
|
483
|
+
if (def.horizon !== void 0 && (!isNum(def.horizon) || def.horizon < 0)) return bad("scenario-horizon-invalid");
|
|
484
|
+
const gens = def.generators;
|
|
485
|
+
if (gens === void 0) return { ok: true };
|
|
486
|
+
if (!Array.isArray(gens)) return bad("scenario-generators-invalid");
|
|
487
|
+
for (let i = 0; i < gens.length; i++) {
|
|
488
|
+
const r = validateGenerator(gens[i], i);
|
|
489
|
+
if (!r.ok) return r;
|
|
490
|
+
}
|
|
491
|
+
return { ok: true };
|
|
492
|
+
}
|
|
493
|
+
|
|
436
494
|
// src/domain-definition.ts
|
|
437
495
|
var OP_PARAM = {
|
|
438
496
|
/** 양품률(0..1, 무차원). 없으면 커널 기본값 — 기본값을 쓴 사실은 `specCoverage()` 가 밝힌다. */
|
|
@@ -858,6 +916,8 @@ var ObservedReducer = class {
|
|
|
858
916
|
persons = /* @__PURE__ */ new Map();
|
|
859
917
|
assets = /* @__PURE__ */ new Map();
|
|
860
918
|
orders = /* @__PURE__ */ new Map();
|
|
919
|
+
/** 확인해 둔 주목 신호 id — `attention.acked` 이벤트로만 들어온다(계산으로 만들지 않는다). */
|
|
920
|
+
acked = /* @__PURE__ */ new Set();
|
|
861
921
|
revision = 0;
|
|
862
922
|
/** 받은 정정 선언 — 상태에 반영하지 않되 **버리지도 않는다**(소비처가 볼 수 있게). */
|
|
863
923
|
corrections = [];
|
|
@@ -1095,6 +1155,11 @@ var ObservedReducer = class {
|
|
|
1095
1155
|
this.touchLocation(d.location);
|
|
1096
1156
|
break;
|
|
1097
1157
|
}
|
|
1158
|
+
case OP_EVENT.attentionAck: {
|
|
1159
|
+
const d = e.data;
|
|
1160
|
+
if (d?.id) this.acked.add(d.id);
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1098
1163
|
case OP_EVENT.order: {
|
|
1099
1164
|
const d = e.data;
|
|
1100
1165
|
if (this.stale(`order:${d.orderId}`, e)) return;
|
|
@@ -1311,7 +1376,8 @@ var ObservedReducer = class {
|
|
|
1311
1376
|
assets: [...this.assets.values()].map((a) => ({ ...a, ...this.effectivityPart(a) })),
|
|
1312
1377
|
tasks: [...this.tasks.values()].map((t) => ({ ...t })),
|
|
1313
1378
|
equipment: [...this.equipment.values()].map((m) => ({ ...m, ...this.effectivityPart(m), ...this.offShiftPart(`eq:${m.id}`) })),
|
|
1314
|
-
orders: [...this.orders.values()].map((o) => ({ ...o }))
|
|
1379
|
+
orders: [...this.orders.values()].map((o) => ({ ...o })),
|
|
1380
|
+
acked: [...this.acked]
|
|
1315
1381
|
};
|
|
1316
1382
|
}
|
|
1317
1383
|
};
|
|
@@ -1372,24 +1438,27 @@ var BTT = {
|
|
|
1372
1438
|
po: "urn:epcglobal:cbv:btt:po",
|
|
1373
1439
|
so: "urn:epcglobal:cbv:btt:so"
|
|
1374
1440
|
};
|
|
1375
|
-
var WMS_LOCATION_TYPES = ["dock", "storage", "staging", "dock-ship"];
|
|
1441
|
+
var WMS_LOCATION_TYPES = ["dock", "storage", "staging", "dock-ship", "vas-station"];
|
|
1376
1442
|
var WMS_TYPES = [
|
|
1377
|
-
...WMS_LOCATION_TYPES.map((k) => ({ key: k, role: "location", label: `twin.type.${k}`, standardClass: { epcis: "bizLocation" }, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable"] })),
|
|
1378
|
-
{ key: "
|
|
1443
|
+
...WMS_LOCATION_TYPES.filter((k) => k !== "vas-station").map((k) => ({ key: k, role: "location", label: `twin.type.${k}`, standardClass: { epcis: "bizLocation" }, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable"] })),
|
|
1444
|
+
{ key: "vas-station", role: "location", label: "twin.type.vas-station", standardClass: { epcis: "bizLocation", isa95: "WorkCenter" }, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable", "processable"] },
|
|
1445
|
+
{ key: "forklift", role: "equipment", label: "twin.type.forklift", standardClass: { epcis: "object", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["mobile", "operable"] },
|
|
1446
|
+
/** 유통가공 작업자·작업대 설비 — 가공을 수행하는 능동 자원(ISA-95 `Equipment`). */
|
|
1447
|
+
{ key: "packer", role: "equipment", label: "twin.type.packer", standardClass: { epcis: "object", isa95: "Equipment" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] }
|
|
1379
1448
|
];
|
|
1380
1449
|
|
|
1381
1450
|
// src/capability.ts
|
|
1382
1451
|
var CAPABILITIES = {
|
|
1383
1452
|
operable: {
|
|
1384
1453
|
key: "operable",
|
|
1385
|
-
label: "
|
|
1454
|
+
label: "twin.capability.operable",
|
|
1386
1455
|
semantics: "\uB2A5\uB3D9 \uC790\uC6D0\uC758 \uC6B4\uC601 \uC0C1\uD0DC(\uC720\uD734/\uAC00\uB3D9/\uACE0\uC7A5). status \uAD50\uCC28 \uAD00\uC2EC\uC0AC\uB97C \uC5EC\uAE30 \uD558\uB098\uB85C.",
|
|
1387
1456
|
stateFields: ["status"],
|
|
1388
1457
|
results: ["statusChanged"]
|
|
1389
1458
|
},
|
|
1390
1459
|
storable: {
|
|
1391
1460
|
key: "storable",
|
|
1392
|
-
label: "
|
|
1461
|
+
label: "twin.capability.storable",
|
|
1393
1462
|
semantics: "\uC544\uC774\uD15C\uC744 \uBCF4\uC720\uD558\uB294 \uC704\uCE58 \u2014 \uC810\uC720/\uC6A9\uB7C9. (\uC52C \uAE30\uC81C: Capacity)",
|
|
1394
1463
|
stateFields: ["occupancy", "capacity"],
|
|
1395
1464
|
invariants: ["0 <= occupancy <= capacity (capacity>0)"],
|
|
@@ -1397,7 +1466,7 @@ var CAPABILITIES = {
|
|
|
1397
1466
|
},
|
|
1398
1467
|
mobile: {
|
|
1399
1468
|
key: "mobile",
|
|
1400
|
-
label: "
|
|
1469
|
+
label: "twin.capability.mobile",
|
|
1401
1470
|
semantics: "\uC790\uC6D0 \uC790\uC2E0\uC774 \uC790\uB9AC \uAC04 \uC774\uB3D9. Transferable(\uC544\uC774\uD15C \uC774\uB3D9)\uACFC \uB2E4\uB984. (\uC52C \uAE30\uC81C: CarrierLine)",
|
|
1402
1471
|
stateFields: ["location", "motion"],
|
|
1403
1472
|
models: ["Motion"],
|
|
@@ -1405,14 +1474,14 @@ var CAPABILITIES = {
|
|
|
1405
1474
|
},
|
|
1406
1475
|
processable: {
|
|
1407
1476
|
key: "processable",
|
|
1408
|
-
label: "
|
|
1477
|
+
label: "twin.capability.processable",
|
|
1409
1478
|
semantics: "\uBCC0\uD658/\uAC00\uACF5 \uC218\uD589 \u2014 \uC0B0\uCD9C(\uC591\uD488/\uBD88\uB7C9). \uC6B4\uC601 status \uB294 Operable \uC870\uD569. progress \uBC29\uCD9C\uC740 \uD6C4\uC18D.",
|
|
1410
1479
|
stateFields: ["output"],
|
|
1411
1480
|
results: ["completed"]
|
|
1412
1481
|
},
|
|
1413
1482
|
trackable: {
|
|
1414
1483
|
key: "trackable",
|
|
1415
|
-
label: "
|
|
1484
|
+
label: "twin.capability.trackable",
|
|
1416
1485
|
semantics: "\uC624\uB354/\uC544\uC774\uD15C \uC0DD\uC560 \uCD94\uC801 \u2014 \uC0DD\uC560\uB2E8\uACC4(\uB3C4\uBA54\uC778 \uB77C\uBCA8, \uBB34\uBC29\uC5B8)\xB7\uC9C4\uD589\xB7\uBCF4\uB958.",
|
|
1417
1486
|
stateFields: ["lifecycle", "progress", "held"],
|
|
1418
1487
|
results: ["lifecycleChanged"]
|
|
@@ -2023,6 +2092,7 @@ var FlowEngine = class {
|
|
|
2023
2092
|
// ── TwinKernel (mechanics, 도메인 무관) ───────────────────────────────────
|
|
2024
2093
|
loadBoard(def) {
|
|
2025
2094
|
this.boardDef = def;
|
|
2095
|
+
if (def.productionSpec?.definition?.operations?.length) this.loadOperations(def.productionSpec.definition.operations);
|
|
2026
2096
|
for (const n of readBoardLocations(def)) this.locations.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, occupancy: 0, status: "idle", parentId: n.parentId });
|
|
2027
2097
|
this.classDefs = { personnel: def.personnelClasses, equipment: def.equipmentClasses, asset: def.assetClasses, material: def.materialClasses };
|
|
2028
2098
|
this.materialDefs = new Map((def.materialDefinitions ?? []).filter((d) => d?.id).map((d) => [d.id, d]));
|
|
@@ -2075,6 +2145,7 @@ var FlowEngine = class {
|
|
|
2075
2145
|
this.revision = from;
|
|
2076
2146
|
}
|
|
2077
2147
|
hydrateObserved(snap, orders = []) {
|
|
2148
|
+
for (const id of snap.acked ?? []) this._acked.add(id);
|
|
2078
2149
|
for (const n of snap.locations) {
|
|
2079
2150
|
this.locations.set(n.id, { id: n.id, type: n.type, capacity: n.capacity ?? 0, parallelism: n.parallelism, occupancy: n.occupancy ?? 0, status: n.status ?? "idle", parentId: n.parentId });
|
|
2080
2151
|
}
|
|
@@ -2282,6 +2353,14 @@ var FlowEngine = class {
|
|
|
2282
2353
|
_attentionSince = /* @__PURE__ */ new Map();
|
|
2283
2354
|
// 확인(ack)된 주목 신호 id — 조건 지속돼도 acknowledged 로 표시(재발 시 재활성)
|
|
2284
2355
|
dispatch(cmd) {
|
|
2356
|
+
this._correlationId = cmd.correlationId ?? cmd.commandId;
|
|
2357
|
+
try {
|
|
2358
|
+
return this.dispatchInner(cmd);
|
|
2359
|
+
} finally {
|
|
2360
|
+
this._correlationId = void 0;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
dispatchInner(cmd) {
|
|
2285
2364
|
const ok = () => ({ commandId: cmd.commandId, accepted: true });
|
|
2286
2365
|
const fail = (errorCode, errorParams) => ({ commandId: cmd.commandId, accepted: false, errorCode, errorParams, error: errorCode });
|
|
2287
2366
|
switch (cmd.type) {
|
|
@@ -2296,7 +2375,10 @@ var FlowEngine = class {
|
|
|
2296
2375
|
}
|
|
2297
2376
|
case CMD.attentionAck: {
|
|
2298
2377
|
const id = cmd.args?.id;
|
|
2299
|
-
if (id)
|
|
2378
|
+
if (id) {
|
|
2379
|
+
this._acked.add(id);
|
|
2380
|
+
this.emitOp(OP_EVENT.attentionAck, { id, at: this.now() });
|
|
2381
|
+
}
|
|
2300
2382
|
return ok();
|
|
2301
2383
|
}
|
|
2302
2384
|
// Operable 코어 — 자원(설비·설비) 제어. capability-keyed(resourceId), 모든 operable 자원 공통.
|
|
@@ -2475,7 +2557,9 @@ var FlowEngine = class {
|
|
|
2475
2557
|
...o.endTime ? { endTime: o.endTime } : {},
|
|
2476
2558
|
held: o.held
|
|
2477
2559
|
})),
|
|
2478
|
-
attentions: this.computeAttentions()
|
|
2560
|
+
attentions: this.computeAttentions(),
|
|
2561
|
+
/* 확인해 둔 신호 — 스냅샷으로 왕복해야 재기동 후에도 확인 상태가 유지된다. */
|
|
2562
|
+
acked: [...this._acked]
|
|
2479
2563
|
};
|
|
2480
2564
|
}
|
|
2481
2565
|
/*
|
|
@@ -2614,6 +2698,15 @@ var FlowEngine = class {
|
|
|
2614
2698
|
* 오퍼레이션 명세를 싣는다 — 도메인 정의(ISA-95 OperationsSegment)의 시뮬 명세를 커널이 소비하는 입구.
|
|
2615
2699
|
* 같은 key 를 다시 실으면 덮어쓴다(정의가 권위).
|
|
2616
2700
|
*/
|
|
2701
|
+
/**
|
|
2702
|
+
* 선언된 오퍼레이션들 — 도메인 커널이 "무엇을 만들 수 있나" 를 물을 수 있게.
|
|
2703
|
+
*
|
|
2704
|
+
* `operationSpecs` 를 도메인이 직접 뒤지지 않게 읽기 창구를 둔다: 저장 형태(맵)가 바뀌어도
|
|
2705
|
+
* 도메인은 몰라야 하고, 도메인이 그 맵에 쓰는 일이 생기면 정의가 권위라는 규약이 깨진다.
|
|
2706
|
+
*/
|
|
2707
|
+
declaredOperations() {
|
|
2708
|
+
return [...this.operationSpecs.values()];
|
|
2709
|
+
}
|
|
2617
2710
|
loadOperations(ops = []) {
|
|
2618
2711
|
for (const o of ops) if (o?.key) this.operationSpecs.set(o.key, o);
|
|
2619
2712
|
}
|
|
@@ -2929,14 +3022,25 @@ var FlowEngine = class {
|
|
|
2929
3022
|
for (const n of this.locations.values()) if (n.type === locationType) views.push({ id: n.id, capacity: n.capacity, occupancy: n.occupancy, reserved: reserved.get(n.id) ?? 0 });
|
|
2930
3023
|
return views;
|
|
2931
3024
|
}
|
|
3025
|
+
/**
|
|
3026
|
+
* 지금 처리 중인 커맨드의 상관값 — **디스패치 동안에만 있다.**
|
|
3027
|
+
*
|
|
3028
|
+
* 커맨드가 낳은 이벤트에 이 값을 실어야 "이 지시가 실제로 무엇을 일으켰나" 를 나중에 물을 수 있다.
|
|
3029
|
+
* 그게 없으면 승인 기록은 "허락했다" 까지이고, 그 뒤 공장이 어떻게 움직였는지와 이어지지 않는다.
|
|
3030
|
+
*
|
|
3031
|
+
* **한계를 밝힌다**: 여기서 잇는 것은 그 자리에서 방출된 이벤트뿐이다. 나중 틱에 일어나는 후속
|
|
3032
|
+
* (예: resourceDown 이 정한 수리 완료)은 시뮬 시간의 결과라 이어지지 않는다. 즉시 인과만 잇는다 —
|
|
3033
|
+
* 먼 인과까지 같은 값으로 묶으면 "이 승인 때문"이라는 말이 사실보다 넓어진다.
|
|
3034
|
+
*/
|
|
3035
|
+
_correlationId;
|
|
2932
3036
|
emit(event) {
|
|
2933
3037
|
this.revision++;
|
|
2934
|
-
const e = { eventId: `${this.tenantId}-evt-${++this.eventSeq}`, eventType: `epcis.${event.type}`, eventTime: event.eventTime, tenantId: this.tenantId, data: event };
|
|
3038
|
+
const e = { eventId: `${this.tenantId}-evt-${++this.eventSeq}`, eventType: `epcis.${event.type}`, eventTime: event.eventTime, tenantId: this.tenantId, ...this._correlationId ? { correlationId: this._correlationId } : {}, data: event };
|
|
2935
3039
|
for (const h of this.handlers) h(e);
|
|
2936
3040
|
}
|
|
2937
3041
|
emitOp(eventType, data) {
|
|
2938
3042
|
this.revision++;
|
|
2939
|
-
const e = { eventId: `${this.tenantId}-evt-${++this.eventSeq}`, eventType, eventTime: this.now(), tenantId: this.tenantId, data };
|
|
3043
|
+
const e = { eventId: `${this.tenantId}-evt-${++this.eventSeq}`, eventType, eventTime: this.now(), tenantId: this.tenantId, ...this._correlationId ? { correlationId: this._correlationId } : {}, data };
|
|
2940
3044
|
for (const h of this.handlers) h(e);
|
|
2941
3045
|
}
|
|
2942
3046
|
/**
|
|
@@ -3682,6 +3786,59 @@ var FlowEngine = class {
|
|
|
3682
3786
|
}
|
|
3683
3787
|
};
|
|
3684
3788
|
|
|
3789
|
+
// src/make-to-order.ts
|
|
3790
|
+
function producedByIndex(ops) {
|
|
3791
|
+
const idx = /* @__PURE__ */ new Map();
|
|
3792
|
+
for (const op of ops) {
|
|
3793
|
+
if (op.intent !== "process") continue;
|
|
3794
|
+
for (const m of op.materialSpecification ?? []) {
|
|
3795
|
+
if (m.use !== "produced") continue;
|
|
3796
|
+
if (!m.materialDefinition) continue;
|
|
3797
|
+
if (!idx.has(m.materialDefinition)) idx.set(m.materialDefinition, op);
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
return idx;
|
|
3801
|
+
}
|
|
3802
|
+
var consumedOf = (op) => (op.materialSpecification ?? []).filter((m) => m.use === "consumed" && (m.quantity ?? 0) > 0);
|
|
3803
|
+
var outputPerRun = (op, gtin) => {
|
|
3804
|
+
const spec = (op.materialSpecification ?? []).find((m) => m.use === "produced" && m.materialDefinition === gtin);
|
|
3805
|
+
return Math.max(1, spec?.quantity ?? 1);
|
|
3806
|
+
};
|
|
3807
|
+
function planMakeToOrder(gtin, shortQty, ops, stock, locations) {
|
|
3808
|
+
const op = producedByIndex(ops).get(gtin);
|
|
3809
|
+
if (!op) return { steps: [], reason: "not-producible" };
|
|
3810
|
+
const station = op.locationType ? locations.find((l) => l.type === op.locationType) : void 0;
|
|
3811
|
+
if (!station) return { steps: [], reason: "no-station" };
|
|
3812
|
+
const need = consumedOf(op);
|
|
3813
|
+
if (!need.length) {
|
|
3814
|
+
return { steps: [{ step: "process", operation: op.key, at: station.id }] };
|
|
3815
|
+
}
|
|
3816
|
+
const runs = Math.max(1, Math.ceil(shortQty / outputPerRun(op, gtin)));
|
|
3817
|
+
const feeds = [];
|
|
3818
|
+
let short = false;
|
|
3819
|
+
for (const req of need) {
|
|
3820
|
+
const want = (req.quantity ?? 0) * runs;
|
|
3821
|
+
const atStation = stock.filter((s) => s.location === station.id && matches(s, req)).reduce((n, s) => n + s.qty, 0);
|
|
3822
|
+
let missing = want - atStation;
|
|
3823
|
+
if (missing <= 0) continue;
|
|
3824
|
+
for (const s of stock) {
|
|
3825
|
+
if (missing <= 0) break;
|
|
3826
|
+
if (s.location === station.id || !s.sellable || !matches(s, req)) continue;
|
|
3827
|
+
const take = Math.min(s.qty, missing);
|
|
3828
|
+
feeds.push({ step: "feed", gtin: s.gtin, from: s.location, to: station.id, qty: take });
|
|
3829
|
+
missing -= take;
|
|
3830
|
+
}
|
|
3831
|
+
if (missing > 0) short = true;
|
|
3832
|
+
}
|
|
3833
|
+
if (short) return { steps: [], reason: "short-materials" };
|
|
3834
|
+
if (feeds.length) return { steps: feeds, reason: "waiting-feed" };
|
|
3835
|
+
return { steps: [{ step: "process", operation: op.key, at: station.id }] };
|
|
3836
|
+
}
|
|
3837
|
+
function matches(s, req) {
|
|
3838
|
+
if (req.materialDefinition) return s.gtin === req.materialDefinition;
|
|
3839
|
+
return false;
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3685
3842
|
// src/kernel.ts
|
|
3686
3843
|
var TRAVEL_MS = 3e4;
|
|
3687
3844
|
var COMPANY_PREFIX = "0614141";
|
|
@@ -3809,7 +3966,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
3809
3966
|
chosenAll.push(epc);
|
|
3810
3967
|
}
|
|
3811
3968
|
}
|
|
3812
|
-
if (chosenAll.length === 0) return;
|
|
3969
|
+
if (chosenAll.length === 0) return this.makeShortLines(o);
|
|
3813
3970
|
this.reserve(chosenAll, BIZSTEP.storing);
|
|
3814
3971
|
this.emit(transactionEvent({ eventTime: this.now(), action: "ADD", bizStep: BIZSTEP.picking, bizTransactionList: [{ type: BTT.so, bizTransaction: o.bizTransaction }], epcList: chosenAll.slice() }));
|
|
3815
3972
|
for (const epc of chosenAll) {
|
|
@@ -3824,6 +3981,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
3824
3981
|
}
|
|
3825
3982
|
/** 태스크 완료 — 이동 반영 후 putaway=storing, pick=picking(+전량 시 pack→stage→ship). */
|
|
3826
3983
|
onTaskComplete(t) {
|
|
3984
|
+
if (t.intent === "process") return this.onProcessComplete(t);
|
|
3827
3985
|
const item = this.itemByRef(t.itemEpc);
|
|
3828
3986
|
if (!item) throw new Error(`task ${t.id}: item "${t.itemEpc}" vanished between the core check and the domain hook`);
|
|
3829
3987
|
const from = this.locations.get(t.fromNode);
|
|
@@ -3831,6 +3989,10 @@ var WmsKernel = class extends FlowEngine {
|
|
|
3831
3989
|
from.occupancy--;
|
|
3832
3990
|
to.occupancy++;
|
|
3833
3991
|
item.location = to.id;
|
|
3992
|
+
if (t.kind === "feed") {
|
|
3993
|
+
this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep: BIZSTEP.storing, disposition: item.disposition, epcList: [item.epc], quantityList: [{ epcClass: item.gtin, quantity: item.qty }], readPoint: to.id, bizLocation: to.id }));
|
|
3994
|
+
return;
|
|
3995
|
+
}
|
|
3834
3996
|
if (t.kind === "putaway") {
|
|
3835
3997
|
item.disposition = DISP.sellable;
|
|
3836
3998
|
this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep: BIZSTEP.storing, disposition: DISP.sellable, epcList: [item.epc], quantityList: [{ epcClass: item.gtin, quantity: item.qty }], readPoint: to.id, bizLocation: to.id }));
|
|
@@ -3843,6 +4005,112 @@ var WmsKernel = class extends FlowEngine {
|
|
|
3843
4005
|
order.picked.push(item.epc);
|
|
3844
4006
|
if (order.picked.length === order.allocated.length) this.finalizeOrder(order, to);
|
|
3845
4007
|
}
|
|
4008
|
+
// ── 수요가 부르는 생산(유통가공) ────────────────────────────────────────────
|
|
4009
|
+
/**
|
|
4010
|
+
* 부족한 라인을 **만들어서** 채운다 — 부품 이송 → 가공 → 되돌리기 3단 사슬.
|
|
4011
|
+
*
|
|
4012
|
+
* 사슬인 이유는 커널의 두 규칙 때문이다(둘 다 의도된 규칙이라 우회하지 않는다):
|
|
4013
|
+
* 자재는 **작업이 일어나는 자리에** 있어야 소비되고, 산출물은 `in_progress` 로 태어나 **팔 수 있는
|
|
4014
|
+
* 재고가 아니다.** 그래서 부품을 작업대로 옮기고, 가공하고, 나온 것을 보관 자리로 되돌린다.
|
|
4015
|
+
*
|
|
4016
|
+
* **한 오더에 사슬 하나만** 굴린다. 매 tick 마다 다시 발행하면 같은 부품을 두 번 끌어오는 작업이
|
|
4017
|
+
* 쌓이고, 그중 하나만 성공한 뒤 나머지는 영원히 재료를 기다린다.
|
|
4018
|
+
*/
|
|
4019
|
+
makeShortLines(o) {
|
|
4020
|
+
if (!o.lines?.length) return;
|
|
4021
|
+
if (this.hasOpenMakeChain(o.id)) return;
|
|
4022
|
+
const stock = this.stockLines();
|
|
4023
|
+
const locations = [...this.locations.values()].map((l) => ({ id: l.id, type: l.type }));
|
|
4024
|
+
const ops = this.declaredOperations();
|
|
4025
|
+
for (const line of o.lines) {
|
|
4026
|
+
const have = stock.filter((s) => s.gtin === line.gtin && s.sellable && this.locations.get(s.location)?.type === "storage").reduce((n, s) => n + s.qty, 0);
|
|
4027
|
+
const short = line.requested - have;
|
|
4028
|
+
if (short <= 0) continue;
|
|
4029
|
+
const plan = planMakeToOrder(line.gtin, short, ops, stock, locations);
|
|
4030
|
+
for (const step of plan.steps) {
|
|
4031
|
+
if (step.step === "feed") this.issueFeed(o, step.gtin, step.from, step.to, step.qty);
|
|
4032
|
+
else this.issueProcess(o, step.operation, step.at);
|
|
4033
|
+
}
|
|
4034
|
+
if (plan.steps.length) return;
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
/** 이 오더가 굴리고 있는 생산 사슬이 있나 — 이송·가공·되돌리기 중 하나라도 열려 있으면 그렇다. */
|
|
4038
|
+
hasOpenMakeChain(orderId) {
|
|
4039
|
+
for (const t of this.tasks.values()) {
|
|
4040
|
+
if (t.orderId !== orderId || t.status === "completed") continue;
|
|
4041
|
+
if (t.kind === "feed" || t.intent === "process") return true;
|
|
4042
|
+
if (t.kind === "putaway" && t.orderId === orderId) return true;
|
|
4043
|
+
}
|
|
4044
|
+
return false;
|
|
4045
|
+
}
|
|
4046
|
+
/** 지금 재고 — 판단 함수가 보는 형태로. */
|
|
4047
|
+
stockLines() {
|
|
4048
|
+
return [...this.items.values()].filter((i) => i.gtin).map((i) => ({ gtin: i.gtin, location: i.location, qty: i.qty ?? 1, sellable: i.disposition === DISP.sellable }));
|
|
4049
|
+
}
|
|
4050
|
+
/** 부품을 작업대로 — 팔레트 이동이므로 피킹과 같은 기제다(부분 소비는 코어가 한다). */
|
|
4051
|
+
issueFeed(o, gtin, from, to, qty) {
|
|
4052
|
+
const src = [...this.items.values()].find((i) => i.gtin === gtin && i.location === from && i.disposition === DISP.sellable);
|
|
4053
|
+
if (!src) return;
|
|
4054
|
+
src.disposition = DISP.reserved;
|
|
4055
|
+
this.pushTask(o, "feed", src.epc, from, to);
|
|
4056
|
+
}
|
|
4057
|
+
/** 가공 — 작업 종류를 **오퍼레이션 키**로 둔다(코어가 그 키로 명세를 찾는다). */
|
|
4058
|
+
issueProcess(o, operation, at) {
|
|
4059
|
+
this.pushTask(o, operation, "", at, at, "process");
|
|
4060
|
+
}
|
|
4061
|
+
pushTask(o, kind, itemEpc, from, to, intent) {
|
|
4062
|
+
const id = `task-${++this.taskSeq}`;
|
|
4063
|
+
const task = {
|
|
4064
|
+
id,
|
|
4065
|
+
kind,
|
|
4066
|
+
status: "created",
|
|
4067
|
+
itemEpc,
|
|
4068
|
+
fromNode: from,
|
|
4069
|
+
toNode: to,
|
|
4070
|
+
resource: null,
|
|
4071
|
+
remainingMs: 0,
|
|
4072
|
+
durationMs: this.durationOf({ kind, fromNode: from, toNode: to }, TRAVEL_MS),
|
|
4073
|
+
...intent ? { intent } : {},
|
|
4074
|
+
...o ? { orderId: o.id } : {}
|
|
4075
|
+
};
|
|
4076
|
+
this.tasks.set(id, task);
|
|
4077
|
+
this.emitTask(task);
|
|
4078
|
+
}
|
|
4079
|
+
/**
|
|
4080
|
+
* 가공 완료 — 코어가 이미 소비(시작)와 산출(완료 직전)을 실행했다. 남은 일은 만든 것을 **재고로
|
|
4081
|
+
* 들여놓는 것**이다: 산출물은 `in_progress` 로 태어나 팔 수 있는 재고가 아니다.
|
|
4082
|
+
*
|
|
4083
|
+
* ── 왜 팔레트로 묶나 ───────────────────────────────────────────────────────
|
|
4084
|
+
* 코어의 산출은 **클래스+수량 줄**이고 그 키에는 자리가 박혀 있다(`품목@자리`). 창고의 출고 경로는
|
|
4085
|
+
* 직렬 물류단위(팔레트 SSCC)를 다루므로, 그 줄을 그대로 출고에 태우면 키를 EPC 로 착각해 조회가
|
|
4086
|
+
* 깨진다(실제로 그렇게 터졌다). 억지로 태우면 EPCIS 에도 **EPC 가 아닌 문자열**이 `epcList` 로
|
|
4087
|
+
* 나가는데, 그것은 코어가 산출에서 경계한 바로 그 일이다.
|
|
4088
|
+
*
|
|
4089
|
+
* 그래서 만든 물건을 **입고가 하는 것과 같은 방식**으로 들여놓는다: 팔레트(SSCC)를 만들고 그 안에
|
|
4090
|
+
* 세트 N개를 담는다(AggregationEvent). 현장에서도 가공물은 팔레트에 실려 보관으로 간다.
|
|
4091
|
+
* 그 뒤는 기존 경로 그대로다 — `putaway` 로 보관 자리에 넣으면 판매 가능이 된다.
|
|
4092
|
+
*/
|
|
4093
|
+
onProcessComplete(t) {
|
|
4094
|
+
const made = (t.materialActual ?? []).filter((m) => m.use === "produced");
|
|
4095
|
+
const order = t.orderId ? this.orders.get(t.orderId) : void 0;
|
|
4096
|
+
const at = t.toNode;
|
|
4097
|
+
for (const m of made) {
|
|
4098
|
+
const key = subLotIdOf(m.definitionId, at);
|
|
4099
|
+
const row = this.items.get(key);
|
|
4100
|
+
if (!row) continue;
|
|
4101
|
+
const qty = row.qty ?? 0;
|
|
4102
|
+
if (qty <= 0) continue;
|
|
4103
|
+
this.items.delete(key);
|
|
4104
|
+
const pallet = ssccUri(COMPANY_PREFIX, ++this.epcSeq);
|
|
4105
|
+
const qtyList = [{ epcClass: m.definitionId, quantity: qty }];
|
|
4106
|
+
this.items.set(pallet, { epc: pallet, gtin: m.definitionId, qty, location: at, disposition: DISP.in_progress });
|
|
4107
|
+
const eventTime = this.now();
|
|
4108
|
+
this.emit(aggregationEvent({ eventTime, action: "ADD", bizStep: BIZSTEP.packing, parentID: pallet, childQuantityList: qtyList, readPoint: at }));
|
|
4109
|
+
this.emit(objectEvent({ eventTime, action: "ADD", bizStep: BIZSTEP.packing, disposition: DISP.in_progress, epcList: [pallet], quantityList: qtyList, readPoint: at, bizLocation: at }));
|
|
4110
|
+
const storage = this.locationByType("storage");
|
|
4111
|
+
if (storage) this.pushTask(order, "putaway", pallet, at, storage.id);
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
3846
4114
|
/** 전량 피킹 → packing(조립)·staging·shipping 마감. 화물 사이트 이탈, 백오더 잔량 재할당. */
|
|
3847
4115
|
finalizeOrder(order, staging) {
|
|
3848
4116
|
const shipDock = this.locationByType("dock-ship") ?? staging;
|
|
@@ -4067,13 +4335,18 @@ var MesKernel = class extends FlowEngine {
|
|
|
4067
4335
|
wipSeq = 0;
|
|
4068
4336
|
prodSeq = 0;
|
|
4069
4337
|
/** 정의-구동 모드(선택). 미지정 시 레거시 하드코딩 경로 — byte-identical. */
|
|
4070
|
-
|
|
4071
|
-
constructor(tenantId, policy = firstFitPolicy,
|
|
4338
|
+
productionSpec;
|
|
4339
|
+
constructor(tenantId, policy = firstFitPolicy, productionSpec) {
|
|
4072
4340
|
super(tenantId, policy);
|
|
4073
|
-
this.
|
|
4074
|
-
if (
|
|
4075
|
-
this.loadOperations(
|
|
4076
|
-
this.assertNoDoubleProduction(
|
|
4341
|
+
this.productionSpec = productionSpec;
|
|
4342
|
+
if (productionSpec?.definition?.operations) {
|
|
4343
|
+
this.loadOperations(productionSpec.definition.operations);
|
|
4344
|
+
this.assertNoDoubleProduction(productionSpec.definition.operations);
|
|
4345
|
+
}
|
|
4346
|
+
if (productionSpec?.definition?.recipes?.length && (!productionSpec.binding || !productionSpec.companyPrefix)) {
|
|
4347
|
+
throw new Error(
|
|
4348
|
+
"recipe-driven production needs `binding` and `companyPrefix` \u2014 the definition does not know GTINs, so material keys cannot be resolved to GS1 identifiers"
|
|
4349
|
+
);
|
|
4077
4350
|
}
|
|
4078
4351
|
}
|
|
4079
4352
|
/**
|
|
@@ -4089,10 +4362,10 @@ var MesKernel = class extends FlowEngine {
|
|
|
4089
4362
|
* 모든 계산이 거짓이 된다).
|
|
4090
4363
|
*/
|
|
4091
4364
|
assertNoDoubleProduction(ops) {
|
|
4092
|
-
const
|
|
4093
|
-
if (!
|
|
4365
|
+
const bad2 = ops.filter((o) => (o.materialSpecification ?? []).some((m) => m.use === "produced")).map((o) => o.key);
|
|
4366
|
+
if (!bad2.length) return;
|
|
4094
4367
|
throw new Error(
|
|
4095
|
-
`MES recipe already produces outputs \u2014 operations [${
|
|
4368
|
+
`MES recipe already produces outputs \u2014 operations [${bad2.join(", ")}] must not also declare materialSpecification use:'produced' (that would create the same output twice). Consumption specs are fine; declare production in the recipe.`
|
|
4096
4369
|
);
|
|
4097
4370
|
}
|
|
4098
4371
|
productOf(gtin) {
|
|
@@ -4121,7 +4394,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
4121
4394
|
}
|
|
4122
4395
|
/** 부품 수령(다품종) — skuMix 의 gtin 으로 부품 종류 결정. */
|
|
4123
4396
|
onArrival(spec) {
|
|
4124
|
-
if (this.
|
|
4397
|
+
if (this.productionSpec) return this.onArrivalDef(spec);
|
|
4125
4398
|
const rawStore = this.locationByType("raw-store");
|
|
4126
4399
|
if (!rawStore) return;
|
|
4127
4400
|
const gtin = this.pickGtin(spec.content.skuMix);
|
|
@@ -4134,7 +4407,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
4134
4407
|
}
|
|
4135
4408
|
/** 작업지시 — 제품 2종 교대(체인지오버 유발). 제품 gtin 을 오더에 기록. */
|
|
4136
4409
|
onOrder(_spec) {
|
|
4137
|
-
if (this.
|
|
4410
|
+
if (this.productionSpec) return this.onOrderDef(_spec);
|
|
4138
4411
|
const product = PRODUCTS[this.orderSeq % PRODUCTS.length];
|
|
4139
4412
|
const id = `order-${++this.orderSeq}`;
|
|
4140
4413
|
const wo = gdtiUri(CP2, "403", ++this.soSeq);
|
|
@@ -4144,7 +4417,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
4144
4417
|
}
|
|
4145
4418
|
/** 할당 — 제품 BOM 각 라인의 부품 예약(하나라도 부족하면 대기) + 라우트 첫 스테이션(절단) 태스크. */
|
|
4146
4419
|
allocate(o) {
|
|
4147
|
-
if (this.
|
|
4420
|
+
if (this.productionSpec) return this.allocateDef(o);
|
|
4148
4421
|
const s0 = ROUTE[0];
|
|
4149
4422
|
const first = this.locationByType(s0.locationType);
|
|
4150
4423
|
const product = this.productOf(o.gtin);
|
|
@@ -4179,12 +4452,12 @@ var MesKernel = class extends FlowEngine {
|
|
|
4179
4452
|
* 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만 접는다.
|
|
4180
4453
|
*/
|
|
4181
4454
|
canComplete(t) {
|
|
4182
|
-
if (this.
|
|
4455
|
+
if (this.productionSpec) return !!(t.orderId && this.orders.get(t.orderId));
|
|
4183
4456
|
const order = t.orderId ? this.orders.get(t.orderId) : void 0;
|
|
4184
4457
|
return !!order && !!this.productOf(order.gtin);
|
|
4185
4458
|
}
|
|
4186
4459
|
onTaskComplete(t) {
|
|
4187
|
-
if (this.
|
|
4460
|
+
if (this.productionSpec) return this.onTaskCompleteDef(t);
|
|
4188
4461
|
const order = this.orders.get(t.orderId);
|
|
4189
4462
|
const product = order && this.productOf(order.gtin);
|
|
4190
4463
|
if (!order || !product) throw new Error(`task ${t.id}: order/product vanished between the core check and the domain hook`);
|
|
@@ -4217,15 +4490,15 @@ var MesKernel = class extends FlowEngine {
|
|
|
4217
4490
|
}
|
|
4218
4491
|
// ── 정의-구동 모드 (도메인 정의 데이터로 실행 — 레거시와 분리, 하드코딩 대체) ──
|
|
4219
4492
|
recipeDef() {
|
|
4220
|
-
const d = this.
|
|
4221
|
-
return this.
|
|
4493
|
+
const d = this.productionSpec.definition;
|
|
4494
|
+
return this.productionSpec.recipeKey ? d.recipes?.find((r) => r.key === this.productionSpec.recipeKey) : d.recipes?.[0];
|
|
4222
4495
|
}
|
|
4223
4496
|
/** 자재 키 → 구체 gtin 클래스(idpat). 구체 식별은 바인딩+prefix 로 인스턴스가 주입. */
|
|
4224
4497
|
classOf(materialKey) {
|
|
4225
|
-
return sgtinClass(this.
|
|
4498
|
+
return sgtinClass(this.productionSpec.companyPrefix, this.productionSpec.binding[materialKey]);
|
|
4226
4499
|
}
|
|
4227
4500
|
serialOf(materialKey, serial) {
|
|
4228
|
-
return sgtinUri(this.
|
|
4501
|
+
return sgtinUri(this.productionSpec.companyPrefix, this.productionSpec.binding[materialKey], serial);
|
|
4229
4502
|
}
|
|
4230
4503
|
/**
|
|
4231
4504
|
* 라우트를 용량 계산에 알려 준다 — 수율을 거슬러 올릴 때 순서가 곧 계산이다.
|
|
@@ -4234,13 +4507,13 @@ var MesKernel = class extends FlowEngine {
|
|
|
4234
4507
|
* 틀린다). 생산 정의를 가진 커널만 이 답을 안다.
|
|
4235
4508
|
*/
|
|
4236
4509
|
routeKeys() {
|
|
4237
|
-
if (!this.
|
|
4238
|
-
const d = this.
|
|
4510
|
+
if (!this.productionSpec) return void 0;
|
|
4511
|
+
const d = this.productionSpec.definition;
|
|
4239
4512
|
return d.routes?.find((r) => r.key === this.recipeDef().route)?.steps;
|
|
4240
4513
|
}
|
|
4241
4514
|
/** recipe.route → 오퍼레이션 시퀀스 해소. */
|
|
4242
4515
|
routeOps() {
|
|
4243
|
-
const d = this.
|
|
4516
|
+
const d = this.productionSpec.definition;
|
|
4244
4517
|
const route = d.routes?.find((r) => r.key === this.recipeDef().route);
|
|
4245
4518
|
return (route?.steps ?? []).map((sk) => d.operations?.find((o) => o.key === sk)).filter((o) => !!o);
|
|
4246
4519
|
}
|
|
@@ -4261,7 +4534,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
4261
4534
|
onOrderDef(_spec) {
|
|
4262
4535
|
const rc = this.recipeDef();
|
|
4263
4536
|
const id = `order-${++this.orderSeq}`;
|
|
4264
|
-
const wo = gdtiUri(this.
|
|
4537
|
+
const wo = gdtiUri(this.productionSpec.companyPrefix, "403", ++this.soSeq);
|
|
4265
4538
|
const order = { id, kind: "workorder", status: "created", gtin: this.classOf(rc.outputs[0].material), requested: 1, fulfilled: 0, bizTransaction: wo, allocated: [], picked: [], ...this.promiseOf(_spec) };
|
|
4266
4539
|
this.orders.set(id, order);
|
|
4267
4540
|
this.emitOrder(order);
|
|
@@ -4304,8 +4577,8 @@ var MesKernel = class extends FlowEngine {
|
|
|
4304
4577
|
const isLast = i === ops.length - 1;
|
|
4305
4578
|
if (!isLast) {
|
|
4306
4579
|
const inputs = order.allocated.slice();
|
|
4307
|
-
const wip2 = sgtinUri(this.
|
|
4308
|
-
const wipGtin = sgtinClass(this.
|
|
4580
|
+
const wip2 = sgtinUri(this.productionSpec.companyPrefix, "WIP", ++this.wipSeq);
|
|
4581
|
+
const wipGtin = sgtinClass(this.productionSpec.companyPrefix, "WIP");
|
|
4309
4582
|
this.transform(inputs, [{ epc: wip2, gtin: wipGtin, qty: 1, location: loc.id, disposition: DISP.in_progress }], { bizStep, disposition: DISP.in_progress, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
4310
4583
|
order.allocated = [wip2];
|
|
4311
4584
|
const next = ops[i + 1];
|
|
@@ -4473,6 +4746,7 @@ function retiredVocabularyIn(line) {
|
|
|
4473
4746
|
transformationEvent,
|
|
4474
4747
|
validateDomainDefinition,
|
|
4475
4748
|
validateEpcisEvent,
|
|
4749
|
+
validateScenario,
|
|
4476
4750
|
weekdayAt,
|
|
4477
4751
|
workingTimeOfWeek
|
|
4478
4752
|
});
|
package/package.json
CHANGED