@tailor-platform/erp-kit 0.5.1 → 0.6.0
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/CHANGELOG.md +15 -0
- package/dist/cli.mjs +103 -23
- package/package.json +1 -1
- package/skills/erp-kit-app-5-impl-backend/SKILL.md +7 -4
- package/skills/erp-kit-app-7-impl-review/SKILL.md +1 -1
- package/skills/erp-kit-module-6-impl-review/SKILL.md +39 -17
- package/src/commands/generate-doc.ts +1 -1
- package/src/commands/lib/discovery.test.ts +13 -3
- package/src/commands/lib/discovery.ts +10 -2
- package/src/commands/lib/paths.ts +4 -2
- package/src/commands/lib/sync-check-tests.test.ts +84 -6
- package/src/commands/lib/sync-check-tests.ts +63 -3
- package/src/commands/sync-check.ts +7 -3
- package/src/generator/generate-app-code.ts +51 -16
- package/src/generator/generate-stubs.ts +4 -0
- package/src/generator/stub-templates.test.ts +11 -0
- package/src/generator/stub-templates.ts +22 -1
- package/src/modules/inventory/docs/features/inventory-adjustment.md +2 -1
- package/src/modules/inventory/docs/features/scrap-management.md +39 -1
- package/src/modules/manufacturing/README.md +63 -0
- package/src/modules/manufacturing/command/.gitkeep +0 -0
- package/src/modules/manufacturing/command/activateBillOfMaterial.generated.ts +6 -0
- package/src/modules/manufacturing/command/activateBillOfMaterial.test.ts +166 -0
- package/src/modules/manufacturing/command/activateBillOfMaterial.ts +173 -0
- package/src/modules/manufacturing/command/activateRouting.generated.ts +6 -0
- package/src/modules/manufacturing/command/activateRouting.test.ts +152 -0
- package/src/modules/manufacturing/command/activateRouting.ts +92 -0
- package/src/modules/manufacturing/command/activateWorkCenter.generated.ts +6 -0
- package/src/modules/manufacturing/command/activateWorkCenter.test.ts +135 -0
- package/src/modules/manufacturing/command/activateWorkCenter.ts +91 -0
- package/src/modules/manufacturing/command/cancelProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/cancelProductionOrder.test.ts +151 -0
- package/src/modules/manufacturing/command/cancelProductionOrder.ts +114 -0
- package/src/modules/manufacturing/command/closeProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/closeProductionOrder.test.ts +126 -0
- package/src/modules/manufacturing/command/closeProductionOrder.ts +87 -0
- package/src/modules/manufacturing/command/completeProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/completeProductionOrder.test.ts +132 -0
- package/src/modules/manufacturing/command/completeProductionOrder.ts +97 -0
- package/src/modules/manufacturing/command/completeWorkOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/completeWorkOrder.test.ts +369 -0
- package/src/modules/manufacturing/command/completeWorkOrder.ts +212 -0
- package/src/modules/manufacturing/command/createBillOfMaterial.generated.ts +6 -0
- package/src/modules/manufacturing/command/createBillOfMaterial.test.ts +210 -0
- package/src/modules/manufacturing/command/createBillOfMaterial.ts +176 -0
- package/src/modules/manufacturing/command/createProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/createProductionOrder.test.ts +160 -0
- package/src/modules/manufacturing/command/createProductionOrder.ts +129 -0
- package/src/modules/manufacturing/command/createRouting.generated.ts +6 -0
- package/src/modules/manufacturing/command/createRouting.test.ts +168 -0
- package/src/modules/manufacturing/command/createRouting.ts +128 -0
- package/src/modules/manufacturing/command/createWorkCenter.generated.ts +6 -0
- package/src/modules/manufacturing/command/createWorkCenter.test.ts +148 -0
- package/src/modules/manufacturing/command/createWorkCenter.ts +131 -0
- package/src/modules/manufacturing/command/deactivateBillOfMaterial.generated.ts +6 -0
- package/src/modules/manufacturing/command/deactivateBillOfMaterial.test.ts +103 -0
- package/src/modules/manufacturing/command/deactivateBillOfMaterial.ts +78 -0
- package/src/modules/manufacturing/command/deactivateRouting.generated.ts +6 -0
- package/src/modules/manufacturing/command/deactivateRouting.test.ts +112 -0
- package/src/modules/manufacturing/command/deactivateRouting.ts +76 -0
- package/src/modules/manufacturing/command/deactivateWorkCenter.generated.ts +6 -0
- package/src/modules/manufacturing/command/deactivateWorkCenter.test.ts +113 -0
- package/src/modules/manufacturing/command/deactivateWorkCenter.ts +85 -0
- package/src/modules/manufacturing/command/pauseWorkOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/pauseWorkOrder.test.ts +118 -0
- package/src/modules/manufacturing/command/pauseWorkOrder.ts +82 -0
- package/src/modules/manufacturing/command/recordInventoryIssueOutcome.generated.ts +6 -0
- package/src/modules/manufacturing/command/recordInventoryIssueOutcome.test.ts +183 -0
- package/src/modules/manufacturing/command/recordInventoryIssueOutcome.ts +139 -0
- package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.generated.ts +6 -0
- package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.test.ts +120 -0
- package/src/modules/manufacturing/command/recordManufacturingCostSettlementAcknowledgment.ts +110 -0
- package/src/modules/manufacturing/command/releaseProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/releaseProductionOrder.test.ts +220 -0
- package/src/modules/manufacturing/command/releaseProductionOrder.ts +450 -0
- package/src/modules/manufacturing/command/reopenProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/reopenProductionOrder.test.ts +196 -0
- package/src/modules/manufacturing/command/reopenProductionOrder.ts +98 -0
- package/src/modules/manufacturing/command/reportWorkOrderProgress.generated.ts +6 -0
- package/src/modules/manufacturing/command/reportWorkOrderProgress.test.ts +204 -0
- package/src/modules/manufacturing/command/reportWorkOrderProgress.ts +129 -0
- package/src/modules/manufacturing/command/rescheduleProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/rescheduleProductionOrder.test.ts +185 -0
- package/src/modules/manufacturing/command/rescheduleProductionOrder.ts +95 -0
- package/src/modules/manufacturing/command/resumeWorkOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/resumeWorkOrder.test.ts +122 -0
- package/src/modules/manufacturing/command/resumeWorkOrder.ts +94 -0
- package/src/modules/manufacturing/command/reviewManufacturingCostSummary.generated.ts +6 -0
- package/src/modules/manufacturing/command/reviewManufacturingCostSummary.test.ts +231 -0
- package/src/modules/manufacturing/command/reviewManufacturingCostSummary.ts +137 -0
- package/src/modules/manufacturing/command/startWorkOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/startWorkOrder.test.ts +118 -0
- package/src/modules/manufacturing/command/startWorkOrder.ts +126 -0
- package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.test.ts +153 -0
- package/src/modules/manufacturing/command/technicallyCompleteProductionOrder.ts +106 -0
- package/src/modules/manufacturing/command/unreleaseProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/unreleaseProductionOrder.test.ts +140 -0
- package/src/modules/manufacturing/command/unreleaseProductionOrder.ts +131 -0
- package/src/modules/manufacturing/command/updateBillOfMaterial.generated.ts +6 -0
- package/src/modules/manufacturing/command/updateBillOfMaterial.test.ts +149 -0
- package/src/modules/manufacturing/command/updateBillOfMaterial.ts +174 -0
- package/src/modules/manufacturing/command/updateProductionOrder.generated.ts +6 -0
- package/src/modules/manufacturing/command/updateProductionOrder.test.ts +112 -0
- package/src/modules/manufacturing/command/updateProductionOrder.ts +145 -0
- package/src/modules/manufacturing/command/updateRouting.generated.ts +6 -0
- package/src/modules/manufacturing/command/updateRouting.test.ts +211 -0
- package/src/modules/manufacturing/command/updateRouting.ts +124 -0
- package/src/modules/manufacturing/command/updateWorkCenter.generated.ts +6 -0
- package/src/modules/manufacturing/command/updateWorkCenter.test.ts +152 -0
- package/src/modules/manufacturing/command/updateWorkCenter.ts +137 -0
- package/src/modules/manufacturing/db/.gitkeep +0 -0
- package/src/modules/manufacturing/db/billOfMaterial.ts +70 -0
- package/src/modules/manufacturing/db/billOfMaterialLine.ts +49 -0
- package/src/modules/manufacturing/db/costVarianceLine.ts +53 -0
- package/src/modules/manufacturing/db/manufacturingCostLine.ts +35 -0
- package/src/modules/manufacturing/db/manufacturingCostSettlementRecord.ts +39 -0
- package/src/modules/manufacturing/db/manufacturingCostSummary.ts +59 -0
- package/src/modules/manufacturing/db/productionOrder.ts +83 -0
- package/src/modules/manufacturing/db/productionOrderBomSnapshot.ts +44 -0
- package/src/modules/manufacturing/db/productionOrderCostBaseline.ts +44 -0
- package/src/modules/manufacturing/db/productionOrderMaterialRequirement.ts +57 -0
- package/src/modules/manufacturing/db/productionOrderRoutingSnapshot.ts +43 -0
- package/src/modules/manufacturing/db/routing.ts +63 -0
- package/src/modules/manufacturing/db/routingOperation.ts +57 -0
- package/src/modules/manufacturing/db/workCenter.ts +87 -0
- package/src/modules/manufacturing/db/workOrder.ts +65 -0
- package/src/modules/manufacturing/db/workOrderExecutionEvent.ts +54 -0
- package/src/modules/manufacturing/docs/commands/ActivateBillOfMaterial.md +50 -0
- package/src/modules/manufacturing/docs/commands/ActivateRouting.md +48 -0
- package/src/modules/manufacturing/docs/commands/ActivateWorkCenter.md +49 -0
- package/src/modules/manufacturing/docs/commands/CancelProductionOrder.md +48 -0
- package/src/modules/manufacturing/docs/commands/CloseProductionOrder.md +46 -0
- package/src/modules/manufacturing/docs/commands/CompleteProductionOrder.md +48 -0
- package/src/modules/manufacturing/docs/commands/CompleteWorkOrder.md +66 -0
- package/src/modules/manufacturing/docs/commands/CreateBillOfMaterial.md +54 -0
- package/src/modules/manufacturing/docs/commands/CreateProductionOrder.md +49 -0
- package/src/modules/manufacturing/docs/commands/CreateRouting.md +50 -0
- package/src/modules/manufacturing/docs/commands/CreateWorkCenter.md +51 -0
- package/src/modules/manufacturing/docs/commands/DeactivateBillOfMaterial.md +45 -0
- package/src/modules/manufacturing/docs/commands/DeactivateRouting.md +45 -0
- package/src/modules/manufacturing/docs/commands/DeactivateWorkCenter.md +45 -0
- package/src/modules/manufacturing/docs/commands/PauseWorkOrder.md +44 -0
- package/src/modules/manufacturing/docs/commands/RecordInventoryIssueOutcome.md +59 -0
- package/src/modules/manufacturing/docs/commands/RecordManufacturingCostSettlementAcknowledgment.md +49 -0
- package/src/modules/manufacturing/docs/commands/ReleaseProductionOrder.md +57 -0
- package/src/modules/manufacturing/docs/commands/ReopenProductionOrder.md +54 -0
- package/src/modules/manufacturing/docs/commands/ReportWorkOrderProgress.md +53 -0
- package/src/modules/manufacturing/docs/commands/RescheduleProductionOrder.md +45 -0
- package/src/modules/manufacturing/docs/commands/ResumeWorkOrder.md +44 -0
- package/src/modules/manufacturing/docs/commands/ReviewManufacturingCostSummary.md +52 -0
- package/src/modules/manufacturing/docs/commands/StartWorkOrder.md +46 -0
- package/src/modules/manufacturing/docs/commands/TechnicallyCompleteProductionOrder.md +51 -0
- package/src/modules/manufacturing/docs/commands/UnreleaseProductionOrder.md +46 -0
- package/src/modules/manufacturing/docs/commands/UpdateBillOfMaterial.md +48 -0
- package/src/modules/manufacturing/docs/commands/UpdateProductionOrder.md +48 -0
- package/src/modules/manufacturing/docs/commands/UpdateRouting.md +52 -0
- package/src/modules/manufacturing/docs/commands/UpdateWorkCenter.md +48 -0
- package/src/modules/manufacturing/docs/features/bill-of-material-management.md +83 -0
- package/src/modules/manufacturing/docs/features/manufacturing-cost-and-variance.md +191 -0
- package/src/modules/manufacturing/docs/features/production-order-lifecycle.md +103 -0
- package/src/modules/manufacturing/docs/features/routing-and-work-center-definition.md +63 -0
- package/src/modules/manufacturing/docs/features/work-order-execution.md +115 -0
- package/src/modules/manufacturing/docs/models/BillOfMaterial.md +60 -0
- package/src/modules/manufacturing/docs/models/ManufacturingCostSummary.md +66 -0
- package/src/modules/manufacturing/docs/models/ProductionOrder.md +76 -0
- package/src/modules/manufacturing/docs/models/Routing.md +58 -0
- package/src/modules/manufacturing/docs/models/WorkCenter.md +56 -0
- package/src/modules/manufacturing/docs/models/WorkOrder.md +63 -0
- package/src/modules/manufacturing/docs/queries/DetectBillOfMaterialCircularReference.md +39 -0
- package/src/modules/manufacturing/docs/queries/ExplodeBillOfMaterial.md +56 -0
- package/src/modules/manufacturing/docs/queries/GetBillOfMaterial.md +37 -0
- package/src/modules/manufacturing/docs/queries/GetManufacturingCostSummary.md +39 -0
- package/src/modules/manufacturing/docs/queries/GetProductionOrder.md +37 -0
- package/src/modules/manufacturing/docs/queries/GetRouting.md +39 -0
- package/src/modules/manufacturing/docs/queries/GetWorkCenter.md +35 -0
- package/src/modules/manufacturing/docs/queries/GetWorkOrder.md +38 -0
- package/src/modules/manufacturing/docs/queries/ListBillOfMaterialsByItem.md +42 -0
- package/src/modules/manufacturing/docs/queries/ListManufacturingCostSummariesByStatus.md +41 -0
- package/src/modules/manufacturing/docs/queries/ListProductionOrdersByStatus.md +41 -0
- package/src/modules/manufacturing/docs/queries/ListRoutingsByItem.md +42 -0
- package/src/modules/manufacturing/docs/queries/ListWorkCentersBySite.md +38 -0
- package/src/modules/manufacturing/docs/queries/ListWorkOrdersByProductionOrder.md +39 -0
- package/src/modules/manufacturing/docs/queries/ListWorkOrdersByWorkCenter.md +43 -0
- package/src/modules/manufacturing/executor/.gitkeep +0 -0
- package/src/modules/manufacturing/generated/enums.ts +113 -0
- package/src/modules/manufacturing/generated/kysely-tailordb.ts +247 -0
- package/src/modules/manufacturing/index.ts +2 -0
- package/src/modules/manufacturing/lib/_db_deps.ts +22 -0
- package/src/modules/manufacturing/lib/errors.generated.ts +592 -0
- package/src/modules/manufacturing/lib/permissions.generated.ts +35 -0
- package/src/modules/manufacturing/lib/types.ts +111 -0
- package/src/modules/manufacturing/module.ts +226 -0
- package/src/modules/manufacturing/permissions.ts +3 -0
- package/src/modules/manufacturing/query/.gitkeep +0 -0
- package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.generated.ts +5 -0
- package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.test.ts +115 -0
- package/src/modules/manufacturing/query/detectBillOfMaterialCircularReference.ts +79 -0
- package/src/modules/manufacturing/query/explodeBillOfMaterial.generated.ts +5 -0
- package/src/modules/manufacturing/query/explodeBillOfMaterial.test.ts +445 -0
- package/src/modules/manufacturing/query/explodeBillOfMaterial.ts +306 -0
- package/src/modules/manufacturing/query/getBillOfMaterial.generated.ts +5 -0
- package/src/modules/manufacturing/query/getBillOfMaterial.test.ts +64 -0
- package/src/modules/manufacturing/query/getBillOfMaterial.ts +27 -0
- package/src/modules/manufacturing/query/getManufacturingCostSummary.generated.ts +5 -0
- package/src/modules/manufacturing/query/getManufacturingCostSummary.test.ts +147 -0
- package/src/modules/manufacturing/query/getManufacturingCostSummary.ts +46 -0
- package/src/modules/manufacturing/query/getProductionOrder.generated.ts +5 -0
- package/src/modules/manufacturing/query/getProductionOrder.test.ts +139 -0
- package/src/modules/manufacturing/query/getProductionOrder.ts +84 -0
- package/src/modules/manufacturing/query/getRouting.generated.ts +5 -0
- package/src/modules/manufacturing/query/getRouting.test.ts +71 -0
- package/src/modules/manufacturing/query/getRouting.ts +34 -0
- package/src/modules/manufacturing/query/getWorkCenter.generated.ts +5 -0
- package/src/modules/manufacturing/query/getWorkCenter.test.ts +37 -0
- package/src/modules/manufacturing/query/getWorkCenter.ts +21 -0
- package/src/modules/manufacturing/query/getWorkOrder.generated.ts +5 -0
- package/src/modules/manufacturing/query/getWorkOrder.test.ts +73 -0
- package/src/modules/manufacturing/query/getWorkOrder.ts +28 -0
- package/src/modules/manufacturing/query/listBillOfMaterialsByItem.generated.ts +5 -0
- package/src/modules/manufacturing/query/listBillOfMaterialsByItem.test.ts +107 -0
- package/src/modules/manufacturing/query/listBillOfMaterialsByItem.ts +58 -0
- package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.generated.ts +5 -0
- package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.test.ts +96 -0
- package/src/modules/manufacturing/query/listManufacturingCostSummariesByStatus.ts +77 -0
- package/src/modules/manufacturing/query/listProductionOrdersByStatus.generated.ts +5 -0
- package/src/modules/manufacturing/query/listProductionOrdersByStatus.test.ts +121 -0
- package/src/modules/manufacturing/query/listProductionOrdersByStatus.ts +83 -0
- package/src/modules/manufacturing/query/listRoutingsByItem.generated.ts +5 -0
- package/src/modules/manufacturing/query/listRoutingsByItem.test.ts +110 -0
- package/src/modules/manufacturing/query/listRoutingsByItem.ts +54 -0
- package/src/modules/manufacturing/query/listWorkCentersBySite.generated.ts +5 -0
- package/src/modules/manufacturing/query/listWorkCentersBySite.test.ts +81 -0
- package/src/modules/manufacturing/query/listWorkCentersBySite.ts +70 -0
- package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.generated.ts +5 -0
- package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.test.ts +102 -0
- package/src/modules/manufacturing/query/listWorkOrdersByProductionOrder.ts +53 -0
- package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.generated.ts +5 -0
- package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.test.ts +143 -0
- package/src/modules/manufacturing/query/listWorkOrdersByWorkCenter.ts +56 -0
- package/src/modules/manufacturing/seed/index.ts +19 -0
- package/src/modules/manufacturing/tailor.config.ts +13 -0
- package/src/modules/manufacturing/tailor.d.ts +13 -0
- package/src/modules/manufacturing/testing/commandTestUtils.ts +29 -0
- package/src/modules/manufacturing/testing/fixtures.ts +402 -0
- package/templates/scaffold/app/backend/package.json +9 -2
- package/templates/scaffold/app/backend/src/tests/utils/graphql-client.ts +66 -0
- package/templates/scaffold/app/backend/src/tests/utils/setup.ts +21 -0
- package/templates/scaffold/app/backend/tsconfig.json +9 -2
- package/templates/scaffold/app/backend/vitest.config.ts +35 -0
- package/templates/scaffold/app/frontend/package.json +2 -2
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
ProductionOrderNotFoundError,
|
|
4
|
+
ProductionOrderNotCompletableError,
|
|
5
|
+
OpenWorkOrderRemainsError,
|
|
6
|
+
FinalOutputRequiredError,
|
|
7
|
+
FinalReceiptRequiredError,
|
|
8
|
+
} from "../lib/errors.generated";
|
|
9
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
10
|
+
|
|
11
|
+
export interface CompleteProductionOrderInput {
|
|
12
|
+
id: string;
|
|
13
|
+
from?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Function: completeProductionOrder
|
|
18
|
+
*
|
|
19
|
+
* Marks physical production complete once required work orders are finished
|
|
20
|
+
* and final receipt obligations are satisfied. The command freezes production
|
|
21
|
+
* execution while still allowing later technical completion and review.
|
|
22
|
+
*/
|
|
23
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
24
|
+
db: Transaction,
|
|
25
|
+
input: CompleteProductionOrderInput & CF,
|
|
26
|
+
_ctx: CommandContext,
|
|
27
|
+
) {
|
|
28
|
+
const { id, from, ...customFields } = input;
|
|
29
|
+
void customFields;
|
|
30
|
+
|
|
31
|
+
const allowedStatuses = from ?? ["IN_PROGRESS"];
|
|
32
|
+
|
|
33
|
+
// 1. Fetch production order with lock
|
|
34
|
+
const order = await db
|
|
35
|
+
.selectFrom("ProductionOrder")
|
|
36
|
+
.selectAll()
|
|
37
|
+
.where("id", "=", id)
|
|
38
|
+
.forUpdate()
|
|
39
|
+
.executeTakeFirst();
|
|
40
|
+
|
|
41
|
+
if (!order) {
|
|
42
|
+
return err(new ProductionOrderNotFoundError(id));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Validate status is completable
|
|
46
|
+
if (!allowedStatuses.includes(order.status)) {
|
|
47
|
+
return err(new ProductionOrderNotCompletableError(id));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. Check all required work orders are COMPLETE or CANCELLED
|
|
51
|
+
const workOrders = await db
|
|
52
|
+
.selectFrom("WorkOrder")
|
|
53
|
+
.selectAll()
|
|
54
|
+
.where("productionOrderId", "=", id)
|
|
55
|
+
.execute();
|
|
56
|
+
|
|
57
|
+
const hasOpenWorkOrders = workOrders.some(
|
|
58
|
+
(wo) => wo.status !== "COMPLETE" && wo.status !== "CANCELLED",
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (hasOpenWorkOrders) {
|
|
62
|
+
return err(new OpenWorkOrderRemainsError(id));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 4. Check final output has been reported
|
|
66
|
+
const hasCompletedOutput = workOrders.some(
|
|
67
|
+
(wo) => wo.status === "COMPLETE" && wo.completedQuantity > 0,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (!hasCompletedOutput) {
|
|
71
|
+
return err(new FinalOutputRequiredError(id));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 5. Check receipt handoff evidence exists
|
|
75
|
+
const costSummary = await db
|
|
76
|
+
.selectFrom("ManufacturingCostSummary")
|
|
77
|
+
.selectAll()
|
|
78
|
+
.where("productionOrderId", "=", id)
|
|
79
|
+
.executeTakeFirst();
|
|
80
|
+
|
|
81
|
+
if (!costSummary || costSummary.actualMaterialCost <= 0) {
|
|
82
|
+
return err(new FinalReceiptRequiredError(id));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 6. Set status to COMPLETED
|
|
86
|
+
const completedOrder = await db
|
|
87
|
+
.updateTable("ProductionOrder")
|
|
88
|
+
.set({
|
|
89
|
+
status: "COMPLETED",
|
|
90
|
+
updatedAt: new Date(),
|
|
91
|
+
})
|
|
92
|
+
.where("id", "=", id)
|
|
93
|
+
.returningAll()
|
|
94
|
+
.executeTakeFirstOrThrow();
|
|
95
|
+
|
|
96
|
+
return ok({ productionOrder: completedOrder });
|
|
97
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./completeWorkOrder";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const completeWorkOrder = defineCommand(permissions.completeWorkOrder, run);
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../../testing/index";
|
|
3
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
4
|
+
import {
|
|
5
|
+
WorkOrderNotFoundError,
|
|
6
|
+
WorkOrderNotCompletableError,
|
|
7
|
+
WorkOrderNotStartedError,
|
|
8
|
+
InvalidCompletionQuantityError,
|
|
9
|
+
DuplicateBackflushRiskError,
|
|
10
|
+
ReceiptHandoffRequiredError,
|
|
11
|
+
LotReferenceRequiredError,
|
|
12
|
+
SerialReferenceRequiredError,
|
|
13
|
+
} from "../lib/errors.generated";
|
|
14
|
+
import {
|
|
15
|
+
baseInProgressWorkOrder,
|
|
16
|
+
basePendingWorkOrder,
|
|
17
|
+
baseCompleteWorkOrder,
|
|
18
|
+
} from "../testing/fixtures";
|
|
19
|
+
import { run } from "./completeWorkOrder";
|
|
20
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
21
|
+
|
|
22
|
+
describe("completeWorkOrder", () => {
|
|
23
|
+
const ctx: CommandContext = {
|
|
24
|
+
actorId: "test-actor",
|
|
25
|
+
permissions: ["manufacturing:completeWorkOrder"],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const baseInput = {
|
|
29
|
+
id: baseInProgressWorkOrder.id,
|
|
30
|
+
completedQuantity: 50,
|
|
31
|
+
backflushRequired: false,
|
|
32
|
+
receiptRequired: false,
|
|
33
|
+
notes: "Final completion",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
it("completes an in-progress work order with final quantity reporting", async () => {
|
|
37
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
38
|
+
const completedWorkOrder = {
|
|
39
|
+
...baseInProgressWorkOrder,
|
|
40
|
+
status: "COMPLETE" as const,
|
|
41
|
+
completedQuantity: 100,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// work order lookup
|
|
45
|
+
spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
|
|
46
|
+
// update work order
|
|
47
|
+
spies.update.mockReturnValueOnce(completedWorkOrder);
|
|
48
|
+
// insert execution event
|
|
49
|
+
spies.insert.mockReturnValueOnce(undefined);
|
|
50
|
+
// sibling work orders (all complete after this one)
|
|
51
|
+
spies.select.mockReturnValueOnce([
|
|
52
|
+
{ ...baseInProgressWorkOrder, id: baseInProgressWorkOrder.id, status: "IN_PROGRESS" },
|
|
53
|
+
]);
|
|
54
|
+
// update parent production order
|
|
55
|
+
spies.update.mockReturnValueOnce(undefined);
|
|
56
|
+
|
|
57
|
+
const result = await run(db, baseInput, ctx);
|
|
58
|
+
|
|
59
|
+
expect(result.ok).toBe(true);
|
|
60
|
+
if (result.ok) {
|
|
61
|
+
expect(result.value.workOrder.status).toBe("COMPLETE");
|
|
62
|
+
expect(result.value.workOrder.completedQuantity).toBe(100);
|
|
63
|
+
}
|
|
64
|
+
expect(spies.update).toHaveBeenCalled();
|
|
65
|
+
expect(spies.insert).toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns error when the work order does not exist", async () => {
|
|
69
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
70
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
71
|
+
|
|
72
|
+
const result = await run(db, { ...baseInput, id: "nonexistent" }, ctx);
|
|
73
|
+
|
|
74
|
+
expect(result.ok).toBe(false);
|
|
75
|
+
if (!result.ok) {
|
|
76
|
+
expect(result.error).toBeInstanceOf(WorkOrderNotFoundError);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns error when the work order is not in progress", async () => {
|
|
81
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
82
|
+
spies.select.mockReturnValueOnce(basePendingWorkOrder);
|
|
83
|
+
|
|
84
|
+
const result = await run(db, { ...baseInput, id: basePendingWorkOrder.id }, ctx);
|
|
85
|
+
|
|
86
|
+
expect(result.ok).toBe(false);
|
|
87
|
+
if (!result.ok) {
|
|
88
|
+
expect(result.error).toBeInstanceOf(WorkOrderNotCompletableError);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns error when the work order was never started", async () => {
|
|
93
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
94
|
+
const unstartedWorkOrder = {
|
|
95
|
+
...baseInProgressWorkOrder,
|
|
96
|
+
actualStartDate: null,
|
|
97
|
+
};
|
|
98
|
+
spies.select.mockReturnValueOnce(unstartedWorkOrder);
|
|
99
|
+
|
|
100
|
+
const result = await run(db, baseInput, ctx);
|
|
101
|
+
|
|
102
|
+
expect(result.ok).toBe(false);
|
|
103
|
+
if (!result.ok) {
|
|
104
|
+
expect(result.error).toBeInstanceOf(WorkOrderNotStartedError);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("returns error when completion quantity is invalid", async () => {
|
|
109
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
110
|
+
spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
|
|
111
|
+
|
|
112
|
+
const result = await run(db, { ...baseInput, completedQuantity: 0 }, ctx);
|
|
113
|
+
|
|
114
|
+
expect(result.ok).toBe(false);
|
|
115
|
+
if (!result.ok) {
|
|
116
|
+
expect(result.error).toBeInstanceOf(InvalidCompletionQuantityError);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("returns error when backflush would duplicate manual issue", async () => {
|
|
121
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
122
|
+
spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
|
|
123
|
+
|
|
124
|
+
const result = await run(
|
|
125
|
+
db,
|
|
126
|
+
{
|
|
127
|
+
...baseInput,
|
|
128
|
+
backflushRequired: true,
|
|
129
|
+
manuallyIssuedQuantity: 10,
|
|
130
|
+
},
|
|
131
|
+
ctx,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(result.ok).toBe(false);
|
|
135
|
+
if (!result.ok) {
|
|
136
|
+
expect(result.error).toBeInstanceOf(DuplicateBackflushRiskError);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("emits receipt handoff when output receipt is required", async () => {
|
|
141
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
142
|
+
const completedWorkOrder = {
|
|
143
|
+
...baseInProgressWorkOrder,
|
|
144
|
+
status: "COMPLETE" as const,
|
|
145
|
+
completedQuantity: 100,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// work order lookup
|
|
149
|
+
spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
|
|
150
|
+
// update work order
|
|
151
|
+
spies.update.mockReturnValueOnce(completedWorkOrder);
|
|
152
|
+
// insert execution event
|
|
153
|
+
spies.insert.mockReturnValueOnce(undefined);
|
|
154
|
+
// sibling work orders
|
|
155
|
+
spies.select.mockReturnValueOnce([
|
|
156
|
+
{ ...baseInProgressWorkOrder, id: baseInProgressWorkOrder.id, status: "IN_PROGRESS" },
|
|
157
|
+
]);
|
|
158
|
+
// update parent production order
|
|
159
|
+
spies.update.mockReturnValueOnce(undefined);
|
|
160
|
+
|
|
161
|
+
const result = await run(
|
|
162
|
+
db,
|
|
163
|
+
{
|
|
164
|
+
...baseInput,
|
|
165
|
+
receiptRequired: true,
|
|
166
|
+
receiptData: {
|
|
167
|
+
itemReference: "item-fg-1",
|
|
168
|
+
unitOfMeasure: "EA",
|
|
169
|
+
siteReference: "site-1",
|
|
170
|
+
postingDate: new Date("2024-03-01T12:00:00.000Z"),
|
|
171
|
+
lotTracked: false,
|
|
172
|
+
serialTracked: false,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
ctx,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(result.ok).toBe(true);
|
|
179
|
+
if (result.ok) {
|
|
180
|
+
expect(result.value.workOrder.status).toBe("COMPLETE");
|
|
181
|
+
expect(result.value.receiptHandoff?.itemReference).toBe("item-fg-1");
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("returns error when receipt is required but receipt data is missing", async () => {
|
|
186
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
187
|
+
spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
|
|
188
|
+
|
|
189
|
+
const result = await run(
|
|
190
|
+
db,
|
|
191
|
+
{
|
|
192
|
+
...baseInput,
|
|
193
|
+
receiptRequired: true,
|
|
194
|
+
receiptData: null,
|
|
195
|
+
},
|
|
196
|
+
ctx,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
expect(result.ok).toBe(false);
|
|
200
|
+
if (!result.ok) {
|
|
201
|
+
expect(result.error).toBeInstanceOf(ReceiptHandoffRequiredError);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("returns error when a lot-tracked receipt omits finishedGoodLotReference", async () => {
|
|
206
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
207
|
+
spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
|
|
208
|
+
|
|
209
|
+
const result = await run(
|
|
210
|
+
db,
|
|
211
|
+
{
|
|
212
|
+
...baseInput,
|
|
213
|
+
receiptRequired: true,
|
|
214
|
+
receiptData: {
|
|
215
|
+
itemReference: "item-fg-1",
|
|
216
|
+
unitOfMeasure: "EA",
|
|
217
|
+
siteReference: "site-1",
|
|
218
|
+
postingDate: new Date("2024-03-01T12:00:00.000Z"),
|
|
219
|
+
lotTracked: true,
|
|
220
|
+
serialTracked: false,
|
|
221
|
+
finishedGoodLotReference: null,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
ctx,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
expect(result.ok).toBe(false);
|
|
228
|
+
if (!result.ok) {
|
|
229
|
+
expect(result.error).toBeInstanceOf(LotReferenceRequiredError);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("returns error when a serial-tracked receipt omits serialReferences", async () => {
|
|
234
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
235
|
+
spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
|
|
236
|
+
|
|
237
|
+
const result = await run(
|
|
238
|
+
db,
|
|
239
|
+
{
|
|
240
|
+
...baseInput,
|
|
241
|
+
receiptRequired: true,
|
|
242
|
+
receiptData: {
|
|
243
|
+
itemReference: "item-fg-1",
|
|
244
|
+
unitOfMeasure: "EA",
|
|
245
|
+
siteReference: "site-1",
|
|
246
|
+
postingDate: new Date("2024-03-01T12:00:00.000Z"),
|
|
247
|
+
lotTracked: false,
|
|
248
|
+
serialTracked: true,
|
|
249
|
+
serialReferences: null,
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
ctx,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
expect(result.ok).toBe(false);
|
|
256
|
+
if (!result.ok) {
|
|
257
|
+
expect(result.error).toBeInstanceOf(SerialReferenceRequiredError);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("rolls up completion to the parent order", async () => {
|
|
262
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
263
|
+
const completedWorkOrder = {
|
|
264
|
+
...baseInProgressWorkOrder,
|
|
265
|
+
status: "COMPLETE" as const,
|
|
266
|
+
completedQuantity: 100,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// work order lookup
|
|
270
|
+
spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
|
|
271
|
+
// update work order
|
|
272
|
+
spies.update.mockReturnValueOnce(completedWorkOrder);
|
|
273
|
+
// insert execution event
|
|
274
|
+
spies.insert.mockReturnValueOnce(undefined);
|
|
275
|
+
// sibling work orders - all siblings already complete/cancelled
|
|
276
|
+
spies.select.mockReturnValueOnce([
|
|
277
|
+
{ ...baseInProgressWorkOrder, id: baseInProgressWorkOrder.id, status: "IN_PROGRESS" },
|
|
278
|
+
{ ...baseCompleteWorkOrder, id: "work-order-sibling", status: "COMPLETE" },
|
|
279
|
+
]);
|
|
280
|
+
// update parent production order to COMPLETED
|
|
281
|
+
spies.update.mockReturnValueOnce(undefined);
|
|
282
|
+
|
|
283
|
+
const result = await run(db, baseInput, ctx);
|
|
284
|
+
|
|
285
|
+
expect(result.ok).toBe(true);
|
|
286
|
+
// The update spy should have been called twice: once for the work order, once for the parent order
|
|
287
|
+
expect(spies.update).toHaveBeenCalledTimes(2);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("allows zero-quantity completion only under an explicit bypass policy", async () => {
|
|
291
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
292
|
+
const completedWorkOrder = {
|
|
293
|
+
...baseInProgressWorkOrder,
|
|
294
|
+
status: "COMPLETE" as const,
|
|
295
|
+
completedQuantity: baseInProgressWorkOrder.completedQuantity,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
|
|
299
|
+
spies.update.mockReturnValueOnce(completedWorkOrder);
|
|
300
|
+
spies.insert.mockReturnValueOnce(undefined);
|
|
301
|
+
spies.select.mockReturnValueOnce([
|
|
302
|
+
{ ...baseInProgressWorkOrder, id: baseInProgressWorkOrder.id, status: "IN_PROGRESS" },
|
|
303
|
+
]);
|
|
304
|
+
spies.update.mockReturnValueOnce(undefined);
|
|
305
|
+
|
|
306
|
+
const result = await run(
|
|
307
|
+
db,
|
|
308
|
+
{
|
|
309
|
+
...baseInput,
|
|
310
|
+
completedQuantity: 0,
|
|
311
|
+
zeroQuantityBypassPolicy: {
|
|
312
|
+
allowZeroCompletion: true,
|
|
313
|
+
reasonCode: "QUALITY_HOLD",
|
|
314
|
+
},
|
|
315
|
+
receiptRequired: true,
|
|
316
|
+
receiptData: {
|
|
317
|
+
itemReference: "item-fg-1",
|
|
318
|
+
unitOfMeasure: "EA",
|
|
319
|
+
siteReference: "site-1",
|
|
320
|
+
postingDate: new Date("2024-03-01T12:00:00.000Z"),
|
|
321
|
+
lotTracked: false,
|
|
322
|
+
serialTracked: false,
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
ctx,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
expect(result.ok).toBe(true);
|
|
329
|
+
if (result.ok) {
|
|
330
|
+
expect(result.value.backflushHandoff).toBeNull();
|
|
331
|
+
expect(result.value.receiptHandoff?.quantity).toBe(0);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("emits a backflush handoff when completion requires backflush consumption", async () => {
|
|
336
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
337
|
+
const completedWorkOrder = {
|
|
338
|
+
...baseInProgressWorkOrder,
|
|
339
|
+
status: "COMPLETE" as const,
|
|
340
|
+
completedQuantity: 100,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
spies.select.mockReturnValueOnce(baseInProgressWorkOrder);
|
|
344
|
+
spies.update.mockReturnValueOnce(completedWorkOrder);
|
|
345
|
+
spies.insert.mockReturnValueOnce(undefined);
|
|
346
|
+
spies.select.mockReturnValueOnce([
|
|
347
|
+
{ ...baseInProgressWorkOrder, id: baseInProgressWorkOrder.id, status: "IN_PROGRESS" },
|
|
348
|
+
]);
|
|
349
|
+
spies.update.mockReturnValueOnce(undefined);
|
|
350
|
+
|
|
351
|
+
const result = await run(
|
|
352
|
+
db,
|
|
353
|
+
{
|
|
354
|
+
...baseInput,
|
|
355
|
+
backflushRequired: true,
|
|
356
|
+
manuallyIssuedQuantity: 0,
|
|
357
|
+
},
|
|
358
|
+
ctx,
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
expect(result.ok).toBe(true);
|
|
362
|
+
if (result.ok) {
|
|
363
|
+
expect(result.value.backflushHandoff?.productionOrderReference).toBe(
|
|
364
|
+
baseInProgressWorkOrder.productionOrderId,
|
|
365
|
+
);
|
|
366
|
+
expect(result.value.backflushHandoff?.completedQuantity).toBe(50);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
WorkOrderNotFoundError,
|
|
4
|
+
WorkOrderNotCompletableError,
|
|
5
|
+
WorkOrderNotStartedError,
|
|
6
|
+
InvalidCompletionQuantityError,
|
|
7
|
+
DuplicateBackflushRiskError,
|
|
8
|
+
ReceiptHandoffRequiredError,
|
|
9
|
+
LotReferenceRequiredError,
|
|
10
|
+
SerialReferenceRequiredError,
|
|
11
|
+
} from "../lib/errors.generated";
|
|
12
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
13
|
+
|
|
14
|
+
export interface ReceiptData {
|
|
15
|
+
itemReference?: string | null;
|
|
16
|
+
unitOfMeasure?: string | null;
|
|
17
|
+
siteReference?: string | null;
|
|
18
|
+
postingDate?: Date | null;
|
|
19
|
+
storageLocationReference?: string | null;
|
|
20
|
+
lotTracked: boolean;
|
|
21
|
+
serialTracked: boolean;
|
|
22
|
+
finishedGoodLotReference?: string | null;
|
|
23
|
+
serialReferences?: string[] | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ZeroQuantityBypassPolicy {
|
|
27
|
+
allowZeroCompletion: boolean;
|
|
28
|
+
reasonCode?: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CompleteWorkOrderInput {
|
|
32
|
+
id: string;
|
|
33
|
+
completedQuantity: number;
|
|
34
|
+
zeroQuantityBypassPolicy?: ZeroQuantityBypassPolicy | null;
|
|
35
|
+
backflushRequired: boolean;
|
|
36
|
+
manuallyIssuedQuantity?: number;
|
|
37
|
+
receiptRequired: boolean;
|
|
38
|
+
receiptData?: ReceiptData | null;
|
|
39
|
+
notes?: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Function: completeWorkOrder
|
|
44
|
+
*
|
|
45
|
+
* Finishes execution on an in-progress work order. Records the final completed
|
|
46
|
+
* quantity, validates backflush and receipt-handoff obligations, creates a
|
|
47
|
+
* COMPLETED execution event, and rolls up completion to the parent production
|
|
48
|
+
* order when all sibling work orders are finished.
|
|
49
|
+
*/
|
|
50
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
51
|
+
db: Transaction,
|
|
52
|
+
input: CompleteWorkOrderInput & CF,
|
|
53
|
+
_ctx: CommandContext,
|
|
54
|
+
) {
|
|
55
|
+
const {
|
|
56
|
+
id,
|
|
57
|
+
completedQuantity,
|
|
58
|
+
zeroQuantityBypassPolicy,
|
|
59
|
+
backflushRequired,
|
|
60
|
+
manuallyIssuedQuantity,
|
|
61
|
+
receiptRequired,
|
|
62
|
+
receiptData,
|
|
63
|
+
notes,
|
|
64
|
+
...customFields
|
|
65
|
+
} = input;
|
|
66
|
+
void customFields;
|
|
67
|
+
|
|
68
|
+
// 1. Fetch work order with lock
|
|
69
|
+
const workOrder = await db
|
|
70
|
+
.selectFrom("WorkOrder")
|
|
71
|
+
.selectAll()
|
|
72
|
+
.where("id", "=", id)
|
|
73
|
+
.forUpdate()
|
|
74
|
+
.executeTakeFirst();
|
|
75
|
+
|
|
76
|
+
if (!workOrder) {
|
|
77
|
+
return err(new WorkOrderNotFoundError(id));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 2. Validate status is IN_PROGRESS
|
|
81
|
+
if (workOrder.status !== "IN_PROGRESS") {
|
|
82
|
+
return err(new WorkOrderNotCompletableError(id));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 3. Validate actual start evidence exists
|
|
86
|
+
if (!workOrder.actualStartDate) {
|
|
87
|
+
return err(new WorkOrderNotStartedError(id));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 4. Validate completed quantity, allowing explicit zero-quantity bypass.
|
|
91
|
+
const zeroQuantityBypassAllowed =
|
|
92
|
+
completedQuantity === 0 && zeroQuantityBypassPolicy?.allowZeroCompletion === true;
|
|
93
|
+
if (completedQuantity < 0 || (completedQuantity === 0 && !zeroQuantityBypassAllowed)) {
|
|
94
|
+
return err(new InvalidCompletionQuantityError(id));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 5. Validate backflush does not duplicate manual issue
|
|
98
|
+
if (backflushRequired && manuallyIssuedQuantity != null && manuallyIssuedQuantity > 0) {
|
|
99
|
+
return err(new DuplicateBackflushRiskError(id));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 6. Validate receipt handoff data when receipt is required
|
|
103
|
+
if (receiptRequired) {
|
|
104
|
+
if (!receiptData) {
|
|
105
|
+
return err(new ReceiptHandoffRequiredError(id));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
!receiptData.itemReference ||
|
|
110
|
+
!receiptData.unitOfMeasure ||
|
|
111
|
+
!receiptData.siteReference ||
|
|
112
|
+
!receiptData.postingDate
|
|
113
|
+
) {
|
|
114
|
+
return err(new ReceiptHandoffRequiredError(id));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (receiptData.lotTracked && !receiptData.finishedGoodLotReference) {
|
|
118
|
+
return err(new LotReferenceRequiredError(id));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
receiptData.serialTracked &&
|
|
123
|
+
(!receiptData.serialReferences || receiptData.serialReferences.length === 0)
|
|
124
|
+
) {
|
|
125
|
+
return err(new SerialReferenceRequiredError(id));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 7. Update work order to COMPLETE
|
|
130
|
+
const now = new Date();
|
|
131
|
+
const updatedWorkOrder = await db
|
|
132
|
+
.updateTable("WorkOrder")
|
|
133
|
+
.set({
|
|
134
|
+
status: "COMPLETE",
|
|
135
|
+
completedQuantity: workOrder.completedQuantity + completedQuantity,
|
|
136
|
+
executionNotes: notes ?? workOrder.executionNotes,
|
|
137
|
+
updatedAt: now,
|
|
138
|
+
})
|
|
139
|
+
.where("id", "=", id)
|
|
140
|
+
.returningAll()
|
|
141
|
+
.executeTakeFirstOrThrow();
|
|
142
|
+
|
|
143
|
+
// 8. Create COMPLETED execution event
|
|
144
|
+
await db
|
|
145
|
+
.insertInto("WorkOrderExecutionEvent")
|
|
146
|
+
.values({
|
|
147
|
+
workOrderId: id,
|
|
148
|
+
eventType: "COMPLETED",
|
|
149
|
+
timestamp: now,
|
|
150
|
+
quantity: completedQuantity,
|
|
151
|
+
timeValue: null,
|
|
152
|
+
scrapValue: null,
|
|
153
|
+
notes: notes ?? null,
|
|
154
|
+
createdAt: now,
|
|
155
|
+
updatedAt: null,
|
|
156
|
+
})
|
|
157
|
+
.execute();
|
|
158
|
+
|
|
159
|
+
const backflushHandoff = backflushRequired
|
|
160
|
+
? {
|
|
161
|
+
productionOrderReference: workOrder.productionOrderId,
|
|
162
|
+
workOrderReference: id,
|
|
163
|
+
completedQuantity,
|
|
164
|
+
manuallyIssuedQuantity: manuallyIssuedQuantity ?? 0,
|
|
165
|
+
postingDate: receiptData?.postingDate ?? now,
|
|
166
|
+
bypassReason: zeroQuantityBypassAllowed
|
|
167
|
+
? (zeroQuantityBypassPolicy?.reasonCode ?? null)
|
|
168
|
+
: null,
|
|
169
|
+
}
|
|
170
|
+
: null;
|
|
171
|
+
|
|
172
|
+
const receiptHandoff =
|
|
173
|
+
receiptRequired && receiptData
|
|
174
|
+
? {
|
|
175
|
+
productionOrderReference: workOrder.productionOrderId,
|
|
176
|
+
workOrderReference: id,
|
|
177
|
+
itemReference: receiptData.itemReference,
|
|
178
|
+
quantity: completedQuantity,
|
|
179
|
+
unitOfMeasure: receiptData.unitOfMeasure,
|
|
180
|
+
siteReference: receiptData.siteReference,
|
|
181
|
+
postingDate: receiptData.postingDate,
|
|
182
|
+
storageLocationReference: receiptData.storageLocationReference ?? null,
|
|
183
|
+
finishedGoodLotReference: receiptData.finishedGoodLotReference ?? null,
|
|
184
|
+
serialReferences: receiptData.serialReferences ?? null,
|
|
185
|
+
}
|
|
186
|
+
: null;
|
|
187
|
+
|
|
188
|
+
// 9. Roll up to parent production order
|
|
189
|
+
const siblingWorkOrders = await db
|
|
190
|
+
.selectFrom("WorkOrder")
|
|
191
|
+
.selectAll()
|
|
192
|
+
.where("productionOrderId", "=", workOrder.productionOrderId)
|
|
193
|
+
.execute();
|
|
194
|
+
|
|
195
|
+
const allComplete = (siblingWorkOrders as { id: string; status: string }[]).every((wo) => {
|
|
196
|
+
if (wo.id === id) return true; // this one was just completed
|
|
197
|
+
return wo.status === "COMPLETE" || wo.status === "CANCELLED";
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (allComplete) {
|
|
201
|
+
await db
|
|
202
|
+
.updateTable("ProductionOrder")
|
|
203
|
+
.set({
|
|
204
|
+
status: "COMPLETED",
|
|
205
|
+
updatedAt: now,
|
|
206
|
+
})
|
|
207
|
+
.where("id", "=", workOrder.productionOrderId)
|
|
208
|
+
.execute();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return ok({ workOrder: updatedWorkOrder, backflushHandoff, receiptHandoff });
|
|
212
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./createBillOfMaterial";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const createBillOfMaterial = defineCommand(permissions.createBillOfMaterial, run);
|