@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,153 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../../testing/index";
|
|
3
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
4
|
+
import {
|
|
5
|
+
ProductionOrderNotFoundError,
|
|
6
|
+
ProductionOrderNotTechnicallyCompletableError,
|
|
7
|
+
ExecutionExceptionRemainsError,
|
|
8
|
+
PendingMaterialIssueRequestsError,
|
|
9
|
+
CostSummaryNotReadyError,
|
|
10
|
+
} from "../lib/errors.generated";
|
|
11
|
+
import {
|
|
12
|
+
baseCompletedProductionOrder,
|
|
13
|
+
baseInProgressProductionOrder,
|
|
14
|
+
baseCompleteWorkOrder,
|
|
15
|
+
baseCancelledWorkOrder,
|
|
16
|
+
baseInProgressWorkOrder,
|
|
17
|
+
basePausedWorkOrder,
|
|
18
|
+
basePendingWorkOrder,
|
|
19
|
+
baseCollectingCostSummary,
|
|
20
|
+
basePendingReviewCostSummary,
|
|
21
|
+
} from "../testing/fixtures";
|
|
22
|
+
import { run } from "./technicallyCompleteProductionOrder";
|
|
23
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
24
|
+
|
|
25
|
+
describe("technicallyCompleteProductionOrder", () => {
|
|
26
|
+
const ctx: CommandContext = {
|
|
27
|
+
actorId: "test-actor",
|
|
28
|
+
permissions: ["manufacturing:technicallyCompleteProductionOrder"],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
it("technically completes a completed order and opens variance review", async () => {
|
|
32
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
33
|
+
const techComplete = {
|
|
34
|
+
...baseCompletedProductionOrder,
|
|
35
|
+
status: "TECHNICALLY_COMPLETE" as const,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// order lookup
|
|
39
|
+
spies.select.mockReturnValueOnce(baseCompletedProductionOrder);
|
|
40
|
+
// work orders - all complete or cancelled
|
|
41
|
+
spies.select.mockReturnValueOnce([baseCompleteWorkOrder, baseCancelledWorkOrder]);
|
|
42
|
+
// cost summary in COLLECTING
|
|
43
|
+
spies.select.mockReturnValueOnce({
|
|
44
|
+
...baseCollectingCostSummary,
|
|
45
|
+
productionOrderId: baseCompletedProductionOrder.id,
|
|
46
|
+
});
|
|
47
|
+
// update cost summary
|
|
48
|
+
spies.update.mockReturnValueOnce(undefined);
|
|
49
|
+
// update order status
|
|
50
|
+
spies.update.mockReturnValue(techComplete);
|
|
51
|
+
|
|
52
|
+
const result = await run(db, { id: baseCompletedProductionOrder.id }, ctx);
|
|
53
|
+
|
|
54
|
+
expect(result.ok).toBe(true);
|
|
55
|
+
if (result.ok) {
|
|
56
|
+
expect(result.value.productionOrder.status).toBe("TECHNICALLY_COMPLETE");
|
|
57
|
+
}
|
|
58
|
+
expect(spies.update).toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns error when the order does not exist", async () => {
|
|
62
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
63
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
64
|
+
|
|
65
|
+
const result = await run(db, { id: "nonexistent" }, ctx);
|
|
66
|
+
|
|
67
|
+
expect(result.ok).toBe(false);
|
|
68
|
+
if (!result.ok) {
|
|
69
|
+
expect(result.error).toBeInstanceOf(ProductionOrderNotFoundError);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("returns error when the order is not in COMPLETED", async () => {
|
|
74
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
75
|
+
spies.select.mockReturnValueOnce(baseInProgressProductionOrder);
|
|
76
|
+
|
|
77
|
+
const result = await run(db, { id: baseInProgressProductionOrder.id }, ctx);
|
|
78
|
+
|
|
79
|
+
expect(result.ok).toBe(false);
|
|
80
|
+
if (!result.ok) {
|
|
81
|
+
expect(result.error).toBeInstanceOf(ProductionOrderNotTechnicallyCompletableError);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns error when unresolved execution exceptions remain", async () => {
|
|
86
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
87
|
+
|
|
88
|
+
// order lookup
|
|
89
|
+
spies.select.mockReturnValueOnce(baseCompletedProductionOrder);
|
|
90
|
+
// work orders - one still in progress
|
|
91
|
+
spies.select.mockReturnValueOnce([baseInProgressWorkOrder]);
|
|
92
|
+
|
|
93
|
+
const result = await run(db, { id: baseCompletedProductionOrder.id }, ctx);
|
|
94
|
+
|
|
95
|
+
expect(result.ok).toBe(false);
|
|
96
|
+
if (!result.ok) {
|
|
97
|
+
expect(result.error).toBeInstanceOf(ExecutionExceptionRemainsError);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns error when pending material issue requests or unresolved backflush intents remain", async () => {
|
|
102
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
103
|
+
|
|
104
|
+
// order lookup
|
|
105
|
+
spies.select.mockReturnValueOnce(baseCompletedProductionOrder);
|
|
106
|
+
// work orders - one still pending
|
|
107
|
+
spies.select.mockReturnValueOnce([baseCompleteWorkOrder, basePendingWorkOrder]);
|
|
108
|
+
|
|
109
|
+
const result = await run(db, { id: baseCompletedProductionOrder.id }, ctx);
|
|
110
|
+
|
|
111
|
+
expect(result.ok).toBe(false);
|
|
112
|
+
if (!result.ok) {
|
|
113
|
+
expect(result.error).toBeInstanceOf(PendingMaterialIssueRequestsError);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns error when the linked cost summary is not ready for review", async () => {
|
|
118
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
119
|
+
|
|
120
|
+
// order lookup
|
|
121
|
+
spies.select.mockReturnValueOnce(baseCompletedProductionOrder);
|
|
122
|
+
// work orders - all complete
|
|
123
|
+
spies.select.mockReturnValueOnce([baseCompleteWorkOrder]);
|
|
124
|
+
// cost summary already in PENDING_VARIANCE_REVIEW (not COLLECTING)
|
|
125
|
+
spies.select.mockReturnValueOnce({
|
|
126
|
+
...basePendingReviewCostSummary,
|
|
127
|
+
productionOrderId: baseCompletedProductionOrder.id,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const result = await run(db, { id: baseCompletedProductionOrder.id }, ctx);
|
|
131
|
+
|
|
132
|
+
expect(result.ok).toBe(false);
|
|
133
|
+
if (!result.ok) {
|
|
134
|
+
expect(result.error).toBeInstanceOf(CostSummaryNotReadyError);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("blocks further shop-floor reporting after technical completion", async () => {
|
|
139
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
140
|
+
|
|
141
|
+
// order lookup - already paused work order still open
|
|
142
|
+
spies.select.mockReturnValueOnce(baseCompletedProductionOrder);
|
|
143
|
+
// work orders - one paused (execution exception)
|
|
144
|
+
spies.select.mockReturnValueOnce([basePausedWorkOrder]);
|
|
145
|
+
|
|
146
|
+
const result = await run(db, { id: baseCompletedProductionOrder.id }, ctx);
|
|
147
|
+
|
|
148
|
+
expect(result.ok).toBe(false);
|
|
149
|
+
if (!result.ok) {
|
|
150
|
+
expect(result.error).toBeInstanceOf(ExecutionExceptionRemainsError);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
ProductionOrderNotFoundError,
|
|
4
|
+
ProductionOrderNotTechnicallyCompletableError,
|
|
5
|
+
ExecutionExceptionRemainsError,
|
|
6
|
+
PendingMaterialIssueRequestsError,
|
|
7
|
+
CostSummaryNotReadyError,
|
|
8
|
+
} from "../lib/errors.generated";
|
|
9
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
10
|
+
|
|
11
|
+
export interface TechnicallyCompleteProductionOrderInput {
|
|
12
|
+
id: string;
|
|
13
|
+
from?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Function: technicallyCompleteProductionOrder
|
|
18
|
+
*
|
|
19
|
+
* Freezes the production order after physical completion and moves the linked
|
|
20
|
+
* manufacturing cost summary into variance review. Marks the point where no
|
|
21
|
+
* more normal execution or rescheduling is expected.
|
|
22
|
+
*/
|
|
23
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
24
|
+
db: Transaction,
|
|
25
|
+
input: TechnicallyCompleteProductionOrderInput & CF,
|
|
26
|
+
_ctx: CommandContext,
|
|
27
|
+
) {
|
|
28
|
+
const { id, from, ...customFields } = input;
|
|
29
|
+
void customFields;
|
|
30
|
+
|
|
31
|
+
const allowedStatuses = from ?? ["COMPLETED"];
|
|
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
|
|
46
|
+
if (!allowedStatuses.includes(order.status)) {
|
|
47
|
+
return err(new ProductionOrderNotTechnicallyCompletableError(id));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. Check for open execution exceptions (work orders not COMPLETE or CANCELLED)
|
|
51
|
+
const workOrders = await db
|
|
52
|
+
.selectFrom("WorkOrder")
|
|
53
|
+
.selectAll()
|
|
54
|
+
.where("productionOrderId", "=", id)
|
|
55
|
+
.execute();
|
|
56
|
+
|
|
57
|
+
const hasExecutionExceptions = workOrders.some(
|
|
58
|
+
(wo) => wo.status === "IN_PROGRESS" || wo.status === "PAUSED",
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (hasExecutionExceptions) {
|
|
62
|
+
return err(new ExecutionExceptionRemainsError(id));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 4. Check for pending material issue requests (work orders still PENDING)
|
|
66
|
+
const hasPendingWork = workOrders.some((wo) => wo.status === "PENDING");
|
|
67
|
+
|
|
68
|
+
if (hasPendingWork) {
|
|
69
|
+
return err(new PendingMaterialIssueRequestsError(id));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 5. Find and validate cost summary
|
|
73
|
+
const costSummary = await db
|
|
74
|
+
.selectFrom("ManufacturingCostSummary")
|
|
75
|
+
.selectAll()
|
|
76
|
+
.where("productionOrderId", "=", id)
|
|
77
|
+
.forUpdate()
|
|
78
|
+
.executeTakeFirst();
|
|
79
|
+
|
|
80
|
+
if (costSummary?.status !== "COLLECTING") {
|
|
81
|
+
return err(new CostSummaryNotReadyError(id));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 6. Move cost summary to PENDING_VARIANCE_REVIEW
|
|
85
|
+
await db
|
|
86
|
+
.updateTable("ManufacturingCostSummary")
|
|
87
|
+
.set({
|
|
88
|
+
status: "PENDING_VARIANCE_REVIEW",
|
|
89
|
+
updatedAt: new Date(),
|
|
90
|
+
})
|
|
91
|
+
.where("id", "=", costSummary.id)
|
|
92
|
+
.execute();
|
|
93
|
+
|
|
94
|
+
// 7. Set order status to TECHNICALLY_COMPLETE
|
|
95
|
+
const techCompleteOrder = await db
|
|
96
|
+
.updateTable("ProductionOrder")
|
|
97
|
+
.set({
|
|
98
|
+
status: "TECHNICALLY_COMPLETE",
|
|
99
|
+
updatedAt: new Date(),
|
|
100
|
+
})
|
|
101
|
+
.where("id", "=", id)
|
|
102
|
+
.returningAll()
|
|
103
|
+
.executeTakeFirstOrThrow();
|
|
104
|
+
|
|
105
|
+
return ok({ productionOrder: techCompleteOrder });
|
|
106
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./unreleaseProductionOrder";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const unreleaseProductionOrder = defineCommand(permissions.unreleaseProductionOrder, run);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../../testing/index";
|
|
3
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
4
|
+
import {
|
|
5
|
+
ProductionOrderNotFoundError,
|
|
6
|
+
ProductionOrderNotUnreleasableError,
|
|
7
|
+
ExecutionAlreadyStartedError,
|
|
8
|
+
InventoryHandoffExistsError,
|
|
9
|
+
} from "../lib/errors.generated";
|
|
10
|
+
import {
|
|
11
|
+
baseDraftProductionOrder,
|
|
12
|
+
baseReleasedProductionOrder,
|
|
13
|
+
basePendingWorkOrder,
|
|
14
|
+
baseInProgressWorkOrder,
|
|
15
|
+
baseMaterialRequirement,
|
|
16
|
+
baseCollectingCostSummary,
|
|
17
|
+
} from "../testing/fixtures";
|
|
18
|
+
import { run } from "./unreleaseProductionOrder";
|
|
19
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
20
|
+
|
|
21
|
+
describe("unreleaseProductionOrder", () => {
|
|
22
|
+
const ctx: CommandContext = {
|
|
23
|
+
actorId: "test-actor",
|
|
24
|
+
permissions: ["manufacturing:unreleaseProductionOrder"],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
it("unreleases a released order with no execution evidence", async () => {
|
|
28
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
29
|
+
const draftOrder = { ...baseReleasedProductionOrder, status: "DRAFT" as const };
|
|
30
|
+
|
|
31
|
+
// order lookup
|
|
32
|
+
spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
|
|
33
|
+
// work orders - all pending
|
|
34
|
+
spies.select.mockReturnValueOnce([basePendingWorkOrder]);
|
|
35
|
+
// material requirements
|
|
36
|
+
spies.select.mockReturnValueOnce([baseMaterialRequirement]);
|
|
37
|
+
// cost summary - no actuals
|
|
38
|
+
spies.select.mockReturnValueOnce(baseCollectingCostSummary);
|
|
39
|
+
// delete operations return
|
|
40
|
+
spies.delete.mockReturnValue(undefined);
|
|
41
|
+
// update order status
|
|
42
|
+
spies.update.mockReturnValue(draftOrder);
|
|
43
|
+
|
|
44
|
+
const result = await run(db, { id: baseReleasedProductionOrder.id }, ctx);
|
|
45
|
+
|
|
46
|
+
expect(result.ok).toBe(true);
|
|
47
|
+
if (result.ok) {
|
|
48
|
+
expect(result.value.productionOrder.status).toBe("DRAFT");
|
|
49
|
+
}
|
|
50
|
+
expect(spies.update).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns error when the order does not exist", async () => {
|
|
54
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
55
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
56
|
+
|
|
57
|
+
const result = await run(db, { id: "nonexistent" }, ctx);
|
|
58
|
+
|
|
59
|
+
expect(result.ok).toBe(false);
|
|
60
|
+
if (!result.ok) {
|
|
61
|
+
expect(result.error).toBeInstanceOf(ProductionOrderNotFoundError);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns error when the order is not in RELEASED", async () => {
|
|
66
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
67
|
+
spies.select.mockReturnValueOnce(baseDraftProductionOrder);
|
|
68
|
+
|
|
69
|
+
const result = await run(db, { id: baseDraftProductionOrder.id }, ctx);
|
|
70
|
+
|
|
71
|
+
expect(result.ok).toBe(false);
|
|
72
|
+
if (!result.ok) {
|
|
73
|
+
expect(result.error).toBeInstanceOf(ProductionOrderNotUnreleasableError);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns error when a work order has already started", async () => {
|
|
78
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
79
|
+
|
|
80
|
+
// order lookup
|
|
81
|
+
spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
|
|
82
|
+
// work orders - one in progress
|
|
83
|
+
spies.select.mockReturnValueOnce([baseInProgressWorkOrder]);
|
|
84
|
+
|
|
85
|
+
const result = await run(db, { id: baseReleasedProductionOrder.id }, ctx);
|
|
86
|
+
|
|
87
|
+
expect(result.ok).toBe(false);
|
|
88
|
+
if (!result.ok) {
|
|
89
|
+
expect(result.error).toBeInstanceOf(ExecutionAlreadyStartedError);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns error when inventory handoff evidence already exists", async () => {
|
|
94
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
95
|
+
const costSummaryWithActuals = {
|
|
96
|
+
...baseCollectingCostSummary,
|
|
97
|
+
actualMaterialCost: 500,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// order lookup
|
|
101
|
+
spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
|
|
102
|
+
// work orders - all pending
|
|
103
|
+
spies.select.mockReturnValueOnce([basePendingWorkOrder]);
|
|
104
|
+
// material requirements
|
|
105
|
+
spies.select.mockReturnValueOnce([baseMaterialRequirement]);
|
|
106
|
+
// cost summary with actual costs
|
|
107
|
+
spies.select.mockReturnValueOnce(costSummaryWithActuals);
|
|
108
|
+
|
|
109
|
+
const result = await run(db, { id: baseReleasedProductionOrder.id }, ctx);
|
|
110
|
+
|
|
111
|
+
expect(result.ok).toBe(false);
|
|
112
|
+
if (!result.ok) {
|
|
113
|
+
expect(result.error).toBeInstanceOf(InventoryHandoffExistsError);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("removes work orders and material requirements when unrelease succeeds", async () => {
|
|
118
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
119
|
+
const draftOrder = { ...baseReleasedProductionOrder, status: "DRAFT" as const };
|
|
120
|
+
|
|
121
|
+
// order lookup
|
|
122
|
+
spies.select.mockReturnValueOnce(baseReleasedProductionOrder);
|
|
123
|
+
// work orders - all pending
|
|
124
|
+
spies.select.mockReturnValueOnce([basePendingWorkOrder]);
|
|
125
|
+
// material requirements
|
|
126
|
+
spies.select.mockReturnValueOnce([]);
|
|
127
|
+
// cost summary
|
|
128
|
+
spies.select.mockReturnValueOnce(baseCollectingCostSummary);
|
|
129
|
+
// delete operations
|
|
130
|
+
spies.delete.mockReturnValue(undefined);
|
|
131
|
+
// update order
|
|
132
|
+
spies.update.mockReturnValue(draftOrder);
|
|
133
|
+
|
|
134
|
+
const result = await run(db, { id: baseReleasedProductionOrder.id }, ctx);
|
|
135
|
+
|
|
136
|
+
expect(result.ok).toBe(true);
|
|
137
|
+
expect(spies.delete).toHaveBeenCalled();
|
|
138
|
+
expect(spies.update).toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
2
|
+
import {
|
|
3
|
+
ProductionOrderNotFoundError,
|
|
4
|
+
ProductionOrderNotUnreleasableError,
|
|
5
|
+
ExecutionAlreadyStartedError,
|
|
6
|
+
InventoryHandoffExistsError,
|
|
7
|
+
} from "../lib/errors.generated";
|
|
8
|
+
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";
|
|
9
|
+
|
|
10
|
+
export interface UnreleaseProductionOrderInput {
|
|
11
|
+
id: string;
|
|
12
|
+
from?: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Function: unreleaseProductionOrder
|
|
17
|
+
*
|
|
18
|
+
* Returns a released order to DRAFT when execution has not meaningfully
|
|
19
|
+
* started. Removes release artifacts so the planner can safely revise
|
|
20
|
+
* the order.
|
|
21
|
+
*/
|
|
22
|
+
export async function run<CF extends Record<string, unknown>>(
|
|
23
|
+
db: Transaction,
|
|
24
|
+
input: UnreleaseProductionOrderInput & CF,
|
|
25
|
+
_ctx: CommandContext,
|
|
26
|
+
) {
|
|
27
|
+
const { id, from, ...customFields } = input;
|
|
28
|
+
void customFields;
|
|
29
|
+
|
|
30
|
+
const allowedStatuses = from ?? ["RELEASED"];
|
|
31
|
+
|
|
32
|
+
// 1. Fetch production order with lock
|
|
33
|
+
const order = await db
|
|
34
|
+
.selectFrom("ProductionOrder")
|
|
35
|
+
.selectAll()
|
|
36
|
+
.where("id", "=", id)
|
|
37
|
+
.forUpdate()
|
|
38
|
+
.executeTakeFirst();
|
|
39
|
+
|
|
40
|
+
if (!order) {
|
|
41
|
+
return err(new ProductionOrderNotFoundError(id));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 2. Validate status is unreleasable
|
|
45
|
+
if (!allowedStatuses.includes(order.status)) {
|
|
46
|
+
return err(new ProductionOrderNotUnreleasableError(id));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 3. Check no work order has execution evidence
|
|
50
|
+
const workOrders = await db
|
|
51
|
+
.selectFrom("WorkOrder")
|
|
52
|
+
.selectAll()
|
|
53
|
+
.where("productionOrderId", "=", id)
|
|
54
|
+
.execute();
|
|
55
|
+
|
|
56
|
+
const hasExecution = workOrders.some(
|
|
57
|
+
(wo) =>
|
|
58
|
+
wo.status === "IN_PROGRESS" ||
|
|
59
|
+
wo.status === "PAUSED" ||
|
|
60
|
+
wo.status === "COMPLETE" ||
|
|
61
|
+
wo.completedQuantity > 0 ||
|
|
62
|
+
wo.actualStartDate != null,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (hasExecution) {
|
|
66
|
+
return err(new ExecutionAlreadyStartedError(id));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 4. Check no irreversible inventory handoff
|
|
70
|
+
const _materialRequirements = await db
|
|
71
|
+
.selectFrom("ProductionOrderMaterialRequirement")
|
|
72
|
+
.selectAll()
|
|
73
|
+
.where("productionOrderId", "=", id)
|
|
74
|
+
.execute();
|
|
75
|
+
|
|
76
|
+
// Check if any cost summary has actual costs recorded (inventory handoff evidence)
|
|
77
|
+
const costSummary = await db
|
|
78
|
+
.selectFrom("ManufacturingCostSummary")
|
|
79
|
+
.selectAll()
|
|
80
|
+
.where("productionOrderId", "=", id)
|
|
81
|
+
.executeTakeFirst();
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
costSummary &&
|
|
85
|
+
(costSummary.actualMaterialCost > 0 ||
|
|
86
|
+
costSummary.actualLaborCost > 0 ||
|
|
87
|
+
costSummary.actualMachineCost > 0 ||
|
|
88
|
+
costSummary.actualOverheadCost > 0)
|
|
89
|
+
) {
|
|
90
|
+
return err(new InventoryHandoffExistsError(id));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 5. Remove release artifacts - delete work orders
|
|
94
|
+
await db.deleteFrom("WorkOrder").where("productionOrderId", "=", id).execute();
|
|
95
|
+
|
|
96
|
+
// 6. Remove material requirements
|
|
97
|
+
await db
|
|
98
|
+
.deleteFrom("ProductionOrderMaterialRequirement")
|
|
99
|
+
.where("productionOrderId", "=", id)
|
|
100
|
+
.execute();
|
|
101
|
+
|
|
102
|
+
// 7. Remove BOM snapshot
|
|
103
|
+
await db.deleteFrom("ProductionOrderBomSnapshot").where("productionOrderId", "=", id).execute();
|
|
104
|
+
|
|
105
|
+
// 8. Remove routing snapshot
|
|
106
|
+
await db
|
|
107
|
+
.deleteFrom("ProductionOrderRoutingSnapshot")
|
|
108
|
+
.where("productionOrderId", "=", id)
|
|
109
|
+
.execute();
|
|
110
|
+
|
|
111
|
+
// 9. Remove cost baseline
|
|
112
|
+
await db.deleteFrom("ProductionOrderCostBaseline").where("productionOrderId", "=", id).execute();
|
|
113
|
+
|
|
114
|
+
// 10. Remove cost summary
|
|
115
|
+
await db.deleteFrom("ManufacturingCostSummary").where("productionOrderId", "=", id).execute();
|
|
116
|
+
|
|
117
|
+
// 11. Set status to DRAFT
|
|
118
|
+
const draftOrder = await db
|
|
119
|
+
.updateTable("ProductionOrder")
|
|
120
|
+
.set({
|
|
121
|
+
selectedBomVersionId: null,
|
|
122
|
+
selectedRoutingRevisionId: null,
|
|
123
|
+
status: "DRAFT",
|
|
124
|
+
updatedAt: new Date(),
|
|
125
|
+
})
|
|
126
|
+
.where("id", "=", id)
|
|
127
|
+
.returningAll()
|
|
128
|
+
.executeTakeFirstOrThrow();
|
|
129
|
+
|
|
130
|
+
return ok({ productionOrder: draftOrder });
|
|
131
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @generated — do not edit
|
|
2
|
+
import { permissions } from "../lib/permissions.generated";
|
|
3
|
+
import { run } from "./updateBillOfMaterial";
|
|
4
|
+
import { defineCommand } from "@tailor-platform/erp-kit/module";
|
|
5
|
+
|
|
6
|
+
export const updateBillOfMaterial = defineCommand(permissions.updateBillOfMaterial, run);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../../testing/index";
|
|
3
|
+
import type { Transaction } from "../generated/kysely-tailordb";
|
|
4
|
+
import {
|
|
5
|
+
BomNotFoundError,
|
|
6
|
+
BomNotMutableError,
|
|
7
|
+
InvalidComponentQuantityError,
|
|
8
|
+
ComponentItemInactiveError,
|
|
9
|
+
AmbiguousEffectivityRuleError,
|
|
10
|
+
} from "../lib/errors.generated";
|
|
11
|
+
import { baseDraftBom, baseActiveBom, baseBomLine } from "../testing/fixtures";
|
|
12
|
+
import { run } from "./updateBillOfMaterial";
|
|
13
|
+
import type { CommandContext } from "@tailor-platform/erp-kit/module";
|
|
14
|
+
|
|
15
|
+
describe("updateBillOfMaterial", () => {
|
|
16
|
+
const ctx: CommandContext = {
|
|
17
|
+
actorId: "test-actor",
|
|
18
|
+
permissions: ["manufacturing:updateBillOfMaterial"],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const validInput = {
|
|
22
|
+
id: "bom-1",
|
|
23
|
+
bomType: "PHANTOM" as const,
|
|
24
|
+
lines: [
|
|
25
|
+
{
|
|
26
|
+
itemId: "item-2",
|
|
27
|
+
requiredQuantity: 3.0,
|
|
28
|
+
unitOfMeasure: "EA",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
it("updates draft BOM metadata and component lines", async () => {
|
|
34
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
35
|
+
const updatedBom = {
|
|
36
|
+
...baseDraftBom,
|
|
37
|
+
bomType: "PHANTOM" as const,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// BOM exists and is DRAFT
|
|
41
|
+
spies.select.mockReturnValueOnce(baseDraftBom);
|
|
42
|
+
// component item exists
|
|
43
|
+
spies.select.mockReturnValueOnce({ id: "item-2" });
|
|
44
|
+
// update BOM header
|
|
45
|
+
spies.update.mockReturnValueOnce(updatedBom);
|
|
46
|
+
// delete old lines
|
|
47
|
+
spies.delete.mockReturnValueOnce(undefined);
|
|
48
|
+
// insert new line
|
|
49
|
+
spies.insert.mockReturnValueOnce({ ...baseBomLine, requiredQuantity: 3.0 });
|
|
50
|
+
|
|
51
|
+
const result = await run(db, validInput, ctx);
|
|
52
|
+
|
|
53
|
+
expect(result.ok).toBe(true);
|
|
54
|
+
if (result.ok) {
|
|
55
|
+
expect(result.value.billOfMaterial.bomType).toBe("PHANTOM");
|
|
56
|
+
}
|
|
57
|
+
expect(spies.update).toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns error when the BOM does not exist", async () => {
|
|
61
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
62
|
+
|
|
63
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
64
|
+
|
|
65
|
+
const result = await run(db, validInput, ctx);
|
|
66
|
+
|
|
67
|
+
expect(result.ok).toBe(false);
|
|
68
|
+
if (!result.ok) {
|
|
69
|
+
expect(result.error).toBeInstanceOf(BomNotFoundError);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("returns error when the BOM is not in DRAFT", async () => {
|
|
74
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
75
|
+
|
|
76
|
+
spies.select.mockReturnValueOnce(baseActiveBom);
|
|
77
|
+
|
|
78
|
+
const result = await run(db, { ...validInput, id: "bom-2" }, ctx);
|
|
79
|
+
|
|
80
|
+
expect(result.ok).toBe(false);
|
|
81
|
+
if (!result.ok) {
|
|
82
|
+
expect(result.error).toBeInstanceOf(BomNotMutableError);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns error when a revised component quantity is invalid", async () => {
|
|
87
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
88
|
+
|
|
89
|
+
spies.select.mockReturnValueOnce(baseDraftBom);
|
|
90
|
+
|
|
91
|
+
const result = await run(
|
|
92
|
+
db,
|
|
93
|
+
{
|
|
94
|
+
...validInput,
|
|
95
|
+
lines: [{ itemId: "item-2", requiredQuantity: -1, unitOfMeasure: "EA" }],
|
|
96
|
+
},
|
|
97
|
+
ctx,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(result.ok).toBe(false);
|
|
101
|
+
if (!result.ok) {
|
|
102
|
+
expect(result.error).toBeInstanceOf(InvalidComponentQuantityError);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns error when a newly referenced component is inactive", async () => {
|
|
107
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
108
|
+
|
|
109
|
+
// BOM exists and is DRAFT
|
|
110
|
+
spies.select.mockReturnValueOnce(baseDraftBom);
|
|
111
|
+
// component item not found (inactive)
|
|
112
|
+
spies.select.mockReturnValueOnce(undefined);
|
|
113
|
+
|
|
114
|
+
const result = await run(db, validInput, ctx);
|
|
115
|
+
|
|
116
|
+
expect(result.ok).toBe(false);
|
|
117
|
+
if (!result.ok) {
|
|
118
|
+
expect(result.error).toBeInstanceOf(ComponentItemInactiveError);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("returns error when effectivity changes would create ambiguous selection", async () => {
|
|
123
|
+
const { db, spies } = createMockDb<Transaction>();
|
|
124
|
+
|
|
125
|
+
// BOM exists and is DRAFT
|
|
126
|
+
spies.select.mockReturnValueOnce(baseDraftBom);
|
|
127
|
+
// conflicting active BOM with same default selection
|
|
128
|
+
spies.select.mockReturnValueOnce({
|
|
129
|
+
...baseActiveBom,
|
|
130
|
+
defaultSelection: true,
|
|
131
|
+
effectivityStartDate: null,
|
|
132
|
+
effectivityEndDate: null,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const result = await run(
|
|
136
|
+
db,
|
|
137
|
+
{
|
|
138
|
+
id: "bom-1",
|
|
139
|
+
defaultSelection: true,
|
|
140
|
+
},
|
|
141
|
+
ctx,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
expect(result.ok).toBe(false);
|
|
145
|
+
if (!result.ok) {
|
|
146
|
+
expect(result.error).toBeInstanceOf(AmbiguousEffectivityRuleError);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|