@happyvertical/smrt-manufacturing 0.53.4 → 0.53.6
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/AGENTS.md +58 -7
- package/README.md +228 -4
- package/agents/multilevel-and-runs.md +28 -0
- package/dist/collections/AssemblyCollection.d.ts +15 -0
- package/dist/collections/AssemblyCollection.d.ts.map +1 -0
- package/dist/collections/OperationCollection.d.ts +20 -0
- package/dist/collections/OperationCollection.d.ts.map +1 -0
- package/dist/collections/ProductionRunCollection.d.ts +15 -0
- package/dist/collections/ProductionRunCollection.d.ts.map +1 -0
- package/dist/collections/ProductionRunCompletionCollection.d.ts +14 -0
- package/dist/collections/ProductionRunCompletionCollection.d.ts.map +1 -0
- package/dist/collections/RoutingStepCollection.d.ts +15 -0
- package/dist/collections/RoutingStepCollection.d.ts.map +1 -0
- package/dist/collections/index.d.ts +5 -0
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/index.d.ts +23 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1660 -42
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1910 -161
- package/dist/models/Assembly.d.ts +62 -0
- package/dist/models/Assembly.d.ts.map +1 -0
- package/dist/models/BillOfMaterials.d.ts +12 -4
- package/dist/models/BillOfMaterials.d.ts.map +1 -1
- package/dist/models/BomLine.d.ts +11 -5
- package/dist/models/BomLine.d.ts.map +1 -1
- package/dist/models/Operation.d.ts +59 -0
- package/dist/models/Operation.d.ts.map +1 -0
- package/dist/models/ProductionRun.d.ts +67 -0
- package/dist/models/ProductionRun.d.ts.map +1 -0
- package/dist/models/ProductionRunCompletion.d.ts +36 -0
- package/dist/models/ProductionRunCompletion.d.ts.map +1 -0
- package/dist/models/RoutingStep.d.ts +46 -0
- package/dist/models/RoutingStep.d.ts.map +1 -0
- package/dist/models/index.d.ts +5 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/operation-types.d.ts +108 -0
- package/dist/operation-types.d.ts.map +1 -0
- package/dist/production-run-types.d.ts +100 -0
- package/dist/production-run-types.d.ts.map +1 -0
- package/dist/quantity.d.ts +36 -0
- package/dist/quantity.d.ts.map +1 -0
- package/dist/services/AssemblyService.d.ts +222 -0
- package/dist/services/AssemblyService.d.ts.map +1 -0
- package/dist/services/BomService.d.ts +108 -5
- package/dist/services/BomService.d.ts.map +1 -1
- package/dist/services/OperationService.d.ts +48 -0
- package/dist/services/OperationService.d.ts.map +1 -0
- package/dist/services/ProductionRunService.d.ts +123 -0
- package/dist/services/ProductionRunService.d.ts.map +1 -0
- package/dist/services/ProductionService.d.ts +11 -0
- package/dist/services/ProductionService.d.ts.map +1 -1
- package/dist/services/RoutingService.d.ts +50 -0
- package/dist/services/RoutingService.d.ts.map +1 -0
- package/dist/services/index.d.ts +5 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/structure-walk.d.ts +77 -0
- package/dist/services/structure-walk.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +1771 -207
- package/dist/svelte/components/AssemblyForm.svelte +69 -0
- package/dist/svelte/components/AssemblyForm.svelte.d.ts +38 -0
- package/dist/svelte/components/AssemblyForm.svelte.d.ts.map +1 -0
- package/dist/svelte/components/AssemblyList.svelte +229 -0
- package/dist/svelte/components/AssemblyList.svelte.d.ts +28 -0
- package/dist/svelte/components/AssemblyList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/BomEditor.svelte +575 -0
- package/dist/svelte/components/BomEditor.svelte.d.ts +29 -0
- package/dist/svelte/components/BomEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/BomStructureTree.svelte +164 -0
- package/dist/svelte/components/BomStructureTree.svelte.d.ts +16 -0
- package/dist/svelte/components/BomStructureTree.svelte.d.ts.map +1 -0
- package/dist/svelte/components/OperationForm.svelte +258 -0
- package/dist/svelte/components/OperationForm.svelte.d.ts +17 -0
- package/dist/svelte/components/OperationForm.svelte.d.ts.map +1 -0
- package/dist/svelte/components/OperationList.svelte +170 -0
- package/dist/svelte/components/OperationList.svelte.d.ts +17 -0
- package/dist/svelte/components/OperationList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ProductionRunList.svelte +213 -0
- package/dist/svelte/components/ProductionRunList.svelte.d.ts +18 -0
- package/dist/svelte/components/ProductionRunList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/RequirementsTree.svelte +125 -0
- package/dist/svelte/components/RequirementsTree.svelte.d.ts +13 -0
- package/dist/svelte/components/RequirementsTree.svelte.d.ts.map +1 -0
- package/dist/svelte/components/RequirementsTreeLevel.svelte +120 -0
- package/dist/svelte/components/RequirementsTreeLevel.svelte.d.ts +12 -0
- package/dist/svelte/components/RequirementsTreeLevel.svelte.d.ts.map +1 -0
- package/dist/svelte/i18n.d.ts +119 -0
- package/dist/svelte/i18n.d.ts.map +1 -0
- package/dist/svelte/i18n.js +119 -0
- package/dist/svelte/index.d.ts +33 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +34 -0
- package/dist/svelte/types.d.ts +257 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +249 -0
- package/dist/tenant-scope.d.ts +25 -0
- package/dist/tenant-scope.d.ts.map +1 -0
- package/dist/types.d.ts +207 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +13 -1
- package/dist/types.js.map +1 -1
- package/dist/ui.d.ts +17 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +94 -0
- package/dist/ui.js.map +1 -0
- package/package.json +41 -11
package/dist/index.js
CHANGED
|
@@ -1,18 +1,134 @@
|
|
|
1
|
-
import { BomNotFoundError, NoActiveBomForProductError } from "./types.js";
|
|
2
|
-
import { ObjectRegistry, SmrtCollection, SmrtObject, field, smrt } from "@happyvertical/smrt-core";
|
|
3
|
-
import {
|
|
1
|
+
import { BomExplosionLimitError, BomNotFoundError, MAX_EXPLOSION_DEPTH, MAX_EXPLOSION_LINES, NoActiveBomForProductError } from "./types.js";
|
|
2
|
+
import { ObjectRegistry, SmrtCollection, SmrtObject, ValidationError, crossPackageRef, field, foreignKey, resolveDatabase, smrt } from "@happyvertical/smrt-core";
|
|
3
|
+
import { ProductCollection, SkuCollection } from "@happyvertical/smrt-products/collections";
|
|
4
|
+
import { Material, Product, ProductType } from "@happyvertical/smrt-products/models";
|
|
5
|
+
import { TenantScoped, getCurrentTenant, tenantId, withTenant, withTenantGlobalRead } from "@happyvertical/smrt-tenancy";
|
|
4
6
|
import { createStockService } from "@happyvertical/smrt-inventory";
|
|
5
7
|
import { createLogger } from "@happyvertical/logger";
|
|
8
|
+
//#region \0rolldown/runtime.js
|
|
9
|
+
var __defProp$7 = Object.defineProperty;
|
|
10
|
+
var __exportAll = (all, no_symbols) => {
|
|
11
|
+
let target = {};
|
|
12
|
+
for (var name in all) __defProp$7(target, name, {
|
|
13
|
+
get: all[name],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
if (!no_symbols) __defProp$7(target, Symbol.toStringTag, { value: "Module" });
|
|
17
|
+
return target;
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
6
20
|
//#region src/__smrt-register__.ts
|
|
7
|
-
ObjectRegistry.registerPackageManifest(JSON.parse("{\"version\":\"1.0.0\",\"timestamp\":0,\"packageName\":\"@happyvertical/smrt-manufacturing\",\"packageVersion\":\"0.53.4\",\"objects\":{\"@happyvertical/smrt-manufacturing:BillOfMaterialsCollection\":{\"name\":\"billofmaterialscollection\",\"className\":\"BillOfMaterialsCollection\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:BillOfMaterialsCollection\",\"collection\":\"billofmaterialses\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/collections/BillOfMaterialsCollection.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{},\"methods\":{\"findByProduct\":{\"name\":\"findByProduct\",\"async\":true,\"parameters\":[{\"name\":\"productId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<BillOfMaterials[]>\",\"isStatic\":false,\"isPublic\":true},\"findActiveForProduct\":{\"name\":\"findActiveForProduct\",\"async\":true,\"parameters\":[{\"name\":\"productId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<BillOfMaterials | null>\",\"isStatic\":false,\"isPublic\":true},\"findByStatus\":{\"name\":\"findByStatus\",\"async\":true,\"parameters\":[{\"name\":\"status\",\"type\":\"BomStatus\",\"optional\":false}],\"returnType\":\"Promise<BillOfMaterials[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_boms\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"BillOfMaterials\",\"exportName\":\"BillOfMaterialsCollection\",\"collectionExportName\":\"BillOfMaterialsCollectionCollection\",\"schema\":{\"tableName\":\"manufacturing_boms\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_boms\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"product_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"version\\\" INTEGER NOT NULL DEFAULT 1,\\n \\\"effective_date\\\" TIMESTAMP,\\n \\\"status\\\" TEXT NOT NULL DEFAULT 'draft',\\n \\\"notes\\\" TEXT DEFAULT '',\\n \\\"currency\\\" TEXT DEFAULT 'USD'\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"product_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"version\":{\"type\":\"INTEGER\",\"notNull\":true,\"unique\":false,\"default\":1},\"effective_date\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"draft\"},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"USD\"}},\"indexes\":[{\"name\":\"manufacturing_boms_product_id_version_idx\",\"columns\":[\"product_id\",\"version\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_boms_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_boms_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"b2691adc\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:BomLineCollection\":{\"name\":\"bomlinecollection\",\"className\":\"BomLineCollection\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:BomLineCollection\",\"collection\":\"bomlines\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/collections/BomLineCollection.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{},\"methods\":{\"findByBom\":{\"name\":\"findByBom\",\"async\":true,\"parameters\":[{\"name\":\"bomId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<BomLine[]>\",\"isStatic\":false,\"isPublic\":true},\"findByComponent\":{\"name\":\"findByComponent\",\"async\":true,\"parameters\":[{\"name\":\"componentSkuId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<BomLine[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_bom_lines\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"BomLine\",\"exportName\":\"BomLineCollection\",\"collectionExportName\":\"BomLineCollectionCollection\",\"schema\":{\"tableName\":\"manufacturing_bom_lines\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_bom_lines\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"bom_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"component_sku_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"qty_per_unit\\\" REAL,\\n \\\"uom\\\" TEXT DEFAULT 'each',\\n \\\"waste_percent\\\" REAL,\\n \\\"notes\\\" TEXT DEFAULT ''\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"bom_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"component_sku_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"qty_per_unit\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"uom\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"each\"},\"waste_percent\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"manufacturing_bom_lines_bom_id_component_sku_id_idx\",\"columns\":[\"bom_id\",\"component_sku_id\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_bom_lines_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_bom_lines_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"cfbae7e8\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:BillOfMaterials\":{\"name\":\"billofmaterials\",\"className\":\"BillOfMaterials\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:BillOfMaterials\",\"collection\":\"billofmaterialses\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/BillOfMaterials.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"productId\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"version\":{\"type\":\"integer\",\"required\":true,\"default\":1,\"_meta\":{\"required\":true}},\"effectiveDate\":{\"type\":\"datetime\",\"required\":false},\"status\":{\"type\":\"text\",\"required\":true,\"default\":\"draft\",\"_meta\":{\"required\":true}},\"notes\":{\"type\":\"text\",\"required\":false,\"default\":\"\"},\"currency\":{\"type\":\"text\",\"required\":false,\"default\":\"USD\"}},\"methods\":{},\"decoratorConfig\":{\"tableName\":\"manufacturing_boms\",\"conflictColumns\":[\"product_id\",\"version\",\"tenant_id\"],\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":true,\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"BillOfMaterials\",\"collectionExportName\":\"BillOfMaterialsCollection\",\"validationRules\":[{\"field\":\"productId\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"version\",\"rule\":\"required\",\"fieldType\":\"integer\"},{\"field\":\"status\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"manufacturing_boms\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_boms\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"product_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"version\\\" INTEGER NOT NULL DEFAULT 1,\\n \\\"effective_date\\\" TIMESTAMP,\\n \\\"status\\\" TEXT NOT NULL DEFAULT 'draft',\\n \\\"notes\\\" TEXT DEFAULT '',\\n \\\"currency\\\" TEXT DEFAULT 'USD'\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"product_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"version\":{\"type\":\"INTEGER\",\"notNull\":true,\"unique\":false,\"default\":1},\"effective_date\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"draft\"},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"USD\"}},\"indexes\":[{\"name\":\"manufacturing_boms_product_id_version_idx\",\"columns\":[\"product_id\",\"version\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_boms_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_boms_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"b2691adc\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:BomLine\":{\"name\":\"bomline\",\"className\":\"BomLine\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:BomLine\",\"collection\":\"bomlines\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/BomLine.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"bomId\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"componentSkuId\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"qtyPerUnit\":{\"type\":\"decimal\",\"required\":false,\"_meta\":{}},\"uom\":{\"type\":\"text\",\"required\":false,\"default\":\"each\"},\"wastePercent\":{\"type\":\"decimal\",\"required\":false,\"_meta\":{}},\"notes\":{\"type\":\"text\",\"required\":false,\"default\":\"\"}},\"methods\":{\"effectiveQtyPerUnit\":{\"name\":\"effectiveQtyPerUnit\",\"async\":false,\"parameters\":[],\"returnType\":\"number\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_bom_lines\",\"conflictColumns\":[\"bom_id\",\"component_sku_id\",\"tenant_id\"],\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"skipApiCheck\":true},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"BomLine\",\"collectionExportName\":\"BomLineCollection\",\"validationRules\":[{\"field\":\"bomId\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"componentSkuId\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"manufacturing_bom_lines\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_bom_lines\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"bom_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"component_sku_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"qty_per_unit\\\" REAL,\\n \\\"uom\\\" TEXT DEFAULT 'each',\\n \\\"waste_percent\\\" REAL,\\n \\\"notes\\\" TEXT DEFAULT ''\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"bom_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"component_sku_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"qty_per_unit\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"uom\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"each\"},\"waste_percent\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"manufacturing_bom_lines_bom_id_component_sku_id_idx\",\"columns\":[\"bom_id\",\"component_sku_id\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_bom_lines_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_bom_lines_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"cfbae7e8\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"}},\"moduleType\":\"smrt\",\"smrtDependencies\":[\"@happyvertical/smrt-core\",\"@happyvertical/smrt-inventory\",\"@happyvertical/smrt-tenancy\"]}"));
|
|
21
|
+
ObjectRegistry.registerPackageManifest(JSON.parse("{\"version\":\"1.0.0\",\"timestamp\":0,\"packageName\":\"@happyvertical/smrt-manufacturing\",\"packageVersion\":\"0.53.6\",\"objects\":{\"@happyvertical/smrt-manufacturing:BillOfMaterialsCollection\":{\"name\":\"billofmaterialscollection\",\"className\":\"BillOfMaterialsCollection\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:BillOfMaterialsCollection\",\"collection\":\"billofmaterialses\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/collections/BillOfMaterialsCollection.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{},\"methods\":{\"findByProduct\":{\"name\":\"findByProduct\",\"async\":true,\"parameters\":[{\"name\":\"productId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<BillOfMaterials[]>\",\"isStatic\":false,\"isPublic\":true},\"findActiveForProduct\":{\"name\":\"findActiveForProduct\",\"async\":true,\"parameters\":[{\"name\":\"productId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<BillOfMaterials | null>\",\"isStatic\":false,\"isPublic\":true},\"findByStatus\":{\"name\":\"findByStatus\",\"async\":true,\"parameters\":[{\"name\":\"status\",\"type\":\"BomStatus\",\"optional\":false}],\"returnType\":\"Promise<BillOfMaterials[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_boms\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"BillOfMaterials\",\"exportName\":\"BillOfMaterialsCollection\",\"collectionExportName\":\"BillOfMaterialsCollectionCollection\",\"schema\":{\"tableName\":\"manufacturing_boms\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_boms\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"product_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"version\\\" INTEGER NOT NULL DEFAULT 1,\\n \\\"effective_date\\\" TIMESTAMP,\\n \\\"status\\\" TEXT NOT NULL DEFAULT 'draft',\\n \\\"notes\\\" TEXT DEFAULT '',\\n \\\"currency\\\" TEXT DEFAULT 'USD'\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"product_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"version\":{\"type\":\"INTEGER\",\"notNull\":true,\"unique\":false,\"default\":1},\"effective_date\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"draft\"},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"USD\"}},\"indexes\":[{\"name\":\"manufacturing_boms_product_id_version_idx\",\"columns\":[\"product_id\",\"version\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_boms_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_boms_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"b2691adc\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:BomLineCollection\":{\"name\":\"bomlinecollection\",\"className\":\"BomLineCollection\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:BomLineCollection\",\"collection\":\"bomlines\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/collections/BomLineCollection.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{},\"methods\":{\"findByBom\":{\"name\":\"findByBom\",\"async\":true,\"parameters\":[{\"name\":\"bomId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<BomLine[]>\",\"isStatic\":false,\"isPublic\":true},\"findByComponent\":{\"name\":\"findByComponent\",\"async\":true,\"parameters\":[{\"name\":\"componentSkuId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<BomLine[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_bom_lines\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"BomLine\",\"exportName\":\"BomLineCollection\",\"collectionExportName\":\"BomLineCollectionCollection\",\"schema\":{\"tableName\":\"manufacturing_bom_lines\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_bom_lines\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"bom_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"component_sku_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"qty_per_unit\\\" REAL,\\n \\\"uom\\\" TEXT DEFAULT 'each',\\n \\\"waste_percent\\\" REAL,\\n \\\"notes\\\" TEXT DEFAULT ''\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"bom_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"component_sku_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"qty_per_unit\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"uom\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"each\"},\"waste_percent\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"manufacturing_bom_lines_bom_id_component_sku_id_idx\",\"columns\":[\"bom_id\",\"component_sku_id\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_bom_lines_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_bom_lines_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"cfbae7e8\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:OperationCollection\":{\"name\":\"operationcollection\",\"className\":\"OperationCollection\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:OperationCollection\",\"collection\":\"operations\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/collections/OperationCollection.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{},\"methods\":{\"findByCode\":{\"name\":\"findByCode\",\"async\":true,\"parameters\":[{\"name\":\"code\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<Operation | null>\",\"isStatic\":false,\"isPublic\":true},\"listOperations\":{\"name\":\"listOperations\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true,\"memberTypes\":[\"boolean\"]}],\"returnType\":\"Promise<Operation[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_operations\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"Operation\",\"exportName\":\"OperationCollection\",\"collectionExportName\":\"OperationCollectionCollection\",\"schema\":{\"tableName\":\"manufacturing_operations\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_operations\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"code\\\" TEXT NOT NULL DEFAULT '',\\n \\\"name\\\" TEXT NOT NULL DEFAULT '',\\n \\\"category\\\" TEXT DEFAULT '',\\n \\\"is_active\\\" BOOLEAN DEFAULT TRUE,\\n \\\"required_qualification_id\\\" UUID\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"code\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"name\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"category\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"is_active\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false,\"default\":true},\"required_qualification_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"manufacturing_operations_code_tenant_id_idx\",\"columns\":[\"code\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_operations_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_operations_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"manufacturing_operations_required_qualification_id_idx\",\"columns\":[\"required_qualification_id\"]}],\"version\":\"0b3134d6\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:ProductionRunCollection\":{\"name\":\"productionruncollection\",\"className\":\"ProductionRunCollection\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:ProductionRunCollection\",\"collection\":\"productionruns\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/collections/ProductionRunCollection.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{},\"methods\":{\"findByBom\":{\"name\":\"findByBom\",\"async\":true,\"parameters\":[{\"name\":\"bomId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<ProductionRun[]>\",\"isStatic\":false,\"isPublic\":true},\"findByStatus\":{\"name\":\"findByStatus\",\"async\":true,\"parameters\":[{\"name\":\"status\",\"type\":\"ProductionRunStatus\",\"optional\":false}],\"returnType\":\"Promise<ProductionRun[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_production_runs\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"ProductionRun\",\"exportName\":\"ProductionRunCollection\",\"collectionExportName\":\"ProductionRunCollectionCollection\",\"schema\":{\"tableName\":\"manufacturing_production_runs\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_production_runs\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"bom_id\\\" UUID NOT NULL,\\n \\\"target_qty\\\" REAL,\\n \\\"completed_qty\\\" REAL,\\n \\\"status\\\" TEXT NOT NULL DEFAULT 'planned'\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"bom_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"manufacturing_boms\",\"column\":\"id\",\"onDelete\":\"RESTRICT\",\"onUpdate\":\"CASCADE\"}},\"target_qty\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"completed_qty\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"planned\"}},\"indexes\":[{\"name\":\"manufacturing_production_runs_slug_context_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_production_runs_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"manufacturing_production_runs_bom_id_idx\",\"columns\":[\"bom_id\"]}],\"version\":\"2ca8147d\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:ProductionRunCompletionCollection\":{\"name\":\"productionruncompletioncollection\",\"className\":\"ProductionRunCompletionCollection\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:ProductionRunCompletionCollection\",\"collection\":\"productionruncompletions\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/collections/ProductionRunCompletionCollection.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{},\"methods\":{\"findByRun\":{\"name\":\"findByRun\",\"async\":true,\"parameters\":[{\"name\":\"runId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<ProductionRunCompletion[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_production_run_completions\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"ProductionRunCompletion\",\"exportName\":\"ProductionRunCompletionCollection\",\"collectionExportName\":\"ProductionRunCompletionCollectionCollection\",\"schema\":{\"tableName\":\"manufacturing_production_run_completions\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_production_run_completions\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"run_id\\\" UUID NOT NULL,\\n \\\"qty\\\" REAL,\\n \\\"completed_at\\\" TIMESTAMP\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"run_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"manufacturing_production_runs\",\"column\":\"id\",\"onDelete\":\"CASCADE\",\"onUpdate\":\"CASCADE\"}},\"qty\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"completed_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"manufacturing_production_run_completions_slug_context_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_production_run_completions_tenant_ea7a0d0125_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"manufacturing_production_run_completions_run_id_idx\",\"columns\":[\"run_id\"]}],\"version\":\"c940de6c\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:RoutingStepCollection\":{\"name\":\"routingstepcollection\",\"className\":\"RoutingStepCollection\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:RoutingStepCollection\",\"collection\":\"routingsteps\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/collections/RoutingStepCollection.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{},\"methods\":{\"findByBom\":{\"name\":\"findByBom\",\"async\":true,\"parameters\":[{\"name\":\"bomId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<RoutingStep[]>\",\"isStatic\":false,\"isPublic\":true},\"findByOperation\":{\"name\":\"findByOperation\",\"async\":true,\"parameters\":[{\"name\":\"operationId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<RoutingStep[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_routing_steps\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"RoutingStep\",\"exportName\":\"RoutingStepCollection\",\"collectionExportName\":\"RoutingStepCollectionCollection\",\"schema\":{\"tableName\":\"manufacturing_routing_steps\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_routing_steps\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"bom_id\\\" UUID NOT NULL,\\n \\\"operation_id\\\" UUID NOT NULL,\\n \\\"sequence\\\" INTEGER NOT NULL DEFAULT 1,\\n \\\"estimated_minutes\\\" REAL,\\n \\\"notes\\\" TEXT DEFAULT ''\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"bom_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"manufacturing_boms\",\"column\":\"id\",\"onDelete\":\"CASCADE\",\"onUpdate\":\"CASCADE\"}},\"operation_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"manufacturing_operations\",\"column\":\"id\",\"onDelete\":\"RESTRICT\",\"onUpdate\":\"CASCADE\"}},\"sequence\":{\"type\":\"INTEGER\",\"notNull\":true,\"unique\":false,\"default\":1},\"estimated_minutes\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"manufacturing_routing_steps_bom_id_sequence_idx\",\"columns\":[\"bom_id\",\"sequence\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_routing_steps_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_routing_steps_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"manufacturing_routing_steps_operation_id_idx\",\"columns\":[\"operation_id\"]}],\"version\":\"41ac3c66\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:Assembly\":{\"name\":\"assembly\",\"className\":\"Assembly\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:Assembly\",\"collection\":\"products\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/Assembly.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"created_at\":{\"type\":\"datetime\",\"required\":false},\"updated_at\":{\"type\":\"datetime\",\"required\":false},\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"generated\":true,\"source\":\"tenantScoped_decorator\",\"sqlType\":\"UUID\",\"__tenancy\":{\"isTenantIdField\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"autoFilter\":true,\"autoPopulate\":true,\"allowSuperAdminBypass\":false}}},\"productType\":{\"type\":\"json\",\"required\":false},\"name\":{\"type\":\"text\",\"required\":false},\"description\":{\"type\":\"text\",\"required\":false},\"category\":{\"type\":\"text\",\"required\":false},\"manufacturer\":{\"type\":\"text\",\"required\":false},\"model\":{\"type\":\"text\",\"required\":false},\"price\":{\"type\":\"integer\",\"required\":true,\"default\":0},\"inStock\":{\"type\":\"boolean\",\"required\":true,\"default\":true},\"specifications\":{\"type\":\"json\",\"required\":false,\"default\":{}},\"tags\":{\"type\":\"json\",\"required\":false,\"default\":[]},\"estimatedLabourMinutes\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"defaultOperationId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"Operation\",\"_meta\":{\"nullable\":true,\"onDelete\":\"RESTRICT\"}},\"partReference\":{\"type\":\"text\",\"required\":false,\"default\":\"\"}},\"methods\":{\"getSpecification\":{\"name\":\"getSpecification\",\"async\":true,\"parameters\":[{\"name\":\"key\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<unknown>\",\"isStatic\":false,\"isPublic\":true},\"updateSpecification\":{\"name\":\"updateSpecification\",\"async\":true,\"parameters\":[{\"name\":\"key\",\"type\":\"string\",\"optional\":false},{\"name\":\"value\",\"type\":\"unknown\",\"optional\":false}],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true},\"getAssets\":{\"name\":\"getAssets\",\"async\":true,\"parameters\":[{\"name\":\"relationship\",\"type\":\"string\",\"optional\":true}],\"returnType\":\"Promise<Asset[]>\",\"isStatic\":false,\"isPublic\":true},\"addAsset\":{\"name\":\"addAsset\",\"async\":true,\"parameters\":[{\"name\":\"asset\",\"type\":\"Asset\",\"optional\":false},{\"name\":\"relationship\",\"type\":\"any\",\"optional\":true,\"default\":\"attachment\"},{\"name\":\"sortOrder\",\"type\":\"any\",\"optional\":true}],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true},\"removeAsset\":{\"name\":\"removeAsset\",\"async\":true,\"parameters\":[{\"name\":\"assetId\",\"type\":\"string\",\"optional\":false},{\"name\":\"relationship\",\"type\":\"string\",\"optional\":true}],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"skipApiCheck\":true},\"tenantScoped\":{\"mode\":\"optional\"},\"tableName\":\"products\",\"tableStrategy\":\"sti\"},\"extends\":\"Product\",\"exportName\":\"Assembly\",\"collectionExportName\":\"AssemblyCollection\",\"validationRules\":[{\"field\":\"price\",\"rule\":\"required\",\"fieldType\":\"integer\"},{\"field\":\"inStock\",\"rule\":\"required\",\"fieldType\":\"boolean\"}],\"schema\":{\"tableName\":\"products\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"products\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"_meta_type\\\" TEXT NOT NULL,\\n \\\"_meta_data\\\" JSON,\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"product_type\\\" TEXT,\\n \\\"name\\\" TEXT,\\n \\\"description\\\" TEXT,\\n \\\"category\\\" TEXT,\\n \\\"manufacturer\\\" TEXT,\\n \\\"model\\\" TEXT,\\n \\\"price\\\" INTEGER DEFAULT 0,\\n \\\"in_stock\\\" BOOLEAN DEFAULT TRUE,\\n \\\"specifications\\\" JSON DEFAULT '{}',\\n \\\"tags\\\" JSON DEFAULT '[]',\\n \\\"estimated_labour_minutes\\\" INTEGER DEFAULT 0,\\n \\\"default_operation_id\\\" UUID,\\n \\\"part_reference\\\" TEXT DEFAULT ''\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"_meta_type\":{\"type\":\"TEXT\",\"notNull\":true},\"_meta_data\":{\"type\":\"JSON\",\"notNull\":false},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false},\"product_type\":{\"type\":\"TEXT\",\"notNull\":false},\"name\":{\"type\":\"TEXT\",\"notNull\":false},\"description\":{\"type\":\"TEXT\",\"notNull\":false},\"category\":{\"type\":\"TEXT\",\"notNull\":false},\"manufacturer\":{\"type\":\"TEXT\",\"notNull\":false},\"model\":{\"type\":\"TEXT\",\"notNull\":false},\"price\":{\"type\":\"INTEGER\",\"notNull\":false,\"default\":0},\"in_stock\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"default\":true},\"specifications\":{\"type\":\"JSON\",\"notNull\":false,\"default\":{}},\"tags\":{\"type\":\"JSON\",\"notNull\":false,\"default\":[]},\"estimated_labour_minutes\":{\"type\":\"INTEGER\",\"notNull\":false,\"default\":0},\"default_operation_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"foreignKey\":{\"table\":\"manufacturing_operations\",\"column\":\"id\",\"onDelete\":\"RESTRICT\",\"onUpdate\":\"CASCADE\"}},\"part_reference\":{\"type\":\"TEXT\",\"notNull\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"products_slug_context_meta_type_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\",\"_meta_type\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"products_meta_type_idx\",\"columns\":[\"_meta_type\"]},{\"name\":\"products_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"products_default_operation_id_idx\",\"columns\":[\"default_operation_id\"]}],\"version\":\"f2f2bf95\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:BillOfMaterials\":{\"name\":\"billofmaterials\",\"className\":\"BillOfMaterials\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:BillOfMaterials\",\"collection\":\"billofmaterialses\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/BillOfMaterials.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"productId\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"version\":{\"type\":\"integer\",\"required\":true,\"default\":1,\"_meta\":{\"required\":true}},\"effectiveDate\":{\"type\":\"datetime\",\"required\":false},\"status\":{\"type\":\"text\",\"required\":true,\"default\":\"draft\",\"_meta\":{\"required\":true}},\"notes\":{\"type\":\"text\",\"required\":false,\"default\":\"\"},\"currency\":{\"type\":\"text\",\"required\":false,\"default\":\"USD\"}},\"methods\":{},\"decoratorConfig\":{\"tableName\":\"manufacturing_boms\",\"conflictColumns\":[\"product_id\",\"version\",\"tenant_id\"],\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":true,\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"BillOfMaterials\",\"collectionExportName\":\"BillOfMaterialsCollection\",\"validationRules\":[{\"field\":\"productId\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"version\",\"rule\":\"required\",\"fieldType\":\"integer\"},{\"field\":\"status\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"manufacturing_boms\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_boms\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"product_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"version\\\" INTEGER NOT NULL DEFAULT 1,\\n \\\"effective_date\\\" TIMESTAMP,\\n \\\"status\\\" TEXT NOT NULL DEFAULT 'draft',\\n \\\"notes\\\" TEXT DEFAULT '',\\n \\\"currency\\\" TEXT DEFAULT 'USD'\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"product_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"version\":{\"type\":\"INTEGER\",\"notNull\":true,\"unique\":false,\"default\":1},\"effective_date\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"draft\"},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"USD\"}},\"indexes\":[{\"name\":\"manufacturing_boms_product_id_version_idx\",\"columns\":[\"product_id\",\"version\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_boms_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_boms_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"b2691adc\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:BomLine\":{\"name\":\"bomline\",\"className\":\"BomLine\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:BomLine\",\"collection\":\"bomlines\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/BomLine.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"bomId\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"componentSkuId\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"qtyPerUnit\":{\"type\":\"decimal\",\"required\":false,\"_meta\":{}},\"uom\":{\"type\":\"text\",\"required\":false,\"default\":\"each\"},\"wastePercent\":{\"type\":\"decimal\",\"required\":false,\"_meta\":{}},\"notes\":{\"type\":\"text\",\"required\":false,\"default\":\"\"}},\"methods\":{\"effectiveQtyPerUnit\":{\"name\":\"effectiveQtyPerUnit\",\"async\":false,\"parameters\":[],\"returnType\":\"number\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_bom_lines\",\"conflictColumns\":[\"bom_id\",\"component_sku_id\",\"tenant_id\"],\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"skipApiCheck\":true},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"BomLine\",\"collectionExportName\":\"BomLineCollection\",\"validationRules\":[{\"field\":\"bomId\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"componentSkuId\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"manufacturing_bom_lines\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_bom_lines\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"bom_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"component_sku_id\\\" TEXT NOT NULL DEFAULT '',\\n \\\"qty_per_unit\\\" REAL,\\n \\\"uom\\\" TEXT DEFAULT 'each',\\n \\\"waste_percent\\\" REAL,\\n \\\"notes\\\" TEXT DEFAULT ''\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"bom_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"component_sku_id\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"qty_per_unit\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"uom\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"each\"},\"waste_percent\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"manufacturing_bom_lines_bom_id_component_sku_id_idx\",\"columns\":[\"bom_id\",\"component_sku_id\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_bom_lines_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_bom_lines_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]}],\"version\":\"cfbae7e8\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:Operation\":{\"name\":\"operation\",\"className\":\"Operation\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:Operation\",\"collection\":\"operations\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/Operation.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"code\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"name\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"category\":{\"type\":\"text\",\"required\":false,\"default\":\"\"},\"isActive\":{\"type\":\"boolean\",\"required\":false,\"default\":true},\"requiredQualificationId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-human-resources:Qualification\",\"_meta\":{\"nullable\":true}}},\"methods\":{\"delete\":{\"name\":\"delete\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_operations\",\"conflictColumns\":[\"code\",\"tenant_id\"],\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"Operation\",\"collectionExportName\":\"OperationCollection\",\"validationRules\":[{\"field\":\"code\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"name\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"manufacturing_operations\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_operations\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"code\\\" TEXT NOT NULL DEFAULT '',\\n \\\"name\\\" TEXT NOT NULL DEFAULT '',\\n \\\"category\\\" TEXT DEFAULT '',\\n \\\"is_active\\\" BOOLEAN DEFAULT TRUE,\\n \\\"required_qualification_id\\\" UUID\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"code\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"name\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"category\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"is_active\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false,\"default\":true},\"required_qualification_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"manufacturing_operations_code_tenant_id_idx\",\"columns\":[\"code\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_operations_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_operations_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"manufacturing_operations_required_qualification_id_idx\",\"columns\":[\"required_qualification_id\"]}],\"version\":\"0b3134d6\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:ProductionRun\":{\"name\":\"productionrun\",\"className\":\"ProductionRun\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:ProductionRun\",\"collection\":\"productionruns\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/ProductionRun.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"bomId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"BillOfMaterials\",\"_meta\":{\"required\":true,\"onDelete\":\"RESTRICT\"}},\"targetQty\":{\"type\":\"decimal\",\"required\":false,\"_meta\":{}},\"completedQty\":{\"type\":\"decimal\",\"required\":false,\"_meta\":{}},\"status\":{\"type\":\"text\",\"required\":true,\"default\":\"planned\",\"_meta\":{\"required\":true}}},\"methods\":{\"remainingQty\":{\"name\":\"remainingQty\",\"async\":false,\"parameters\":[],\"returnType\":\"number\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"manufacturing_production_runs\",\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"tenantScoped\":{\"mode\":\"optional\"},\"conflictColumns\":[\"tenant_id\",\"slug\",\"context\"]},\"extends\":\"SmrtObject\",\"exportName\":\"ProductionRun\",\"collectionExportName\":\"ProductionRunCollection\",\"validationRules\":[{\"field\":\"bomId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"},{\"field\":\"status\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"manufacturing_production_runs\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_production_runs\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"bom_id\\\" UUID NOT NULL,\\n \\\"target_qty\\\" REAL,\\n \\\"completed_qty\\\" REAL,\\n \\\"status\\\" TEXT NOT NULL DEFAULT 'planned'\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"bom_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"manufacturing_boms\",\"column\":\"id\",\"onDelete\":\"RESTRICT\",\"onUpdate\":\"CASCADE\"}},\"target_qty\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"completed_qty\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"planned\"}},\"indexes\":[{\"name\":\"manufacturing_production_runs_slug_context_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_production_runs_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"manufacturing_production_runs_bom_id_idx\",\"columns\":[\"bom_id\"]}],\"version\":\"2ca8147d\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:ProductionRunCompletion\":{\"name\":\"productionruncompletion\",\"className\":\"ProductionRunCompletion\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:ProductionRunCompletion\",\"collection\":\"productionruncompletions\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/ProductionRunCompletion.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"runId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"ProductionRun\",\"_meta\":{\"required\":true,\"onDelete\":\"CASCADE\"}},\"qty\":{\"type\":\"decimal\",\"required\":false,\"_meta\":{}},\"completedAt\":{\"type\":\"datetime\",\"required\":false}},\"methods\":{},\"decoratorConfig\":{\"tableName\":\"manufacturing_production_run_completions\",\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"tenantScoped\":{\"mode\":\"optional\"},\"conflictColumns\":[\"tenant_id\",\"slug\",\"context\"]},\"extends\":\"SmrtObject\",\"exportName\":\"ProductionRunCompletion\",\"collectionExportName\":\"ProductionRunCompletionCollection\",\"validationRules\":[{\"field\":\"runId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"}],\"schema\":{\"tableName\":\"manufacturing_production_run_completions\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_production_run_completions\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"run_id\\\" UUID NOT NULL,\\n \\\"qty\\\" REAL,\\n \\\"completed_at\\\" TIMESTAMP\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"run_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"manufacturing_production_runs\",\"column\":\"id\",\"onDelete\":\"CASCADE\",\"onUpdate\":\"CASCADE\"}},\"qty\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"completed_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"manufacturing_production_run_completions_slug_context_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_production_run_completions_tenant_ea7a0d0125_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"manufacturing_production_run_completions_run_id_idx\",\"columns\":[\"run_id\"]}],\"version\":\"c940de6c\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"},\"@happyvertical/smrt-manufacturing:RoutingStep\":{\"name\":\"routingstep\",\"className\":\"RoutingStep\",\"qualifiedName\":\"@happyvertical/smrt-manufacturing:RoutingStep\",\"collection\":\"routingsteps\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/manufacturing/src/models/RoutingStep.ts\",\"packageName\":\"@happyvertical/smrt-manufacturing\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"bomId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"BillOfMaterials\",\"_meta\":{\"required\":true,\"onDelete\":\"CASCADE\"}},\"operationId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"Operation\",\"_meta\":{\"required\":true,\"onDelete\":\"RESTRICT\"}},\"sequence\":{\"type\":\"integer\",\"required\":true,\"default\":1,\"_meta\":{\"required\":true}},\"estimatedMinutes\":{\"type\":\"decimal\",\"required\":false,\"_meta\":{}},\"notes\":{\"type\":\"text\",\"required\":false,\"default\":\"\"}},\"methods\":{},\"decoratorConfig\":{\"tableName\":\"manufacturing_routing_steps\",\"conflictColumns\":[\"bom_id\",\"sequence\",\"tenant_id\"],\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"skipApiCheck\":true},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"RoutingStep\",\"collectionExportName\":\"RoutingStepCollection\",\"validationRules\":[{\"field\":\"bomId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"},{\"field\":\"operationId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"},{\"field\":\"sequence\",\"rule\":\"required\",\"fieldType\":\"integer\"}],\"schema\":{\"tableName\":\"manufacturing_routing_steps\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"manufacturing_routing_steps\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"bom_id\\\" UUID NOT NULL,\\n \\\"operation_id\\\" UUID NOT NULL,\\n \\\"sequence\\\" INTEGER NOT NULL DEFAULT 1,\\n \\\"estimated_minutes\\\" REAL,\\n \\\"notes\\\" TEXT DEFAULT ''\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"bom_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"manufacturing_boms\",\"column\":\"id\",\"onDelete\":\"CASCADE\",\"onUpdate\":\"CASCADE\"}},\"operation_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"manufacturing_operations\",\"column\":\"id\",\"onDelete\":\"RESTRICT\",\"onUpdate\":\"CASCADE\"}},\"sequence\":{\"type\":\"INTEGER\",\"notNull\":true,\"unique\":false,\"default\":1},\"estimated_minutes\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"manufacturing_routing_steps_bom_id_sequence_idx\",\"columns\":[\"bom_id\",\"sequence\",\"tenant_id\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"manufacturing_routing_steps_slug_context_idx\",\"columns\":[\"slug\",\"context\"]},{\"name\":\"manufacturing_routing_steps_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"manufacturing_routing_steps_operation_id_idx\",\"columns\":[\"operation_id\"]}],\"version\":\"41ac3c66\"},\"importPath\":\"@happyvertical/smrt-manufacturing\"}},\"moduleType\":\"smrt\",\"smrtDependencies\":[\"@happyvertical/smrt-core\",\"@happyvertical/smrt-fields\",\"@happyvertical/smrt-inventory\",\"@happyvertical/smrt-products\",\"@happyvertical/smrt-tenancy\"]}"));
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/models/Operation.ts
|
|
24
|
+
var __defProp$6 = Object.defineProperty;
|
|
25
|
+
var __getOwnPropDesc$6 = Object.getOwnPropertyDescriptor;
|
|
26
|
+
var __decorateClass$6 = (decorators, target, key, kind) => {
|
|
27
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$6(target, key) : target;
|
|
28
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
29
|
+
if (kind && result) __defProp$6(target, key, result);
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
var Operation = class extends SmrtObject {
|
|
33
|
+
tenantId = null;
|
|
34
|
+
code = "";
|
|
35
|
+
name = "";
|
|
36
|
+
/**
|
|
37
|
+
* Free-text grouping for lists (`fabrication`, `finishing`, `quality`).
|
|
38
|
+
* Empty when the operation is not grouped.
|
|
39
|
+
*/
|
|
40
|
+
category = "";
|
|
41
|
+
/**
|
|
42
|
+
* `false` once the operation is retired. A retired operation stays on the
|
|
43
|
+
* routings and history that name it and is left out of pickers.
|
|
44
|
+
*/
|
|
45
|
+
isActive = true;
|
|
46
|
+
requiredQualificationId = null;
|
|
47
|
+
constructor(options = {}) {
|
|
48
|
+
super(options);
|
|
49
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
50
|
+
if (options.code !== void 0) this.code = options.code;
|
|
51
|
+
if (options.name !== void 0) this.name = options.name;
|
|
52
|
+
if (options.category !== void 0) this.category = options.category;
|
|
53
|
+
if (options.isActive !== void 0) this.isActive = options.isActive;
|
|
54
|
+
if (options.requiredQualificationId !== void 0) this.requiredQualificationId = options.requiredQualificationId;
|
|
55
|
+
}
|
|
56
|
+
/** Operations are retired, never deleted, so history keeps resolving. */
|
|
57
|
+
async delete() {
|
|
58
|
+
throw new Error(`Operation ${this.code || this.id} cannot be deleted; retire it instead.`);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
__decorateClass$6([tenantId({ nullable: true })], Operation.prototype, "tenantId", 2);
|
|
62
|
+
__decorateClass$6([field({ required: true })], Operation.prototype, "code", 2);
|
|
63
|
+
__decorateClass$6([field({ required: true })], Operation.prototype, "name", 2);
|
|
64
|
+
__decorateClass$6([crossPackageRef("@happyvertical/smrt-human-resources:Qualification", { nullable: true })], Operation.prototype, "requiredQualificationId", 2);
|
|
65
|
+
Operation = __decorateClass$6([TenantScoped({ mode: "optional" }), smrt({
|
|
66
|
+
tableName: "manufacturing_operations",
|
|
67
|
+
conflictColumns: ["code", "tenant_id"],
|
|
68
|
+
api: { include: ["list", "get"] },
|
|
69
|
+
mcp: { include: ["list", "get"] },
|
|
70
|
+
cli: { include: ["list", "get"] }
|
|
71
|
+
})], Operation);
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/models/Assembly.ts
|
|
74
|
+
var __defProp$5 = Object.defineProperty;
|
|
75
|
+
var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
|
|
76
|
+
var __decorateClass$5 = (decorators, target, key, kind) => {
|
|
77
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
|
|
78
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
79
|
+
if (kind && result) __defProp$5(target, key, result);
|
|
80
|
+
return result;
|
|
81
|
+
};
|
|
82
|
+
var Assembly = class extends Product {
|
|
83
|
+
productType = ProductType.ASSEMBLY;
|
|
84
|
+
estimatedLabourMinutes = 0;
|
|
85
|
+
defaultOperationId = null;
|
|
86
|
+
partReference = "";
|
|
87
|
+
constructor(options = {}) {
|
|
88
|
+
super(options);
|
|
89
|
+
if (options.estimatedLabourMinutes !== void 0) this.estimatedLabourMinutes = options.estimatedLabourMinutes;
|
|
90
|
+
if (options.defaultOperationId !== void 0) this.defaultOperationId = options.defaultOperationId;
|
|
91
|
+
if (options.partReference !== void 0) this.partReference = options.partReference;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Refuses a labour estimate that is not a whole number of minutes of zero
|
|
95
|
+
* or more, on every save (generated REST and collection writes included).
|
|
96
|
+
*/
|
|
97
|
+
async validateBeforeSave() {
|
|
98
|
+
await super.validateBeforeSave();
|
|
99
|
+
const minutes = this.estimatedLabourMinutes;
|
|
100
|
+
if (!Number.isSafeInteger(minutes) || minutes < 0) throw ValidationError.invalidValue("estimatedLabourMinutes", minutes, "a whole number of minutes, zero or more");
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
__decorateClass$5([field()], Assembly.prototype, "estimatedLabourMinutes", 2);
|
|
104
|
+
__decorateClass$5([foreignKey(Operation, {
|
|
105
|
+
nullable: true,
|
|
106
|
+
onDelete: "RESTRICT"
|
|
107
|
+
})], Assembly.prototype, "defaultOperationId", 2);
|
|
108
|
+
__decorateClass$5([field()], Assembly.prototype, "partReference", 2);
|
|
109
|
+
Assembly = __decorateClass$5([TenantScoped({ mode: "optional" }), smrt({
|
|
110
|
+
api: { include: [
|
|
111
|
+
"list",
|
|
112
|
+
"get",
|
|
113
|
+
"create",
|
|
114
|
+
"update"
|
|
115
|
+
] },
|
|
116
|
+
mcp: { include: ["list", "get"] },
|
|
117
|
+
cli: { skipApiCheck: true }
|
|
118
|
+
})], Assembly);
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/collections/AssemblyCollection.ts
|
|
121
|
+
var AssemblyCollection = class extends ProductCollection {
|
|
122
|
+
static _itemClass = Assembly;
|
|
123
|
+
};
|
|
8
124
|
//#endregion
|
|
9
125
|
//#region src/models/BillOfMaterials.ts
|
|
10
|
-
var __defProp$
|
|
11
|
-
var __getOwnPropDesc$
|
|
12
|
-
var __decorateClass$
|
|
13
|
-
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$
|
|
126
|
+
var __defProp$4 = Object.defineProperty;
|
|
127
|
+
var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
|
|
128
|
+
var __decorateClass$4 = (decorators, target, key, kind) => {
|
|
129
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
|
|
14
130
|
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
15
|
-
if (kind && result) __defProp$
|
|
131
|
+
if (kind && result) __defProp$4(target, key, result);
|
|
16
132
|
return result;
|
|
17
133
|
};
|
|
18
134
|
var BillOfMaterials = class extends SmrtObject {
|
|
@@ -47,12 +163,25 @@ var BillOfMaterials = class extends SmrtObject {
|
|
|
47
163
|
if (options.notes !== void 0) this.notes = options.notes;
|
|
48
164
|
if (options.currency !== void 0) this.currency = options.currency;
|
|
49
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Refuses saving the bill as `active` when one of its lines would make
|
|
168
|
+
* its product contain itself, directly or through sub-assemblies' active
|
|
169
|
+
* bills, with a `BomCycleError` naming the path. Draft and superseded
|
|
170
|
+
* bills are not part of the structure and are not checked here (their
|
|
171
|
+
* lines are checked as they are saved).
|
|
172
|
+
*/
|
|
173
|
+
async validateBeforeSave() {
|
|
174
|
+
await super.validateBeforeSave();
|
|
175
|
+
if (this.status !== "active") return;
|
|
176
|
+
const { AssemblyService } = await Promise.resolve().then(() => AssemblyService_exports);
|
|
177
|
+
await (await AssemblyService.create({ db: this.db })).assertBillAcyclic(this);
|
|
178
|
+
}
|
|
50
179
|
};
|
|
51
|
-
__decorateClass$
|
|
52
|
-
__decorateClass$
|
|
53
|
-
__decorateClass$
|
|
54
|
-
__decorateClass$
|
|
55
|
-
BillOfMaterials = __decorateClass$
|
|
180
|
+
__decorateClass$4([tenantId({ nullable: true })], BillOfMaterials.prototype, "tenantId", 2);
|
|
181
|
+
__decorateClass$4([field({ required: true })], BillOfMaterials.prototype, "productId", 2);
|
|
182
|
+
__decorateClass$4([field({ required: true })], BillOfMaterials.prototype, "version", 2);
|
|
183
|
+
__decorateClass$4([field({ required: true })], BillOfMaterials.prototype, "status", 2);
|
|
184
|
+
BillOfMaterials = __decorateClass$4([TenantScoped({ mode: "optional" }), smrt({
|
|
56
185
|
tableName: "manufacturing_boms",
|
|
57
186
|
conflictColumns: [
|
|
58
187
|
"product_id",
|
|
@@ -112,12 +241,12 @@ var BillOfMaterialsCollection = class extends SmrtCollection {
|
|
|
112
241
|
};
|
|
113
242
|
//#endregion
|
|
114
243
|
//#region src/models/BomLine.ts
|
|
115
|
-
var __defProp = Object.defineProperty;
|
|
116
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
117
|
-
var __decorateClass = (decorators, target, key, kind) => {
|
|
118
|
-
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
244
|
+
var __defProp$3 = Object.defineProperty;
|
|
245
|
+
var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
|
|
246
|
+
var __decorateClass$3 = (decorators, target, key, kind) => {
|
|
247
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
|
|
119
248
|
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
120
|
-
if (kind && result) __defProp(target, key, result);
|
|
249
|
+
if (kind && result) __defProp$3(target, key, result);
|
|
121
250
|
return result;
|
|
122
251
|
};
|
|
123
252
|
var BomLine = class extends SmrtObject {
|
|
@@ -158,13 +287,24 @@ var BomLine = class extends SmrtObject {
|
|
|
158
287
|
effectiveQtyPerUnit() {
|
|
159
288
|
return Number(this.qtyPerUnit ?? 0) * (1 + Number(this.wastePercent ?? 0) / 100);
|
|
160
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Refuses a line that would make its bill's product contain itself,
|
|
292
|
+
* directly or through sub-assemblies' active bills, with a
|
|
293
|
+
* `BomCycleError` naming the path. Runs on every save, so the generated
|
|
294
|
+
* REST `create`/`update` routes refuse it too.
|
|
295
|
+
*/
|
|
296
|
+
async validateBeforeSave() {
|
|
297
|
+
await super.validateBeforeSave();
|
|
298
|
+
const { AssemblyService } = await Promise.resolve().then(() => AssemblyService_exports);
|
|
299
|
+
await (await AssemblyService.create({ db: this.db })).assertLineAcyclic(this);
|
|
300
|
+
}
|
|
161
301
|
};
|
|
162
|
-
__decorateClass([tenantId({ nullable: true })], BomLine.prototype, "tenantId", 2);
|
|
163
|
-
__decorateClass([field({ required: true })], BomLine.prototype, "bomId", 2);
|
|
164
|
-
__decorateClass([field({ required: true })], BomLine.prototype, "componentSkuId", 2);
|
|
165
|
-
__decorateClass([field({ type: "decimal" })], BomLine.prototype, "qtyPerUnit", 2);
|
|
166
|
-
__decorateClass([field({ type: "decimal" })], BomLine.prototype, "wastePercent", 2);
|
|
167
|
-
BomLine = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
|
|
302
|
+
__decorateClass$3([tenantId({ nullable: true })], BomLine.prototype, "tenantId", 2);
|
|
303
|
+
__decorateClass$3([field({ required: true })], BomLine.prototype, "bomId", 2);
|
|
304
|
+
__decorateClass$3([field({ required: true })], BomLine.prototype, "componentSkuId", 2);
|
|
305
|
+
__decorateClass$3([field({ type: "decimal" })], BomLine.prototype, "qtyPerUnit", 2);
|
|
306
|
+
__decorateClass$3([field({ type: "decimal" })], BomLine.prototype, "wastePercent", 2);
|
|
307
|
+
BomLine = __decorateClass$3([TenantScoped({ mode: "optional" }), smrt({
|
|
168
308
|
tableName: "manufacturing_bom_lines",
|
|
169
309
|
conflictColumns: [
|
|
170
310
|
"bom_id",
|
|
@@ -208,24 +348,710 @@ var BomLineCollection = class extends SmrtCollection {
|
|
|
208
348
|
}
|
|
209
349
|
};
|
|
210
350
|
//#endregion
|
|
351
|
+
//#region src/collections/OperationCollection.ts
|
|
352
|
+
var OperationCollection = class extends SmrtCollection {
|
|
353
|
+
static _itemClass = Operation;
|
|
354
|
+
/** Return the operation with the given code, or `null`. */
|
|
355
|
+
async findByCode(code) {
|
|
356
|
+
return (await this.list({
|
|
357
|
+
where: { code },
|
|
358
|
+
limit: 1
|
|
359
|
+
}))[0] ?? null;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Return operations ordered by category then code. Retired operations are
|
|
363
|
+
* left out unless `includeRetired` is set, so the default is a picker list.
|
|
364
|
+
*/
|
|
365
|
+
async listOperations(options = {}) {
|
|
366
|
+
return this.list({
|
|
367
|
+
where: options.includeRetired ? {} : { isActive: true },
|
|
368
|
+
orderBy: ["category ASC", "code ASC"]
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/quantity.ts
|
|
374
|
+
var QUANTITY_DECIMALS = 6;
|
|
375
|
+
var SCALE = 10 ** 6;
|
|
376
|
+
var MAX_QUANTITY = 999999999;
|
|
377
|
+
function roundQuantity(value) {
|
|
378
|
+
return Number(value.toFixed(6));
|
|
379
|
+
}
|
|
380
|
+
function toMillionths(value) {
|
|
381
|
+
return Number(value.toFixed(6).replace(".", ""));
|
|
382
|
+
}
|
|
383
|
+
function fromMillionths(millionths) {
|
|
384
|
+
return millionths / SCALE;
|
|
385
|
+
}
|
|
386
|
+
//#endregion
|
|
387
|
+
//#region src/models/ProductionRun.ts
|
|
388
|
+
var __defProp$2 = Object.defineProperty;
|
|
389
|
+
var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
|
|
390
|
+
var __decorateClass$2 = (decorators, target, key, kind) => {
|
|
391
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
|
|
392
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
393
|
+
if (kind && result) __defProp$2(target, key, result);
|
|
394
|
+
return result;
|
|
395
|
+
};
|
|
396
|
+
var PRODUCTION_RUN_STATUSES = [
|
|
397
|
+
"planned",
|
|
398
|
+
"in_progress",
|
|
399
|
+
"done",
|
|
400
|
+
"cancelled"
|
|
401
|
+
];
|
|
402
|
+
var ProductionRun = class extends SmrtObject {
|
|
403
|
+
tenantId = null;
|
|
404
|
+
bomId = "";
|
|
405
|
+
targetQty = 0;
|
|
406
|
+
completedQty = 0;
|
|
407
|
+
status = "planned";
|
|
408
|
+
constructor(options = {}) {
|
|
409
|
+
super(options);
|
|
410
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
411
|
+
if (options.bomId !== void 0) this.bomId = options.bomId;
|
|
412
|
+
if (options.targetQty !== void 0) this.targetQty = options.targetQty;
|
|
413
|
+
if (options.completedQty !== void 0) this.completedQty = options.completedQty;
|
|
414
|
+
if (options.status !== void 0) this.status = options.status;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Units still to build, at the six-decimal quantity precision; `0` once
|
|
418
|
+
* the target is reached.
|
|
419
|
+
*/
|
|
420
|
+
remainingQty() {
|
|
421
|
+
return Math.max(0, roundQuantity(Number(this.targetQty) - Number(this.completedQty)));
|
|
422
|
+
}
|
|
423
|
+
/** Refuses a status outside the lifecycle and impossible quantities. */
|
|
424
|
+
async validateBeforeSave() {
|
|
425
|
+
await super.validateBeforeSave();
|
|
426
|
+
if (!PRODUCTION_RUN_STATUSES.includes(this.status)) throw new ValidationError(`Unknown production run status: ${String(this.status)}`, "MANUFACTURING_PRODUCTION_RUN_INVALID", { field: "status" });
|
|
427
|
+
const target = Number(this.targetQty);
|
|
428
|
+
const completed = Number(this.completedQty);
|
|
429
|
+
if (!Number.isFinite(target) || target <= 0) throw new ValidationError("A production run needs a target quantity greater than zero.", "MANUFACTURING_PRODUCTION_RUN_INVALID", { field: "targetQty" });
|
|
430
|
+
if (!Number.isFinite(completed) || completed < 0) throw new ValidationError("A production run cannot have a negative completed quantity.", "MANUFACTURING_PRODUCTION_RUN_INVALID", { field: "completedQty" });
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
__decorateClass$2([tenantId({ nullable: true })], ProductionRun.prototype, "tenantId", 2);
|
|
434
|
+
__decorateClass$2([foreignKey(BillOfMaterials, {
|
|
435
|
+
required: true,
|
|
436
|
+
onDelete: "RESTRICT"
|
|
437
|
+
})], ProductionRun.prototype, "bomId", 2);
|
|
438
|
+
__decorateClass$2([field({ type: "decimal" })], ProductionRun.prototype, "targetQty", 2);
|
|
439
|
+
__decorateClass$2([field({ type: "decimal" })], ProductionRun.prototype, "completedQty", 2);
|
|
440
|
+
__decorateClass$2([field({ required: true })], ProductionRun.prototype, "status", 2);
|
|
441
|
+
ProductionRun = __decorateClass$2([TenantScoped({ mode: "optional" }), smrt({
|
|
442
|
+
tableName: "manufacturing_production_runs",
|
|
443
|
+
api: { include: ["list", "get"] },
|
|
444
|
+
mcp: { include: ["list", "get"] },
|
|
445
|
+
cli: { include: ["list", "get"] }
|
|
446
|
+
})], ProductionRun);
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region src/collections/ProductionRunCollection.ts
|
|
449
|
+
var ProductionRunCollection = class extends SmrtCollection {
|
|
450
|
+
static _itemClass = ProductionRun;
|
|
451
|
+
/** Runs against one bill, newest first. */
|
|
452
|
+
async findByBom(bomId) {
|
|
453
|
+
return this.list({
|
|
454
|
+
where: { bomId },
|
|
455
|
+
orderBy: "created_at DESC"
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
/** Runs in one status, oldest first (a work queue). */
|
|
459
|
+
async findByStatus(status) {
|
|
460
|
+
return this.list({
|
|
461
|
+
where: { status },
|
|
462
|
+
orderBy: "created_at ASC"
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/models/ProductionRunCompletion.ts
|
|
468
|
+
var __defProp$1 = Object.defineProperty;
|
|
469
|
+
var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
|
|
470
|
+
var __decorateClass$1 = (decorators, target, key, kind) => {
|
|
471
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
|
|
472
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
473
|
+
if (kind && result) __defProp$1(target, key, result);
|
|
474
|
+
return result;
|
|
475
|
+
};
|
|
476
|
+
var ProductionRunCompletion = class extends SmrtObject {
|
|
477
|
+
tenantId = null;
|
|
478
|
+
runId = "";
|
|
479
|
+
qty = 0;
|
|
480
|
+
/** When the units were finished (defaults to when the report was made). */
|
|
481
|
+
completedAt = /* @__PURE__ */ new Date();
|
|
482
|
+
constructor(options = {}) {
|
|
483
|
+
super(options);
|
|
484
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
485
|
+
if (options.runId !== void 0) this.runId = options.runId;
|
|
486
|
+
if (options.qty !== void 0) this.qty = options.qty;
|
|
487
|
+
if (options.completedAt !== void 0) this.completedAt = options.completedAt instanceof Date ? options.completedAt : new Date(options.completedAt);
|
|
488
|
+
}
|
|
489
|
+
/** Refuses a report of zero, negative or non-finite units. */
|
|
490
|
+
async validateBeforeSave() {
|
|
491
|
+
await super.validateBeforeSave();
|
|
492
|
+
const qty = Number(this.qty);
|
|
493
|
+
if (!Number.isFinite(qty) || qty <= 0) throw new ValidationError("A completion needs a quantity greater than zero.", "MANUFACTURING_PRODUCTION_RUN_INVALID", { field: "qty" });
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
__decorateClass$1([tenantId({ nullable: true })], ProductionRunCompletion.prototype, "tenantId", 2);
|
|
497
|
+
__decorateClass$1([foreignKey(ProductionRun, {
|
|
498
|
+
required: true,
|
|
499
|
+
onDelete: "CASCADE"
|
|
500
|
+
})], ProductionRunCompletion.prototype, "runId", 2);
|
|
501
|
+
__decorateClass$1([field({ type: "decimal" })], ProductionRunCompletion.prototype, "qty", 2);
|
|
502
|
+
ProductionRunCompletion = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({
|
|
503
|
+
tableName: "manufacturing_production_run_completions",
|
|
504
|
+
api: { include: ["list", "get"] },
|
|
505
|
+
mcp: { include: ["list", "get"] },
|
|
506
|
+
cli: { include: ["list", "get"] }
|
|
507
|
+
})], ProductionRunCompletion);
|
|
508
|
+
//#endregion
|
|
509
|
+
//#region src/collections/ProductionRunCompletionCollection.ts
|
|
510
|
+
var ProductionRunCompletionCollection = class extends SmrtCollection {
|
|
511
|
+
static _itemClass = ProductionRunCompletion;
|
|
512
|
+
/** A run's completions in the order they happened. */
|
|
513
|
+
async findByRun(runId) {
|
|
514
|
+
return this.list({
|
|
515
|
+
where: { runId },
|
|
516
|
+
orderBy: ["completedAt ASC", "created_at ASC"]
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
//#endregion
|
|
521
|
+
//#region src/models/RoutingStep.ts
|
|
522
|
+
var __defProp = Object.defineProperty;
|
|
523
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
524
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
525
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
526
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
527
|
+
if (kind && result) __defProp(target, key, result);
|
|
528
|
+
return result;
|
|
529
|
+
};
|
|
530
|
+
var RoutingStep = class extends SmrtObject {
|
|
531
|
+
tenantId = null;
|
|
532
|
+
bomId = "";
|
|
533
|
+
operationId = "";
|
|
534
|
+
sequence = 1;
|
|
535
|
+
estimatedMinutes = 0;
|
|
536
|
+
/** Optional free-form note for this step. */
|
|
537
|
+
notes = "";
|
|
538
|
+
constructor(options = {}) {
|
|
539
|
+
super(options);
|
|
540
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
541
|
+
if (options.bomId !== void 0) this.bomId = options.bomId;
|
|
542
|
+
if (options.operationId !== void 0) this.operationId = options.operationId;
|
|
543
|
+
if (options.sequence !== void 0) this.sequence = options.sequence;
|
|
544
|
+
if (options.estimatedMinutes !== void 0) this.estimatedMinutes = options.estimatedMinutes;
|
|
545
|
+
if (options.notes !== void 0) this.notes = options.notes;
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
__decorateClass([tenantId({ nullable: true })], RoutingStep.prototype, "tenantId", 2);
|
|
549
|
+
__decorateClass([foreignKey(BillOfMaterials, {
|
|
550
|
+
required: true,
|
|
551
|
+
onDelete: "CASCADE"
|
|
552
|
+
})], RoutingStep.prototype, "bomId", 2);
|
|
553
|
+
__decorateClass([foreignKey(Operation, {
|
|
554
|
+
required: true,
|
|
555
|
+
onDelete: "RESTRICT"
|
|
556
|
+
})], RoutingStep.prototype, "operationId", 2);
|
|
557
|
+
__decorateClass([field({ required: true })], RoutingStep.prototype, "sequence", 2);
|
|
558
|
+
__decorateClass([field({ type: "decimal" })], RoutingStep.prototype, "estimatedMinutes", 2);
|
|
559
|
+
RoutingStep = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
|
|
560
|
+
tableName: "manufacturing_routing_steps",
|
|
561
|
+
conflictColumns: [
|
|
562
|
+
"bom_id",
|
|
563
|
+
"sequence",
|
|
564
|
+
"tenant_id"
|
|
565
|
+
],
|
|
566
|
+
api: { include: ["list", "get"] },
|
|
567
|
+
mcp: { include: ["list", "get"] },
|
|
568
|
+
cli: { skipApiCheck: true }
|
|
569
|
+
})], RoutingStep);
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region src/collections/RoutingStepCollection.ts
|
|
572
|
+
var RoutingStepCollection = class extends SmrtCollection {
|
|
573
|
+
static _itemClass = RoutingStep;
|
|
574
|
+
/** Return the routing of a bill in step order; empty when it has none. */
|
|
575
|
+
async findByBom(bomId) {
|
|
576
|
+
return this.list({
|
|
577
|
+
where: { bomId },
|
|
578
|
+
orderBy: "sequence ASC"
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
/** Return every step that uses the given operation, across bills. */
|
|
582
|
+
async findByOperation(operationId) {
|
|
583
|
+
return this.list({
|
|
584
|
+
where: { operationId },
|
|
585
|
+
orderBy: "bomId ASC"
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
//#endregion
|
|
590
|
+
//#region src/operation-types.ts
|
|
591
|
+
var OperationNotFoundError = class extends Error {
|
|
592
|
+
constructor(operationId) {
|
|
593
|
+
super(`Operation not found: ${operationId}`);
|
|
594
|
+
this.operationId = operationId;
|
|
595
|
+
}
|
|
596
|
+
operationId;
|
|
597
|
+
name = "OperationNotFoundError";
|
|
598
|
+
};
|
|
599
|
+
var DuplicateOperationCodeError = class extends Error {
|
|
600
|
+
constructor(code) {
|
|
601
|
+
super(`An operation with code "${code}" already exists.`);
|
|
602
|
+
this.code = code;
|
|
603
|
+
}
|
|
604
|
+
code;
|
|
605
|
+
name = "DuplicateOperationCodeError";
|
|
606
|
+
};
|
|
607
|
+
var OperationRetiredError = class extends Error {
|
|
608
|
+
constructor(operationId) {
|
|
609
|
+
super(`Operation ${operationId} is retired and cannot be added to a routing.`);
|
|
610
|
+
this.operationId = operationId;
|
|
611
|
+
}
|
|
612
|
+
operationId;
|
|
613
|
+
name = "OperationRetiredError";
|
|
614
|
+
};
|
|
615
|
+
var InvalidOperationInputError = class extends Error {
|
|
616
|
+
name = "InvalidOperationInputError";
|
|
617
|
+
};
|
|
618
|
+
//#endregion
|
|
619
|
+
//#region src/production-run-types.ts
|
|
620
|
+
var ProductionRunNotFoundError = class extends Error {
|
|
621
|
+
constructor(runId) {
|
|
622
|
+
super(`Production run not found: ${runId}`);
|
|
623
|
+
this.runId = runId;
|
|
624
|
+
}
|
|
625
|
+
runId;
|
|
626
|
+
name = "ProductionRunNotFoundError";
|
|
627
|
+
};
|
|
628
|
+
var ProductionRunStateError = class extends Error {
|
|
629
|
+
constructor(runId, status, action) {
|
|
630
|
+
super(`Production run ${runId} is ${status}; cannot ${action} it.`);
|
|
631
|
+
this.runId = runId;
|
|
632
|
+
this.status = status;
|
|
633
|
+
this.action = action;
|
|
634
|
+
}
|
|
635
|
+
runId;
|
|
636
|
+
status;
|
|
637
|
+
action;
|
|
638
|
+
name = "ProductionRunStateError";
|
|
639
|
+
};
|
|
640
|
+
var ProductionRunOverCompletionError = class extends Error {
|
|
641
|
+
constructor(runId, qty, remaining) {
|
|
642
|
+
super(`Production run ${runId} has ${remaining} left to build; cannot report ${qty}.`);
|
|
643
|
+
this.runId = runId;
|
|
644
|
+
this.qty = qty;
|
|
645
|
+
this.remaining = remaining;
|
|
646
|
+
}
|
|
647
|
+
runId;
|
|
648
|
+
qty;
|
|
649
|
+
remaining;
|
|
650
|
+
name = "ProductionRunOverCompletionError";
|
|
651
|
+
};
|
|
652
|
+
var InvalidProductionRunInputError = class extends Error {
|
|
653
|
+
name = "InvalidProductionRunInputError";
|
|
654
|
+
};
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/tenant-scope.ts
|
|
657
|
+
function readOwnAndGlobal(owner, read) {
|
|
658
|
+
const scope = owner ?? getCurrentTenant()?.tenantId ?? null;
|
|
659
|
+
return scope ? withTenantGlobalRead(scope, read) : read();
|
|
660
|
+
}
|
|
661
|
+
function isOwnOrGlobal(row, owner) {
|
|
662
|
+
const rowTenant = row.tenantId ?? null;
|
|
663
|
+
return rowTenant === null || rowTenant === owner;
|
|
664
|
+
}
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region src/services/AssemblyService.ts
|
|
667
|
+
var AssemblyService_exports = /* @__PURE__ */ __exportAll({
|
|
668
|
+
AssemblyService: () => AssemblyService,
|
|
669
|
+
BomCycleError: () => BomCycleError,
|
|
670
|
+
createAssemblyService: () => createAssemblyService
|
|
671
|
+
});
|
|
672
|
+
var BomCycleError = class extends ValidationError {
|
|
673
|
+
constructor(bomId, path) {
|
|
674
|
+
const first = path[0]?.name ?? "";
|
|
675
|
+
super(`Refused: "${first}" would contain itself: ${path.map((entry) => entry.name).join(" → ")}`, "MANUFACTURING_BOM_CYCLE", {
|
|
676
|
+
bomId,
|
|
677
|
+
path
|
|
678
|
+
});
|
|
679
|
+
this.bomId = bomId;
|
|
680
|
+
this.path = path;
|
|
681
|
+
}
|
|
682
|
+
bomId;
|
|
683
|
+
path;
|
|
684
|
+
name = "BomCycleError";
|
|
685
|
+
};
|
|
686
|
+
var UUID_PATTERN$2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
687
|
+
function visibleTo(row, options) {
|
|
688
|
+
if (options.tenantId === void 0) return true;
|
|
689
|
+
const rowTenant = row.tenantId ?? null;
|
|
690
|
+
return rowTenant === null || rowTenant === options.tenantId;
|
|
691
|
+
}
|
|
692
|
+
function isUuid(value) {
|
|
693
|
+
return typeof value === "string" && UUID_PATTERN$2.test(value);
|
|
694
|
+
}
|
|
695
|
+
var AssemblyService = class AssemblyService {
|
|
696
|
+
constructor(products, skus, boms, lines) {
|
|
697
|
+
this.products = products;
|
|
698
|
+
this.skus = skus;
|
|
699
|
+
this.boms = boms;
|
|
700
|
+
this.lines = lines;
|
|
701
|
+
}
|
|
702
|
+
products;
|
|
703
|
+
skus;
|
|
704
|
+
boms;
|
|
705
|
+
lines;
|
|
706
|
+
/** Factory — prefer {@link createAssemblyService}. */
|
|
707
|
+
static async create(options) {
|
|
708
|
+
const db = await resolveDatabase(options.db);
|
|
709
|
+
const [products, skus, boms, lines] = await Promise.all([
|
|
710
|
+
ProductCollection.create({ db }),
|
|
711
|
+
SkuCollection.create({ db }),
|
|
712
|
+
BillOfMaterialsCollection.create({ db }),
|
|
713
|
+
BomLineCollection.create({ db })
|
|
714
|
+
]);
|
|
715
|
+
return new AssemblyService(products, skus, boms, lines);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Resolve a component SKU to what it is: an assembly (with its active bill,
|
|
719
|
+
* or `null` when it has none), a material, a bought item, or missing. Never
|
|
720
|
+
* throws for an unknown id; it reports `missing` instead.
|
|
721
|
+
*
|
|
722
|
+
* This is the documented link a multi-level explosion walks: recurse into
|
|
723
|
+
* `activeBom` while `kind === 'assembly'`, stop at every other kind. Pass
|
|
724
|
+
* the parent bill's `tenantId` so a shared assembly resolves to that
|
|
725
|
+
* tenant's bill, not another tenant's.
|
|
726
|
+
*/
|
|
727
|
+
async resolveComponent(skuId, options = {}) {
|
|
728
|
+
const missing = (sku2) => ({
|
|
729
|
+
skuId,
|
|
730
|
+
kind: "missing",
|
|
731
|
+
sku: sku2,
|
|
732
|
+
product: null,
|
|
733
|
+
assembly: null,
|
|
734
|
+
activeBom: null,
|
|
735
|
+
buildable: false
|
|
736
|
+
});
|
|
737
|
+
if (!isUuid(skuId)) return missing(null);
|
|
738
|
+
const sku = await this.byId(this.skus, skuId, options);
|
|
739
|
+
if (!sku || !visibleTo(sku, options)) return missing(null);
|
|
740
|
+
if (!isUuid(sku.productId)) return missing(sku);
|
|
741
|
+
const product = await this.byId(this.products, sku.productId, options);
|
|
742
|
+
if (!product || !visibleTo(product, options)) return missing(sku);
|
|
743
|
+
if (product instanceof Assembly) {
|
|
744
|
+
const activeBom = await this.findActiveBom(product.id, options);
|
|
745
|
+
return {
|
|
746
|
+
skuId,
|
|
747
|
+
kind: "assembly",
|
|
748
|
+
sku,
|
|
749
|
+
product,
|
|
750
|
+
assembly: product,
|
|
751
|
+
activeBom,
|
|
752
|
+
buildable: activeBom !== null
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
return {
|
|
756
|
+
skuId,
|
|
757
|
+
kind: product instanceof Material ? "material" : "bought",
|
|
758
|
+
sku,
|
|
759
|
+
product,
|
|
760
|
+
assembly: null,
|
|
761
|
+
activeBom: null,
|
|
762
|
+
buildable: false
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* The highest-version active bill for a product. With `options.tenantId`,
|
|
767
|
+
* that tenant's own bill, or else a global one; another tenant's never.
|
|
768
|
+
*/
|
|
769
|
+
async findActiveBom(productId, options = {}) {
|
|
770
|
+
const owner = options.tenantId;
|
|
771
|
+
if (owner === void 0) return this.boms.findActiveForProduct(productId);
|
|
772
|
+
const active = await readOwnAndGlobal(owner, () => this.boms.list({
|
|
773
|
+
where: {
|
|
774
|
+
productId,
|
|
775
|
+
status: "active"
|
|
776
|
+
},
|
|
777
|
+
orderBy: "version DESC"
|
|
778
|
+
}));
|
|
779
|
+
return active.find((bom) => (bom.tenantId ?? null) === owner) ?? active.find((bom) => (bom.tenantId ?? null) === null) ?? null;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* One row by id: through the collection's `get` without an explicit
|
|
783
|
+
* tenant, else as a list read of that tenant's and global rows.
|
|
784
|
+
*/
|
|
785
|
+
async byId(collection, id, options) {
|
|
786
|
+
const owner = options.tenantId;
|
|
787
|
+
if (owner === void 0) return collection.get({ id });
|
|
788
|
+
return (await readOwnAndGlobal(owner, () => collection.list({
|
|
789
|
+
where: { id },
|
|
790
|
+
limit: 1
|
|
791
|
+
})))[0] ?? null;
|
|
792
|
+
}
|
|
793
|
+
/** `true` when the SKU belongs to an {@link Assembly}. */
|
|
794
|
+
async isAssembly(skuId) {
|
|
795
|
+
return (await this.resolveComponent(skuId)).kind === "assembly";
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Read one bill and resolve every line's component. Sub-assembly lines say
|
|
799
|
+
* whether they have an active bill of their own (`component.activeBom`);
|
|
800
|
+
* the caller opens the next level by passing that id back in.
|
|
801
|
+
*
|
|
802
|
+
* @throws {BomNotFoundError} when no bill has this id.
|
|
803
|
+
*/
|
|
804
|
+
async getBillStructure(bomId) {
|
|
805
|
+
const bom = isUuid(bomId) ? await this.boms.get({ id: bomId }) : null;
|
|
806
|
+
if (!bom) throw new BomNotFoundError(bomId);
|
|
807
|
+
const cache = /* @__PURE__ */ new Map();
|
|
808
|
+
const scope = { tenantId: bom.tenantId ?? null };
|
|
809
|
+
const lines = (await readOwnAndGlobal(scope.tenantId, () => this.lines.findByBom(bomId))).filter((line) => visibleTo(line, scope));
|
|
810
|
+
const resolved = [];
|
|
811
|
+
for (const line of lines) resolved.push({
|
|
812
|
+
line,
|
|
813
|
+
component: await this.resolveCached(line.componentSkuId, cache, scope)
|
|
814
|
+
});
|
|
815
|
+
return {
|
|
816
|
+
bom,
|
|
817
|
+
lines: resolved
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Find the path by which adding `componentSkuIds` to a bill for
|
|
822
|
+
* `productId` would make that product contain itself, or `null` when none
|
|
823
|
+
* would. The walk follows each sub-assembly's **active** bill only; a
|
|
824
|
+
* draft or superseded bill is not part of the structure. Pass the bill's
|
|
825
|
+
* `tenantId` so the walk reads that tenant's structure.
|
|
826
|
+
*/
|
|
827
|
+
async findCycle(productId, componentSkuIds, options = {}) {
|
|
828
|
+
const cache = /* @__PURE__ */ new Map();
|
|
829
|
+
const visited = /* @__PURE__ */ new Set();
|
|
830
|
+
const walk = async (skuIds, trail2) => {
|
|
831
|
+
for (const skuId of skuIds) {
|
|
832
|
+
const component = await this.resolveCached(skuId, cache, options);
|
|
833
|
+
const product = component.product;
|
|
834
|
+
if (!product?.id) continue;
|
|
835
|
+
if (product.id === productId) return [...trail2, product];
|
|
836
|
+
if (!component.activeBom || visited.has(product.id)) continue;
|
|
837
|
+
visited.add(product.id);
|
|
838
|
+
const billId = component.activeBom.id;
|
|
839
|
+
const lines = (options.tenantId === void 0 ? await this.lines.findByBom(billId) : await readOwnAndGlobal(options.tenantId, () => this.lines.findByBom(billId))).filter((line) => visibleTo(line, options));
|
|
840
|
+
const found = await walk(lines.map((line) => line.componentSkuId), [...trail2, product]);
|
|
841
|
+
if (found) return found;
|
|
842
|
+
}
|
|
843
|
+
return null;
|
|
844
|
+
};
|
|
845
|
+
const trail = await walk(componentSkuIds, []);
|
|
846
|
+
if (!trail) return null;
|
|
847
|
+
return [trail[trail.length - 1], ...trail].map((product) => ({
|
|
848
|
+
productId: product.id,
|
|
849
|
+
name: product.name || product.id
|
|
850
|
+
}));
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Refuse a bill line that would make its bill's product contain itself.
|
|
854
|
+
* A line on a draft bill is checked too, so a cycle is refused when it is
|
|
855
|
+
* written rather than when the bill is activated. A line whose bill does
|
|
856
|
+
* not exist is left to the caller.
|
|
857
|
+
*
|
|
858
|
+
* @throws {BomCycleError} naming the path.
|
|
859
|
+
*/
|
|
860
|
+
async assertLineAcyclic(line) {
|
|
861
|
+
if (!isUuid(line.bomId)) return;
|
|
862
|
+
const bom = await this.boms.get({ id: line.bomId });
|
|
863
|
+
if (!bom) return;
|
|
864
|
+
const path = await this.findCycle(bom.productId, [line.componentSkuId], { tenantId: bom.tenantId ?? null });
|
|
865
|
+
if (path) throw new BomCycleError(line.bomId, path);
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Refuse saving a bill as `active` when any of its lines would make its
|
|
869
|
+
* product contain itself. Bills in any other status are not part of the
|
|
870
|
+
* structure and pass.
|
|
871
|
+
*
|
|
872
|
+
* The lines checked are the bill's own and those of any stored bill with
|
|
873
|
+
* the same natural key (`productId`, `version`, and the bill's tenant, or
|
|
874
|
+
* the active tenant when the bill has none yet): saving a new
|
|
875
|
+
* instance with that key upserts onto the stored bill and activates its
|
|
876
|
+
* lines, so they are part of what the save makes active.
|
|
877
|
+
*
|
|
878
|
+
* @throws {BomCycleError} naming the path.
|
|
879
|
+
*/
|
|
880
|
+
async assertBillAcyclic(bom) {
|
|
881
|
+
if (bom.status !== "active") return;
|
|
882
|
+
const billIds = /* @__PURE__ */ new Set();
|
|
883
|
+
if (isUuid(bom.id)) billIds.add(bom.id);
|
|
884
|
+
const tenantId = bom.tenantId ?? getCurrentTenant()?.tenantId ?? null;
|
|
885
|
+
const sameKey = await readOwnAndGlobal(tenantId, () => this.boms.list({ where: {
|
|
886
|
+
productId: bom.productId,
|
|
887
|
+
version: bom.version
|
|
888
|
+
} }));
|
|
889
|
+
for (const stored of sameKey) if (stored.id && (stored.tenantId ?? null) === tenantId) billIds.add(stored.id);
|
|
890
|
+
const componentSkuIds = [];
|
|
891
|
+
for (const billId of billIds) for (const line of await readOwnAndGlobal(tenantId, () => this.lines.findByBom(billId))) if (visibleTo(line, { tenantId })) componentSkuIds.push(line.componentSkuId);
|
|
892
|
+
if (componentSkuIds.length === 0) return;
|
|
893
|
+
const path = await this.findCycle(bom.productId, componentSkuIds, { tenantId });
|
|
894
|
+
if (path) throw new BomCycleError(bom.id || [...billIds][0] || "", path);
|
|
895
|
+
}
|
|
896
|
+
resolveCached(skuId, cache, options) {
|
|
897
|
+
let pending = cache.get(skuId);
|
|
898
|
+
if (!pending) {
|
|
899
|
+
pending = this.resolveComponent(skuId, options);
|
|
900
|
+
cache.set(skuId, pending);
|
|
901
|
+
}
|
|
902
|
+
return pending;
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
function createAssemblyService(options) {
|
|
906
|
+
return AssemblyService.create(options);
|
|
907
|
+
}
|
|
908
|
+
//#endregion
|
|
909
|
+
//#region src/services/structure-walk.ts
|
|
910
|
+
var BomStructureCycleError = class extends BomCycleError {
|
|
911
|
+
name = "BomStructureCycleError";
|
|
912
|
+
constructor(bomId, path) {
|
|
913
|
+
super(bomId, path);
|
|
914
|
+
this.message = `The bill structure contains a cycle: ${path.map((entry) => entry.name).join(" → ")}`;
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
var UUID_PATTERN$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
918
|
+
function maxLevelFor(levels) {
|
|
919
|
+
if (levels === void 0) return 1;
|
|
920
|
+
if (levels === "all") return 32;
|
|
921
|
+
if (!Number.isInteger(levels) || levels < 1 || levels > 32) throw new Error(`levels must be a whole number from 1 to 32, or 'all' (got ${String(levels)})`);
|
|
922
|
+
return levels;
|
|
923
|
+
}
|
|
924
|
+
function isMultiLevel(levels) {
|
|
925
|
+
return maxLevelFor(levels) > 1;
|
|
926
|
+
}
|
|
927
|
+
var StructureWalk = class {
|
|
928
|
+
constructor(assemblies, top, levels) {
|
|
929
|
+
this.assemblies = assemblies;
|
|
930
|
+
this.top = top;
|
|
931
|
+
this.levels = levels;
|
|
932
|
+
this.maxLevel = maxLevelFor(levels);
|
|
933
|
+
}
|
|
934
|
+
assemblies;
|
|
935
|
+
top;
|
|
936
|
+
levels;
|
|
937
|
+
components = /* @__PURE__ */ new Map();
|
|
938
|
+
bills = /* @__PURE__ */ new Map();
|
|
939
|
+
lineCount = 0;
|
|
940
|
+
/** The deepest level whose lines the walk reads. */
|
|
941
|
+
maxLevel;
|
|
942
|
+
/**
|
|
943
|
+
* Resolve a component SKU, reading assemblies' active bills in the top
|
|
944
|
+
* bill's tenant so a shared assembly opens that tenant's bill.
|
|
945
|
+
*/
|
|
946
|
+
resolve(skuId) {
|
|
947
|
+
let pending = this.components.get(skuId);
|
|
948
|
+
if (!pending) {
|
|
949
|
+
pending = this.assemblies.resolveComponent(skuId, { tenantId: this.top.tenantId ?? null });
|
|
950
|
+
this.components.set(skuId, pending);
|
|
951
|
+
}
|
|
952
|
+
return pending;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* A bill's lines in the top bill's tenant (and global lines), counted
|
|
956
|
+
* against {@link MAX_EXPLOSION_LINES} every time the walk visits them (a
|
|
957
|
+
* shared sub-assembly counts once per use).
|
|
958
|
+
*/
|
|
959
|
+
async linesOf(bomId) {
|
|
960
|
+
let pending = this.bills.get(bomId);
|
|
961
|
+
if (!pending) {
|
|
962
|
+
const tenantId = this.top.tenantId ?? null;
|
|
963
|
+
pending = readOwnAndGlobal(tenantId, () => this.assemblies.lines.findByBom(bomId)).then((lines2) => lines2.filter((line) => isOwnOrGlobal(line, tenantId)));
|
|
964
|
+
this.bills.set(bomId, pending);
|
|
965
|
+
}
|
|
966
|
+
const lines = await pending;
|
|
967
|
+
this.lineCount += lines.length;
|
|
968
|
+
if (this.lineCount > 1e4) throw new BomExplosionLimitError(this.top.id, "lines");
|
|
969
|
+
return lines;
|
|
970
|
+
}
|
|
971
|
+
/** The path entry for the top bill. */
|
|
972
|
+
async root() {
|
|
973
|
+
const productId = this.top.productId;
|
|
974
|
+
let name = productId;
|
|
975
|
+
if (UUID_PATTERN$1.test(productId)) {
|
|
976
|
+
const owner = this.top.tenantId ?? null;
|
|
977
|
+
const [product] = await readOwnAndGlobal(owner, () => this.assemblies.products.list({
|
|
978
|
+
where: { id: productId },
|
|
979
|
+
limit: 1
|
|
980
|
+
}));
|
|
981
|
+
if (product?.name && isOwnOrGlobal(product, owner)) name = product.name;
|
|
982
|
+
}
|
|
983
|
+
return {
|
|
984
|
+
bomId: this.top.id,
|
|
985
|
+
productId,
|
|
986
|
+
name
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* The bill to open below a line at `level`, or `null` when the component
|
|
991
|
+
* is not a sub-assembly with an active bill or `levels` stops here.
|
|
992
|
+
*
|
|
993
|
+
* @throws {BomStructureCycleError} when the sub-assembly is already on
|
|
994
|
+
* `path` (it would contain itself).
|
|
995
|
+
* @throws {BomExplosionLimitError} when `levels: 'all'` would go deeper
|
|
996
|
+
* than {@link MAX_EXPLOSION_DEPTH}.
|
|
997
|
+
*/
|
|
998
|
+
open(component, level, path) {
|
|
999
|
+
const bom = component.activeBom;
|
|
1000
|
+
const product = component.product;
|
|
1001
|
+
if (!component.buildable || !bom?.id || !product?.id) return null;
|
|
1002
|
+
const name = product.name || product.id;
|
|
1003
|
+
const at = path.findIndex((entry) => entry.productId === product.id);
|
|
1004
|
+
if (at >= 0) throw new BomStructureCycleError(this.top.id, [...path.slice(at).map((entry) => ({
|
|
1005
|
+
productId: entry.productId,
|
|
1006
|
+
name: entry.name
|
|
1007
|
+
})), {
|
|
1008
|
+
productId: product.id,
|
|
1009
|
+
name
|
|
1010
|
+
}]);
|
|
1011
|
+
if (level >= this.maxLevel) {
|
|
1012
|
+
if (this.levels === "all") throw new BomExplosionLimitError(this.top.id, "depth");
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
return {
|
|
1016
|
+
bomId: bom.id,
|
|
1017
|
+
productId: product.id,
|
|
1018
|
+
name
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
//#endregion
|
|
211
1023
|
//#region src/services/BomService.ts
|
|
212
1024
|
var BomService = class BomService {
|
|
213
|
-
constructor(boms, lines, stockService, costResolver) {
|
|
1025
|
+
constructor(boms, lines, stockService, costResolver, routingSteps, operations, rateResolver, assemblies) {
|
|
214
1026
|
this.boms = boms;
|
|
215
1027
|
this.lines = lines;
|
|
216
1028
|
this.stockService = stockService;
|
|
217
1029
|
this.costResolver = costResolver;
|
|
1030
|
+
this.routingSteps = routingSteps;
|
|
1031
|
+
this.operations = operations;
|
|
1032
|
+
this.rateResolver = rateResolver;
|
|
1033
|
+
this.assemblies = assemblies;
|
|
218
1034
|
}
|
|
219
1035
|
boms;
|
|
220
1036
|
lines;
|
|
221
1037
|
stockService;
|
|
222
1038
|
costResolver;
|
|
1039
|
+
routingSteps;
|
|
1040
|
+
operations;
|
|
1041
|
+
rateResolver;
|
|
1042
|
+
assemblies;
|
|
223
1043
|
/** Factory — prefer {@link createBomService}. */
|
|
224
1044
|
static async create(options) {
|
|
225
1045
|
const stockService = options.stockService ?? await buildStockService$1(options);
|
|
226
|
-
const sharedDb = options.db ?? stockService.db;
|
|
227
|
-
const [boms, lines] = await Promise.all([
|
|
228
|
-
|
|
1046
|
+
const sharedDb = await resolveDatabase(options.stockService ? options.db ?? stockService.db : stockService.db);
|
|
1047
|
+
const [boms, lines, routingSteps, operations, assemblies] = await Promise.all([
|
|
1048
|
+
BillOfMaterialsCollection.create({ db: sharedDb }),
|
|
1049
|
+
BomLineCollection.create({ db: sharedDb }),
|
|
1050
|
+
RoutingStepCollection.create({ db: sharedDb }),
|
|
1051
|
+
OperationCollection.create({ db: sharedDb }),
|
|
1052
|
+
AssemblyService.create({ db: sharedDb })
|
|
1053
|
+
]);
|
|
1054
|
+
return new BomService(boms, lines, stockService, options.costResolver, routingSteps, operations, options.rateResolver, assemblies);
|
|
229
1055
|
}
|
|
230
1056
|
/**
|
|
231
1057
|
* Walk every {@link BomLine} for the given BOM, resolve each line's
|
|
@@ -238,10 +1064,19 @@ var BomService = class BomService {
|
|
|
238
1064
|
* set `costUnavailable: true` on their breakdown row; the aggregate
|
|
239
1065
|
* `hasMissingCosts` flag mirrors this so callers can surface a UI
|
|
240
1066
|
* warning.
|
|
1067
|
+
*
|
|
1068
|
+
* With `{ levels }` above 1, a sub-assembly line whose bill the walk opens
|
|
1069
|
+
* is costed from that bill instead of the resolver: its `unitCost` is the
|
|
1070
|
+
* bill's rolled-up material cost per unit, its `components` hold that
|
|
1071
|
+
* bill's lines, and a missing cost anywhere below marks it and
|
|
1072
|
+
* `hasMissingCosts` (its known part still counts, so the total is a lower
|
|
1073
|
+
* bound). Other lines, including sub-assemblies with no active bill, use
|
|
1074
|
+
* the resolver as before.
|
|
241
1075
|
*/
|
|
242
|
-
async computeMaterialCost(bomId) {
|
|
1076
|
+
async computeMaterialCost(bomId, options = {}) {
|
|
1077
|
+
if (isMultiLevel(options.levels)) return this.computeMultiLevelCost(bomId, options);
|
|
243
1078
|
const bom = await this.requireBom(bomId);
|
|
244
|
-
const lines = await this.
|
|
1079
|
+
const lines = await this.linesOf(bom);
|
|
245
1080
|
const lineBreakdown = [];
|
|
246
1081
|
let totalCost = 0;
|
|
247
1082
|
let hasMissingCosts = false;
|
|
@@ -273,6 +1108,66 @@ var BomService = class BomService {
|
|
|
273
1108
|
};
|
|
274
1109
|
}
|
|
275
1110
|
/**
|
|
1111
|
+
* Roll up the estimated labour to build one unit from the bill's routing:
|
|
1112
|
+
* total minutes, plus cost when a `rateResolver` was supplied (hourly rate
|
|
1113
|
+
* times minutes, per step). A bill with no routing returns an empty
|
|
1114
|
+
* estimate with zero totals, so callers can add it to the material cost
|
|
1115
|
+
* unconditionally. Retired operations still count: the routing names them.
|
|
1116
|
+
*
|
|
1117
|
+
* With `{ levels }` above 1, every sub-assembly the walk opens adds its own
|
|
1118
|
+
* bill's routing, multiplied by the units of it one unit needs (waste
|
|
1119
|
+
* included); see {@link LabourEstimate.subAssemblies}.
|
|
1120
|
+
*
|
|
1121
|
+
* Throws {@link BomNotFoundError} when the BOM does not exist.
|
|
1122
|
+
*/
|
|
1123
|
+
async computeLabourEstimate(bomId, options = {}) {
|
|
1124
|
+
if (isMultiLevel(options.levels)) return this.computeMultiLevelLabour(bomId, options);
|
|
1125
|
+
return this.labourFor(await this.requireBom(bomId));
|
|
1126
|
+
}
|
|
1127
|
+
/** One bill's own routing estimate, per unit of its product. */
|
|
1128
|
+
async labourFor(bom) {
|
|
1129
|
+
const bomId = bom.id;
|
|
1130
|
+
const owner = bom.tenantId ?? null;
|
|
1131
|
+
const routing = (await readOwnAndGlobal(owner, () => this.routingSteps.findByBom(bomId))).filter((step) => isOwnOrGlobal(step, owner));
|
|
1132
|
+
const steps = [];
|
|
1133
|
+
let totalMinutes = 0;
|
|
1134
|
+
let totalCost = 0;
|
|
1135
|
+
let hasMissingRates = false;
|
|
1136
|
+
for (const step of routing) {
|
|
1137
|
+
const [operation] = (await readOwnAndGlobal(owner, () => this.operations.list({
|
|
1138
|
+
where: { id: step.operationId },
|
|
1139
|
+
limit: 1
|
|
1140
|
+
}))).filter((row) => isOwnOrGlobal(row, owner));
|
|
1141
|
+
const minutes = Number(step.estimatedMinutes ?? 0);
|
|
1142
|
+
const rate = operation ? await this.resolveRate(operation) : null;
|
|
1143
|
+
const rateUnavailable = rate === null;
|
|
1144
|
+
const stepCost = rate === null ? 0 : minutes / 60 * rate;
|
|
1145
|
+
totalMinutes += minutes;
|
|
1146
|
+
if (rateUnavailable) hasMissingRates = true;
|
|
1147
|
+
else totalCost += stepCost;
|
|
1148
|
+
steps.push({
|
|
1149
|
+
stepId: step.id,
|
|
1150
|
+
sequence: step.sequence,
|
|
1151
|
+
operationId: step.operationId,
|
|
1152
|
+
operationCode: operation?.code ?? "",
|
|
1153
|
+
operationName: operation?.name ?? "",
|
|
1154
|
+
estimatedMinutes: minutes,
|
|
1155
|
+
hourlyRate: rate ?? 0,
|
|
1156
|
+
stepCost,
|
|
1157
|
+
rateUnavailable
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
return {
|
|
1161
|
+
bomId,
|
|
1162
|
+
hasRouting: steps.length > 0,
|
|
1163
|
+
totalMinutes,
|
|
1164
|
+
totalCost,
|
|
1165
|
+
currency: bom.currency || "USD",
|
|
1166
|
+
steps,
|
|
1167
|
+
hasMissingRates
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
276
1171
|
* Return a "shopping list" of materials needed to produce `qty` units of
|
|
277
1172
|
* the parent product against the given BOM. Lines that reference the
|
|
278
1173
|
* same component SKU are summed so each `componentSkuId` appears once
|
|
@@ -282,13 +1177,17 @@ var BomService = class BomService {
|
|
|
282
1177
|
* {@link ProductionService.consumeMaterials} when you're ready to write
|
|
283
1178
|
* stock movements.
|
|
284
1179
|
*
|
|
1180
|
+
* With `{ levels }` above 1, sub-assemblies with an active bill are
|
|
1181
|
+
* replaced by what their bills need, waste compounding down the levels;
|
|
1182
|
+
* this is {@link explode}'s `totals`.
|
|
1183
|
+
*
|
|
285
1184
|
* Throws {@link BomNotFoundError} when the BOM does not exist; throws a
|
|
286
1185
|
* plain `Error` when `qty` is not a positive finite number.
|
|
287
1186
|
*/
|
|
288
|
-
async explodeRequirements(bomId, qty) {
|
|
1187
|
+
async explodeRequirements(bomId, qty, options = {}) {
|
|
1188
|
+
if (isMultiLevel(options.levels)) return (await this.explode(bomId, qty, options)).totals;
|
|
289
1189
|
assertPositiveQty$1(qty, "explodeRequirements");
|
|
290
|
-
await this.requireBom(bomId);
|
|
291
|
-
const lines = await this.lines.findByBom(bomId);
|
|
1190
|
+
const lines = await this.linesOf(await this.requireBom(bomId));
|
|
292
1191
|
const aggregate = /* @__PURE__ */ new Map();
|
|
293
1192
|
for (const line of lines) {
|
|
294
1193
|
const effectiveQty = line.effectiveQtyPerUnit() * qty;
|
|
@@ -314,9 +1213,25 @@ var BomService = class BomService {
|
|
|
314
1213
|
* "where do we pull from?" is left to the caller of
|
|
315
1214
|
* {@link ProductionService.consumeMaterials}).
|
|
316
1215
|
*
|
|
1216
|
+
* With `{ levels }` above 1 the check is {@link planRequirements}: a
|
|
1217
|
+
* sub-assembly that is short and has an active bill is opened for its
|
|
1218
|
+
* shortfall, and each shortage is reported on the line where it occurs,
|
|
1219
|
+
* with its `level` and `path`. Stock is allocated as the walk goes, so a
|
|
1220
|
+
* component used in two places is not counted twice.
|
|
1221
|
+
*
|
|
317
1222
|
* Throws {@link BomNotFoundError} when the BOM does not exist.
|
|
318
1223
|
*/
|
|
319
|
-
async canProduce(bomId, qty) {
|
|
1224
|
+
async canProduce(bomId, qty, options = {}) {
|
|
1225
|
+
if (isMultiLevel(options.levels)) {
|
|
1226
|
+
const plan = await this.planRequirements(bomId, qty, options);
|
|
1227
|
+
return plan.ok ? {
|
|
1228
|
+
ok: true,
|
|
1229
|
+
shortages: []
|
|
1230
|
+
} : {
|
|
1231
|
+
ok: false,
|
|
1232
|
+
shortages: plan.shortages
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
320
1235
|
const requirements = await this.explodeRequirements(bomId, qty);
|
|
321
1236
|
const availabilities = await Promise.all(requirements.map(async (requirement) => ({
|
|
322
1237
|
requirement,
|
|
@@ -338,16 +1253,262 @@ var BomService = class BomService {
|
|
|
338
1253
|
};
|
|
339
1254
|
}
|
|
340
1255
|
/**
|
|
1256
|
+
* Walk the bill for `qty` units and return every line, depth first, with
|
|
1257
|
+
* its level, the path of bills above it and its quantity for the run, plus
|
|
1258
|
+
* the rolled-up `totals`. Gross requirements: stock is not read.
|
|
1259
|
+
*
|
|
1260
|
+
* `levels` (default `1`) says how far to open sub-assemblies; see
|
|
1261
|
+
* {@link ExplosionLevels}. Waste compounds: a sub-assembly line's quantity
|
|
1262
|
+
* includes its own waste, and its bill's lines are multiplied by it.
|
|
1263
|
+
*
|
|
1264
|
+
* Throws {@link BomNotFoundError}, `BomStructureCycleError` when stored
|
|
1265
|
+
* data already has a product containing itself, `BomExplosionLimitError`
|
|
1266
|
+
* past the depth or line limits, and a plain `Error` for a bad `qty` or
|
|
1267
|
+
* `levels`.
|
|
1268
|
+
*/
|
|
1269
|
+
async explode(bomId, qty, options = {}) {
|
|
1270
|
+
assertPositiveQty$1(qty, "explode");
|
|
1271
|
+
const levels = options.levels ?? 1;
|
|
1272
|
+
const top = await this.requireBom(bomId);
|
|
1273
|
+
const walk = new StructureWalk(this.assemblies, top, levels);
|
|
1274
|
+
const lines = [];
|
|
1275
|
+
const visit = async (billId, level, path, parentQty) => {
|
|
1276
|
+
for (const line of await walk.linesOf(billId)) {
|
|
1277
|
+
const component = await walk.resolve(line.componentSkuId);
|
|
1278
|
+
const sub = walk.open(component, level, path);
|
|
1279
|
+
const exploded = explodedLine(line, component, level, path, parentQty);
|
|
1280
|
+
exploded.expanded = sub !== null;
|
|
1281
|
+
lines.push(exploded);
|
|
1282
|
+
if (sub) await visit(sub.bomId, level + 1, [...path, sub], exploded.totalQty);
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
await visit(bomId, 1, [await walk.root()], qty);
|
|
1286
|
+
return {
|
|
1287
|
+
bomId,
|
|
1288
|
+
productId: top.productId,
|
|
1289
|
+
qty,
|
|
1290
|
+
levels,
|
|
1291
|
+
lines,
|
|
1292
|
+
totals: sumRequirements(lines.filter((line) => !line.expanded))
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Net the walk against stock: for each line, the available stock (all
|
|
1297
|
+
* locations) not yet allocated to an earlier line and how much is short.
|
|
1298
|
+
* A sub-assembly that is short and has an active bill is opened for the
|
|
1299
|
+
* shortfall only, so its lines say what building that shortfall takes;
|
|
1300
|
+
* one covered by stock is not opened. Lines that are short and cannot be
|
|
1301
|
+
* opened are the `shortages`, each at its own level.
|
|
1302
|
+
*
|
|
1303
|
+
* These are facts, not a decision: the application decides whether a
|
|
1304
|
+
* short sub-assembly is built or bought. Does not mutate stock.
|
|
1305
|
+
*
|
|
1306
|
+
* Throws as {@link explode} does.
|
|
1307
|
+
*/
|
|
1308
|
+
async planRequirements(bomId, qty, options = {}) {
|
|
1309
|
+
assertPositiveQty$1(qty, "planRequirements");
|
|
1310
|
+
const levels = options.levels ?? 1;
|
|
1311
|
+
const top = await this.requireBom(bomId);
|
|
1312
|
+
const walk = new StructureWalk(this.assemblies, top, levels);
|
|
1313
|
+
const lines = [];
|
|
1314
|
+
const shortages = [];
|
|
1315
|
+
const unallocated = /* @__PURE__ */ new Map();
|
|
1316
|
+
const tenantId = top.tenantId ?? null;
|
|
1317
|
+
const availableFor = (skuId) => {
|
|
1318
|
+
let pending = unallocated.get(skuId);
|
|
1319
|
+
if (!pending) {
|
|
1320
|
+
pending = this.availableInTenant(skuId, tenantId);
|
|
1321
|
+
unallocated.set(skuId, pending);
|
|
1322
|
+
}
|
|
1323
|
+
return pending;
|
|
1324
|
+
};
|
|
1325
|
+
const visit = async (billId, level, path, parentQty) => {
|
|
1326
|
+
for (const line of await walk.linesOf(billId)) {
|
|
1327
|
+
const component = await walk.resolve(line.componentSkuId);
|
|
1328
|
+
const base = explodedLine(line, component, level, path, parentQty);
|
|
1329
|
+
const available = await availableFor(line.componentSkuId);
|
|
1330
|
+
const allocated = Math.min(available, base.totalQty);
|
|
1331
|
+
unallocated.set(line.componentSkuId, Promise.resolve(available - allocated));
|
|
1332
|
+
const short = Math.max(0, roundQuantity(base.totalQty - allocated));
|
|
1333
|
+
const sub = short > 0 ? walk.open(component, level, path) : null;
|
|
1334
|
+
lines.push({
|
|
1335
|
+
...base,
|
|
1336
|
+
expanded: sub !== null,
|
|
1337
|
+
available,
|
|
1338
|
+
short
|
|
1339
|
+
});
|
|
1340
|
+
if (sub) await visit(sub.bomId, level + 1, [...path, sub], short);
|
|
1341
|
+
else if (short > 0) shortages.push({
|
|
1342
|
+
componentSkuId: line.componentSkuId,
|
|
1343
|
+
requested: base.totalQty,
|
|
1344
|
+
available,
|
|
1345
|
+
level,
|
|
1346
|
+
path,
|
|
1347
|
+
uom: base.uom,
|
|
1348
|
+
buildable: component.buildable
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
await visit(bomId, 1, [await walk.root()], qty);
|
|
1353
|
+
return {
|
|
1354
|
+
bomId,
|
|
1355
|
+
productId: top.productId,
|
|
1356
|
+
qty,
|
|
1357
|
+
levels,
|
|
1358
|
+
lines,
|
|
1359
|
+
shortages,
|
|
1360
|
+
ok: shortages.length === 0
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
341
1364
|
* Fetch a BOM by id or throw {@link BomNotFoundError}. Centralises the
|
|
342
1365
|
* "missing BOM" error path so callers don't have to repeat the check.
|
|
343
1366
|
*/
|
|
1367
|
+
/**
|
|
1368
|
+
* A bill's lines: its own tenant's and global ones, whatever tenant
|
|
1369
|
+
* context is active (a context alone would hide a global bill's lines).
|
|
1370
|
+
*/
|
|
1371
|
+
async linesOf(bom) {
|
|
1372
|
+
const owner = bom.tenantId ?? null;
|
|
1373
|
+
return (await readOwnAndGlobal(owner, () => this.lines.findByBom(bom.id))).filter((line) => isOwnOrGlobal(line, owner));
|
|
1374
|
+
}
|
|
344
1375
|
async requireBom(bomId) {
|
|
345
1376
|
if (!bomId) throw new BomNotFoundError(bomId);
|
|
346
|
-
const
|
|
1377
|
+
const tenantId = getCurrentTenant()?.tenantId;
|
|
1378
|
+
const bom = tenantId ? (await readOwnAndGlobal(tenantId, () => this.boms.list({
|
|
1379
|
+
where: { id: bomId },
|
|
1380
|
+
limit: 1
|
|
1381
|
+
}))).find((row) => isOwnOrGlobal(row, tenantId)) ?? null : await this.boms.get(bomId);
|
|
347
1382
|
if (!bom) throw new BomNotFoundError(bomId);
|
|
348
1383
|
return bom;
|
|
349
1384
|
}
|
|
350
1385
|
/**
|
|
1386
|
+
* Available stock of a SKU across locations, counting only the top bill's
|
|
1387
|
+
* tenant's rows and global ones, whatever tenant context is active (the
|
|
1388
|
+
* walk reads its structure the same way).
|
|
1389
|
+
*/
|
|
1390
|
+
async availableInTenant(skuId, tenantId) {
|
|
1391
|
+
const rows = await readOwnAndGlobal(tenantId, () => this.stockService.levels.list({ where: {
|
|
1392
|
+
skuId,
|
|
1393
|
+
state: "available"
|
|
1394
|
+
} }));
|
|
1395
|
+
let total = 0;
|
|
1396
|
+
for (const row of rows) if (isOwnOrGlobal(row, tenantId)) total += Number(row.qty ?? 0);
|
|
1397
|
+
return Math.max(0, total);
|
|
1398
|
+
}
|
|
1399
|
+
/** {@link computeMaterialCost} with `levels` above 1. */
|
|
1400
|
+
async computeMultiLevelCost(bomId, options) {
|
|
1401
|
+
const top = await this.requireBom(bomId);
|
|
1402
|
+
const currency = top.currency || "USD";
|
|
1403
|
+
const walk = new StructureWalk(this.assemblies, top, options.levels ?? 1);
|
|
1404
|
+
const rollUp = async (billId, level, path) => {
|
|
1405
|
+
const lineBreakdown = [];
|
|
1406
|
+
let totalCost = 0;
|
|
1407
|
+
let hasMissingCosts = false;
|
|
1408
|
+
for (const line of await walk.linesOf(billId)) {
|
|
1409
|
+
const component = await walk.resolve(line.componentSkuId);
|
|
1410
|
+
const sub = walk.open(component, level, path);
|
|
1411
|
+
const effectiveQty = line.effectiveQtyPerUnit();
|
|
1412
|
+
const entry = {
|
|
1413
|
+
componentSkuId: line.componentSkuId,
|
|
1414
|
+
qtyPerUnit: Number(line.qtyPerUnit ?? 0),
|
|
1415
|
+
wastePercent: Number(line.wastePercent ?? 0),
|
|
1416
|
+
effectiveQty,
|
|
1417
|
+
unitCost: 0,
|
|
1418
|
+
lineCost: 0,
|
|
1419
|
+
uom: line.uom,
|
|
1420
|
+
costUnavailable: false
|
|
1421
|
+
};
|
|
1422
|
+
const sameCurrency = (component.activeBom?.currency || "USD") === currency;
|
|
1423
|
+
if (sub && sameCurrency) {
|
|
1424
|
+
const inner = await rollUp(sub.bomId, level + 1, [...path, sub]);
|
|
1425
|
+
entry.unitCost = inner.totalCost;
|
|
1426
|
+
entry.lineCost = inner.totalCost * effectiveQty;
|
|
1427
|
+
entry.costUnavailable = inner.hasMissingCosts;
|
|
1428
|
+
entry.subBomId = sub.bomId;
|
|
1429
|
+
entry.components = inner.lineBreakdown;
|
|
1430
|
+
totalCost += entry.lineCost;
|
|
1431
|
+
} else {
|
|
1432
|
+
const resolved = await this.resolveCost(line.componentSkuId);
|
|
1433
|
+
entry.unitCost = resolved ?? 0;
|
|
1434
|
+
entry.lineCost = entry.unitCost * effectiveQty;
|
|
1435
|
+
entry.costUnavailable = resolved === null;
|
|
1436
|
+
if (!entry.costUnavailable) totalCost += entry.lineCost;
|
|
1437
|
+
}
|
|
1438
|
+
if (entry.costUnavailable) hasMissingCosts = true;
|
|
1439
|
+
lineBreakdown.push(entry);
|
|
1440
|
+
}
|
|
1441
|
+
return {
|
|
1442
|
+
totalCost,
|
|
1443
|
+
lineBreakdown,
|
|
1444
|
+
hasMissingCosts
|
|
1445
|
+
};
|
|
1446
|
+
};
|
|
1447
|
+
return {
|
|
1448
|
+
bomId,
|
|
1449
|
+
currency,
|
|
1450
|
+
...await rollUp(bomId, 1, [await walk.root()])
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
/** {@link computeLabourEstimate} with `levels` above 1. */
|
|
1454
|
+
async computeMultiLevelLabour(bomId, options) {
|
|
1455
|
+
const top = await this.requireBom(bomId);
|
|
1456
|
+
const walk = new StructureWalk(this.assemblies, top, options.levels ?? 1);
|
|
1457
|
+
const own = await this.labourFor(top);
|
|
1458
|
+
const estimates = /* @__PURE__ */ new Map();
|
|
1459
|
+
const subAssemblies = [];
|
|
1460
|
+
const visit = async (billId, level, path, units) => {
|
|
1461
|
+
for (const line of await walk.linesOf(billId)) {
|
|
1462
|
+
const component = await walk.resolve(line.componentSkuId);
|
|
1463
|
+
const sub = walk.open(component, level, path);
|
|
1464
|
+
if (!sub || !component.activeBom) continue;
|
|
1465
|
+
const subBill = component.activeBom;
|
|
1466
|
+
let estimate = estimates.get(sub.bomId);
|
|
1467
|
+
if (!estimate) {
|
|
1468
|
+
estimate = this.labourFor(subBill);
|
|
1469
|
+
estimates.set(sub.bomId, estimate);
|
|
1470
|
+
}
|
|
1471
|
+
const unitsPerUnit = units * line.effectiveQtyPerUnit();
|
|
1472
|
+
subAssemblies.push({
|
|
1473
|
+
lineId: line.id,
|
|
1474
|
+
componentSkuId: line.componentSkuId,
|
|
1475
|
+
name: sub.name,
|
|
1476
|
+
level,
|
|
1477
|
+
path,
|
|
1478
|
+
unitsPerUnit,
|
|
1479
|
+
estimate: await estimate
|
|
1480
|
+
});
|
|
1481
|
+
await visit(sub.bomId, level + 1, [...path, sub], unitsPerUnit);
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
await visit(bomId, 1, [await walk.root()], 1);
|
|
1485
|
+
let totalMinutes = own.totalMinutes;
|
|
1486
|
+
let totalCost = own.totalCost;
|
|
1487
|
+
let hasMissingRates = own.hasMissingRates;
|
|
1488
|
+
for (const sub of subAssemblies) {
|
|
1489
|
+
totalMinutes += sub.unitsPerUnit * sub.estimate.totalMinutes;
|
|
1490
|
+
if (sub.estimate.currency === own.currency) {
|
|
1491
|
+
totalCost += sub.unitsPerUnit * sub.estimate.totalCost;
|
|
1492
|
+
if (sub.estimate.hasMissingRates) hasMissingRates = true;
|
|
1493
|
+
} else if (sub.estimate.hasRouting) hasMissingRates = true;
|
|
1494
|
+
}
|
|
1495
|
+
return {
|
|
1496
|
+
...own,
|
|
1497
|
+
hasRouting: own.hasRouting || subAssemblies.some((sub) => sub.estimate.hasRouting),
|
|
1498
|
+
totalMinutes,
|
|
1499
|
+
totalCost,
|
|
1500
|
+
hasMissingRates,
|
|
1501
|
+
subAssemblies
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
/** Run the {@link OperationRateResolver}; `null` when there is no rate. */
|
|
1505
|
+
async resolveRate(operation) {
|
|
1506
|
+
if (!this.rateResolver) return null;
|
|
1507
|
+
const value = await this.rateResolver(operation);
|
|
1508
|
+
if (value === null || value === void 0 || !Number.isFinite(value)) return null;
|
|
1509
|
+
return Number(value);
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
351
1512
|
* Run the supplied {@link ComponentCostResolver} for the given component
|
|
352
1513
|
* SKU. Returns `null` when no resolver is registered or when the
|
|
353
1514
|
* resolver returns `null` / `undefined`.
|
|
@@ -360,6 +1521,40 @@ var BomService = class BomService {
|
|
|
360
1521
|
return Number(value);
|
|
361
1522
|
}
|
|
362
1523
|
};
|
|
1524
|
+
function explodedLine(line, component, level, path, parentQty) {
|
|
1525
|
+
const effectiveQtyPerUnit = line.effectiveQtyPerUnit();
|
|
1526
|
+
return {
|
|
1527
|
+
lineId: line.id,
|
|
1528
|
+
bomId: line.bomId,
|
|
1529
|
+
level,
|
|
1530
|
+
path,
|
|
1531
|
+
componentSkuId: line.componentSkuId,
|
|
1532
|
+
kind: component.kind,
|
|
1533
|
+
name: component.product?.name ?? "",
|
|
1534
|
+
skuCode: component.sku?.code ?? "",
|
|
1535
|
+
qtyPerUnit: Number(line.qtyPerUnit ?? 0),
|
|
1536
|
+
wastePercent: Number(line.wastePercent ?? 0),
|
|
1537
|
+
effectiveQtyPerUnit,
|
|
1538
|
+
uom: line.uom,
|
|
1539
|
+
totalQty: parentQty * effectiveQtyPerUnit,
|
|
1540
|
+
buildable: component.buildable,
|
|
1541
|
+
subBomId: component.activeBom?.id ?? null,
|
|
1542
|
+
expanded: false
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
function sumRequirements(lines) {
|
|
1546
|
+
const totals = /* @__PURE__ */ new Map();
|
|
1547
|
+
for (const line of lines) {
|
|
1548
|
+
const existing = totals.get(line.componentSkuId);
|
|
1549
|
+
if (existing) existing.totalQty += line.totalQty;
|
|
1550
|
+
else totals.set(line.componentSkuId, {
|
|
1551
|
+
componentSkuId: line.componentSkuId,
|
|
1552
|
+
totalQty: line.totalQty,
|
|
1553
|
+
uom: line.uom
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
return Array.from(totals.values());
|
|
1557
|
+
}
|
|
363
1558
|
function assertPositiveQty$1(qty, op) {
|
|
364
1559
|
if (!Number.isFinite(qty) || qty <= 0) throw new Error(`${op}: qty must be a positive finite number (got ${qty})`);
|
|
365
1560
|
}
|
|
@@ -478,13 +1673,14 @@ var ProductionService = class ProductionService {
|
|
|
478
1673
|
const orderId = order.id ?? "";
|
|
479
1674
|
if (!orderId) throw new Error("consumeMaterials: order.id is required for source attribution");
|
|
480
1675
|
const bom = await this.resolveBom(order);
|
|
1676
|
+
const owner = bom.tenantId ?? null;
|
|
481
1677
|
return {
|
|
482
1678
|
orderId,
|
|
483
|
-
lines: await this.lines.findByBom(bom.id),
|
|
1679
|
+
lines: (await readOwnAndGlobal(owner, () => this.lines.findByBom(bom.id))).filter((line) => isOwnOrGlobal(line, owner)),
|
|
484
1680
|
runQty: options.qty,
|
|
485
1681
|
locationId: options.locationId,
|
|
486
1682
|
mutationOptions: {
|
|
487
|
-
sourceType: "ProductionOrder",
|
|
1683
|
+
sourceType: options.sourceType ?? "ProductionOrder",
|
|
488
1684
|
sourceId: orderId,
|
|
489
1685
|
reasonCode: options.reasonCode ?? "production_consume",
|
|
490
1686
|
note: options.note
|
|
@@ -548,8 +1744,13 @@ var ProductionService = class ProductionService {
|
|
|
548
1744
|
*/
|
|
549
1745
|
async resolveBom(order) {
|
|
550
1746
|
if (order.bomId) {
|
|
551
|
-
const
|
|
552
|
-
|
|
1747
|
+
const bomId = order.bomId;
|
|
1748
|
+
const tenantId = getCurrentTenant()?.tenantId;
|
|
1749
|
+
const bom = tenantId ? (await readOwnAndGlobal(tenantId, () => this.boms.list({
|
|
1750
|
+
where: { id: bomId },
|
|
1751
|
+
limit: 1
|
|
1752
|
+
}))).find((row) => isOwnOrGlobal(row, tenantId)) ?? null : await this.boms.get(bomId);
|
|
1753
|
+
if (!bom) throw new BomNotFoundError(bomId);
|
|
553
1754
|
return bom;
|
|
554
1755
|
}
|
|
555
1756
|
const productId = order.productId ?? "";
|
|
@@ -571,7 +1772,7 @@ function prepareProduce(order, options) {
|
|
|
571
1772
|
qty: options.qty,
|
|
572
1773
|
locationId: options.locationId,
|
|
573
1774
|
mutationOptions: {
|
|
574
|
-
sourceType: "ProductionOrder",
|
|
1775
|
+
sourceType: options.sourceType ?? "ProductionOrder",
|
|
575
1776
|
sourceId: orderId,
|
|
576
1777
|
reasonCode: options.reasonCode ?? "production_produce",
|
|
577
1778
|
note: options.note
|
|
@@ -693,6 +1894,423 @@ async function buildProductionService(options) {
|
|
|
693
1894
|
return createProductionService({ db: options.db });
|
|
694
1895
|
}
|
|
695
1896
|
//#endregion
|
|
696
|
-
|
|
1897
|
+
//#region src/services/OperationService.ts
|
|
1898
|
+
var OperationService = class OperationService {
|
|
1899
|
+
constructor(operations) {
|
|
1900
|
+
this.operations = operations;
|
|
1901
|
+
}
|
|
1902
|
+
operations;
|
|
1903
|
+
/** Factory — prefer {@link createOperationService}. */
|
|
1904
|
+
static async create(options) {
|
|
1905
|
+
return new OperationService(await OperationCollection.create({ db: options.db }));
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Add an operation. The code is trimmed and must be unique per tenant;
|
|
1909
|
+
* throws {@link DuplicateOperationCodeError} when it is taken (retired
|
|
1910
|
+
* operations keep their code).
|
|
1911
|
+
*/
|
|
1912
|
+
async define(input) {
|
|
1913
|
+
const code = input.code?.trim() ?? "";
|
|
1914
|
+
const name = input.name?.trim() ?? "";
|
|
1915
|
+
if (!code) throw new InvalidOperationInputError("Operation code is required.");
|
|
1916
|
+
if (!name) throw new InvalidOperationInputError("Operation name is required.");
|
|
1917
|
+
if (await this.operations.findByCode(code)) throw new DuplicateOperationCodeError(code);
|
|
1918
|
+
try {
|
|
1919
|
+
return await this.operations.create({
|
|
1920
|
+
code,
|
|
1921
|
+
name,
|
|
1922
|
+
category: input.category?.trim() ?? "",
|
|
1923
|
+
requiredQualificationId: input.requiredQualificationId?.trim() || null,
|
|
1924
|
+
isActive: true,
|
|
1925
|
+
_insertOnly: true
|
|
1926
|
+
});
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
if (await this.operations.findByCode(code)) throw new DuplicateOperationCodeError(code);
|
|
1929
|
+
throw error;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
/** Fetch an operation or throw {@link OperationNotFoundError}. */
|
|
1933
|
+
async get(id) {
|
|
1934
|
+
const operation = id ? await this.operations.get(id) : null;
|
|
1935
|
+
if (!operation) throw new OperationNotFoundError(id);
|
|
1936
|
+
return operation;
|
|
1937
|
+
}
|
|
1938
|
+
/** List operations; retired ones only with `includeRetired`. */
|
|
1939
|
+
async list(options = {}) {
|
|
1940
|
+
return this.operations.listOperations(options);
|
|
1941
|
+
}
|
|
1942
|
+
/** Change the display name. The code does not change. */
|
|
1943
|
+
async rename(id, name) {
|
|
1944
|
+
const next = name?.trim() ?? "";
|
|
1945
|
+
if (!next) throw new InvalidOperationInputError("Operation name is required.");
|
|
1946
|
+
const operation = await this.get(id);
|
|
1947
|
+
operation.name = next;
|
|
1948
|
+
await operation.save();
|
|
1949
|
+
return operation;
|
|
1950
|
+
}
|
|
1951
|
+
/** Change category and/or required qualification; an empty string clears. */
|
|
1952
|
+
async update(id, changes) {
|
|
1953
|
+
const operation = await this.get(id);
|
|
1954
|
+
if (changes.category !== void 0) operation.category = changes.category.trim();
|
|
1955
|
+
if (changes.requiredQualificationId !== void 0) operation.requiredQualificationId = changes.requiredQualificationId.trim() || null;
|
|
1956
|
+
await operation.save();
|
|
1957
|
+
return operation;
|
|
1958
|
+
}
|
|
1959
|
+
/** Retire: stays on history and routings, leaves pickers. Idempotent. */
|
|
1960
|
+
async retire(id) {
|
|
1961
|
+
return this.setActive(id, false);
|
|
1962
|
+
}
|
|
1963
|
+
/** Reinstate a retired operation. Idempotent. */
|
|
1964
|
+
async reinstate(id) {
|
|
1965
|
+
return this.setActive(id, true);
|
|
1966
|
+
}
|
|
1967
|
+
async setActive(id, isActive) {
|
|
1968
|
+
const operation = await this.get(id);
|
|
1969
|
+
if (operation.isActive !== isActive) {
|
|
1970
|
+
operation.isActive = isActive;
|
|
1971
|
+
await operation.save();
|
|
1972
|
+
}
|
|
1973
|
+
return operation;
|
|
1974
|
+
}
|
|
1975
|
+
};
|
|
1976
|
+
async function createOperationService(options) {
|
|
1977
|
+
return OperationService.create(options);
|
|
1978
|
+
}
|
|
1979
|
+
//#endregion
|
|
1980
|
+
//#region src/services/ProductionRunService.ts
|
|
1981
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1982
|
+
var OPEN_STATUSES = /* @__PURE__ */ new Set(["planned", "in_progress"]);
|
|
1983
|
+
var ProductionRunService = class ProductionRunService {
|
|
1984
|
+
constructor(stockService, runs, completions, boms) {
|
|
1985
|
+
this.stockService = stockService;
|
|
1986
|
+
this.runs = runs;
|
|
1987
|
+
this.completions = completions;
|
|
1988
|
+
this.boms = boms;
|
|
1989
|
+
}
|
|
1990
|
+
stockService;
|
|
1991
|
+
runs;
|
|
1992
|
+
completions;
|
|
1993
|
+
boms;
|
|
1994
|
+
/** Factory — prefer {@link createProductionRunService}. */
|
|
1995
|
+
static async create(options) {
|
|
1996
|
+
let stockService = options.stockService;
|
|
1997
|
+
if (!stockService) {
|
|
1998
|
+
if (!options.db) throw new Error("ProductionRunService.create: either `db` or `stockService` is required");
|
|
1999
|
+
stockService = await createStockService({ db: await resolveDatabase(options.db) });
|
|
2000
|
+
}
|
|
2001
|
+
const db = stockService.db;
|
|
2002
|
+
if (typeof db.transaction !== "function") throw new Error("ProductionRunService requires a database adapter with transaction() support.");
|
|
2003
|
+
const [runs, completions, boms] = await Promise.all([
|
|
2004
|
+
ProductionRunCollection.create({ db }),
|
|
2005
|
+
ProductionRunCompletionCollection.create({ db }),
|
|
2006
|
+
BillOfMaterialsCollection.create({ db })
|
|
2007
|
+
]);
|
|
2008
|
+
return new ProductionRunService(stockService, runs, completions, boms);
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Create a `planned` run of `targetQty` units, pinned to `bomId`, or to the
|
|
2012
|
+
* active bill of `productId`.
|
|
2013
|
+
*
|
|
2014
|
+
* @throws {InvalidProductionRunInputError} for a target that is not a
|
|
2015
|
+
* positive number, or when neither `bomId` nor `productId` is given.
|
|
2016
|
+
* @throws {BomNotFoundError} when `bomId` does not exist.
|
|
2017
|
+
* @throws {NoActiveBomForProductError} when `productId` has no active bill.
|
|
2018
|
+
*/
|
|
2019
|
+
async createRun(input) {
|
|
2020
|
+
const targetQty = positive(input.targetQty, "targetQty");
|
|
2021
|
+
const tenantId = getCurrentTenant()?.tenantId ?? null;
|
|
2022
|
+
let bom = null;
|
|
2023
|
+
if (input.bomId) {
|
|
2024
|
+
const bomId = input.bomId;
|
|
2025
|
+
bom = (UUID_PATTERN.test(bomId) ? await readOwnAndGlobal(tenantId, () => this.boms.list({
|
|
2026
|
+
where: { id: bomId },
|
|
2027
|
+
limit: 1
|
|
2028
|
+
})) : []).find((row) => isOwnOrGlobal(row, tenantId)) ?? null;
|
|
2029
|
+
if (!bom?.id) throw new BomNotFoundError(bomId);
|
|
2030
|
+
} else if (input.productId) {
|
|
2031
|
+
const productId = input.productId;
|
|
2032
|
+
const active = (await readOwnAndGlobal(tenantId, () => this.boms.list({
|
|
2033
|
+
where: {
|
|
2034
|
+
productId,
|
|
2035
|
+
status: "active"
|
|
2036
|
+
},
|
|
2037
|
+
orderBy: "version DESC"
|
|
2038
|
+
}))).filter((candidate) => isOwnOrGlobal(candidate, tenantId));
|
|
2039
|
+
bom = active.find((candidate) => (candidate.tenantId ?? null) === tenantId) ?? active[0] ?? null;
|
|
2040
|
+
if (!bom?.id) throw new NoActiveBomForProductError(input.productId);
|
|
2041
|
+
} else throw new InvalidProductionRunInputError("A production run needs a bomId or a productId.");
|
|
2042
|
+
return this.runs.create({
|
|
2043
|
+
tenantId: bom.tenantId ?? tenantId,
|
|
2044
|
+
bomId: bom.id,
|
|
2045
|
+
targetQty,
|
|
2046
|
+
completedQty: 0,
|
|
2047
|
+
status: "planned"
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
/** The run, or `null` when no run has this id. */
|
|
2051
|
+
async get(runId) {
|
|
2052
|
+
if (!UUID_PATTERN.test(runId)) return null;
|
|
2053
|
+
return this.runs.get({ id: runId });
|
|
2054
|
+
}
|
|
2055
|
+
/** A run's completions in the order they happened. */
|
|
2056
|
+
async listCompletions(runId) {
|
|
2057
|
+
if (!UUID_PATTERN.test(runId)) return [];
|
|
2058
|
+
return this.completions.findByRun(runId);
|
|
2059
|
+
}
|
|
2060
|
+
/** Mark a `planned` run `in_progress`. */
|
|
2061
|
+
async start(runId) {
|
|
2062
|
+
return this.change(runId, "start", (run) => {
|
|
2063
|
+
if (run.status !== "planned") throw new ProductionRunStateError(runId, run.status, "start");
|
|
2064
|
+
run.status = "in_progress";
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
/** Mark an open run `done`, short of its target if work stopped early. */
|
|
2068
|
+
async finish(runId) {
|
|
2069
|
+
return this.change(runId, "finish", (run) => {
|
|
2070
|
+
run.status = "done";
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
/** Cancel an open run. Completions already reported stay. */
|
|
2074
|
+
async cancel(runId) {
|
|
2075
|
+
return this.change(runId, "cancel", (run) => {
|
|
2076
|
+
run.status = "cancelled";
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
/**
|
|
2080
|
+
* Change an open run's target. It may not go below what is already done;
|
|
2081
|
+
* setting it to exactly that finishes the run.
|
|
2082
|
+
*
|
|
2083
|
+
* @throws {InvalidProductionRunInputError} for a target that is not a
|
|
2084
|
+
* positive number or is below the completed quantity.
|
|
2085
|
+
*/
|
|
2086
|
+
async setTarget(runId, targetQty) {
|
|
2087
|
+
const target = positive(targetQty, "targetQty");
|
|
2088
|
+
return this.change(runId, "retarget", (run) => {
|
|
2089
|
+
const completed = toMillionths(Number(run.completedQty));
|
|
2090
|
+
const goal = toMillionths(target);
|
|
2091
|
+
if (goal < completed) throw new InvalidProductionRunInputError(`The target cannot be below the ${fromMillionths(completed)} already done.`);
|
|
2092
|
+
run.targetQty = target;
|
|
2093
|
+
if (completed > 0 && completed >= goal) run.status = "done";
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Report `qty` finished units on an open run: records a dated completion,
|
|
2098
|
+
* adds it to `completedQty`, moves a `planned` run to `in_progress`, and
|
|
2099
|
+
* marks the run `done` when the target is reached. With `consume` and/or
|
|
2100
|
+
* `produce`, also writes the stock movements through
|
|
2101
|
+
* {@link ProductionService}, stamped `sourceType: 'ProductionRunCompletion'`
|
|
2102
|
+
* with the completion id. Everything runs in one transaction.
|
|
2103
|
+
*
|
|
2104
|
+
* Consumption takes the run's bill's own lines for `qty` units: a
|
|
2105
|
+
* sub-assembly on the bill is taken from stock as a component. Building
|
|
2106
|
+
* that sub-assembly is its own run.
|
|
2107
|
+
*
|
|
2108
|
+
* @throws {ProductionRunNotFoundError}
|
|
2109
|
+
* @throws {ProductionRunStateError} when the run is `done` or `cancelled`.
|
|
2110
|
+
* @throws {ProductionRunOverCompletionError} when `qty` is more than the
|
|
2111
|
+
* run has left.
|
|
2112
|
+
* @throws {InvalidProductionRunInputError} for a bad `qty` or date.
|
|
2113
|
+
* @throws `InsufficientStockError` from inventory when consuming would
|
|
2114
|
+
* drive stock below zero; nothing is recorded then.
|
|
2115
|
+
*/
|
|
2116
|
+
async recordCompletion(runId, input) {
|
|
2117
|
+
const qty = positive(input.qty, "qty");
|
|
2118
|
+
const completedAt = input.completedAt === void 0 ? /* @__PURE__ */ new Date() : new Date(input.completedAt);
|
|
2119
|
+
if (Number.isNaN(completedAt.getTime())) throw new InvalidProductionRunInputError("completedAt is not a valid date.");
|
|
2120
|
+
return this.inTransaction(async (tx) => {
|
|
2121
|
+
const run = await this.load(tx, runId);
|
|
2122
|
+
if (!OPEN_STATUSES.has(run.status)) throw new ProductionRunStateError(runId, run.status, "complete");
|
|
2123
|
+
const target = toMillionths(Number(run.targetQty));
|
|
2124
|
+
const done = toMillionths(Number(run.completedQty));
|
|
2125
|
+
const reported = toMillionths(qty);
|
|
2126
|
+
const remaining = Math.max(0, target - done);
|
|
2127
|
+
if (reported > remaining) throw new ProductionRunOverCompletionError(runId, qty, fromMillionths(remaining));
|
|
2128
|
+
const total = done + reported;
|
|
2129
|
+
run.completedQty = fromMillionths(total);
|
|
2130
|
+
run.status = total >= target ? "done" : "in_progress";
|
|
2131
|
+
await run.save();
|
|
2132
|
+
const completion = await tx.completions.create({
|
|
2133
|
+
tenantId: run.tenantId ?? null,
|
|
2134
|
+
runId,
|
|
2135
|
+
qty,
|
|
2136
|
+
completedAt
|
|
2137
|
+
});
|
|
2138
|
+
let consumed = [];
|
|
2139
|
+
let produced = null;
|
|
2140
|
+
const inRunTenant = (work) => run.tenantId ? withTenant({ tenantId: run.tenantId }, work) : work();
|
|
2141
|
+
if (input.consume || input.produce) {
|
|
2142
|
+
const production = await ProductionService.create({ stockService: tx.stock });
|
|
2143
|
+
const ref = {
|
|
2144
|
+
id: completion.id,
|
|
2145
|
+
bomId: run.bomId
|
|
2146
|
+
};
|
|
2147
|
+
const sourceType = "ProductionRunCompletion";
|
|
2148
|
+
const consume = input.consume && {
|
|
2149
|
+
...input.consume,
|
|
2150
|
+
qty,
|
|
2151
|
+
sourceType
|
|
2152
|
+
};
|
|
2153
|
+
const produce = input.produce && {
|
|
2154
|
+
...input.produce,
|
|
2155
|
+
qty,
|
|
2156
|
+
sourceType
|
|
2157
|
+
};
|
|
2158
|
+
if (consume && produce) ({consumed, produced} = await inRunTenant(() => production.runProduction(ref, {
|
|
2159
|
+
consume,
|
|
2160
|
+
produce
|
|
2161
|
+
})));
|
|
2162
|
+
else if (consume) consumed = await inRunTenant(() => production.consumeMaterials(ref, consume));
|
|
2163
|
+
else if (produce) produced = await inRunTenant(() => production.produceFinishedGoods(ref, produce));
|
|
2164
|
+
}
|
|
2165
|
+
return {
|
|
2166
|
+
run,
|
|
2167
|
+
completion,
|
|
2168
|
+
consumed,
|
|
2169
|
+
produced
|
|
2170
|
+
};
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2173
|
+
/** Apply `apply` to an open run and save it, in one transaction. */
|
|
2174
|
+
change(runId, action, apply) {
|
|
2175
|
+
return this.inTransaction(async (tx) => {
|
|
2176
|
+
const run = await this.load(tx, runId);
|
|
2177
|
+
if (!OPEN_STATUSES.has(run.status)) throw new ProductionRunStateError(runId, run.status, action);
|
|
2178
|
+
apply(run);
|
|
2179
|
+
await run.save();
|
|
2180
|
+
return run;
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
async load(tx, runId) {
|
|
2184
|
+
const run = UUID_PATTERN.test(runId) ? await tx.runs.get({ id: runId }) : null;
|
|
2185
|
+
if (!run) throw new ProductionRunNotFoundError(runId);
|
|
2186
|
+
return run;
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Run `work` in one transaction on collections bound to it, rerunning the
|
|
2190
|
+
* whole transaction when it lost a revision race on the run row.
|
|
2191
|
+
*/
|
|
2192
|
+
inTransaction(work) {
|
|
2193
|
+
return withRevisionRetry$1(() => this.stockService.withTransaction(async (stock) => {
|
|
2194
|
+
const db = stock.db;
|
|
2195
|
+
const [runs, completions] = await Promise.all([ProductionRunCollection.create({ db }), ProductionRunCompletionCollection.create({ db })]);
|
|
2196
|
+
return work({
|
|
2197
|
+
stock,
|
|
2198
|
+
runs,
|
|
2199
|
+
completions
|
|
2200
|
+
});
|
|
2201
|
+
}));
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
var MAX_REVISION_ATTEMPTS = 10;
|
|
2205
|
+
async function withRevisionRetry$1(run) {
|
|
2206
|
+
for (let attempt = 1;; attempt++) try {
|
|
2207
|
+
return await run();
|
|
2208
|
+
} catch (error) {
|
|
2209
|
+
if (error?.code !== "RUNTIME_REVISION_CONFLICT" || attempt >= MAX_REVISION_ATTEMPTS) throw error;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
function positive(value, field) {
|
|
2213
|
+
const raw = Number(value);
|
|
2214
|
+
const number = Number.isFinite(raw) ? roundQuantity(raw) : raw;
|
|
2215
|
+
if (!Number.isFinite(number) || number <= 0) throw new InvalidProductionRunInputError(`${field} must be a number greater than zero (got ${String(value)}).`);
|
|
2216
|
+
if (number > 999999999) throw new InvalidProductionRunInputError(`${field} must be at most ${MAX_QUANTITY} (got ${String(value)}).`);
|
|
2217
|
+
return number;
|
|
2218
|
+
}
|
|
2219
|
+
function createProductionRunService(options) {
|
|
2220
|
+
return ProductionRunService.create(options);
|
|
2221
|
+
}
|
|
2222
|
+
//#endregion
|
|
2223
|
+
//#region src/services/RoutingService.ts
|
|
2224
|
+
var RoutingService = class RoutingService {
|
|
2225
|
+
constructor(db, boms, operations, steps) {
|
|
2226
|
+
this.db = db;
|
|
2227
|
+
this.boms = boms;
|
|
2228
|
+
this.operations = operations;
|
|
2229
|
+
this.steps = steps;
|
|
2230
|
+
}
|
|
2231
|
+
db;
|
|
2232
|
+
boms;
|
|
2233
|
+
operations;
|
|
2234
|
+
steps;
|
|
2235
|
+
/** Factory — prefer {@link createRoutingService}. */
|
|
2236
|
+
static async create(options) {
|
|
2237
|
+
const db = await resolveDatabase(options.db);
|
|
2238
|
+
const [boms, operations, steps] = await Promise.all([
|
|
2239
|
+
BillOfMaterialsCollection.create({ db }),
|
|
2240
|
+
OperationCollection.create({ db }),
|
|
2241
|
+
RoutingStepCollection.create({ db })
|
|
2242
|
+
]);
|
|
2243
|
+
return new RoutingService(db, boms, operations, steps);
|
|
2244
|
+
}
|
|
2245
|
+
/** The routing of a bill in step order; empty when it has none. */
|
|
2246
|
+
async list(bomId) {
|
|
2247
|
+
return this.steps.findByBom(bomId);
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Check `inputs` against the routing about to be replaced: durations,
|
|
2251
|
+
* operations, and that a retired operation is not added beyond the
|
|
2252
|
+
* occurrences `current` already has.
|
|
2253
|
+
*/
|
|
2254
|
+
async validate(current, inputs) {
|
|
2255
|
+
const retained = /* @__PURE__ */ new Map();
|
|
2256
|
+
for (const step of current) retained.set(step.operationId, (retained.get(step.operationId) ?? 0) + 1);
|
|
2257
|
+
for (const input of inputs) {
|
|
2258
|
+
const minutes = Number(input.estimatedMinutes);
|
|
2259
|
+
if (!Number.isFinite(minutes) || minutes < 0) throw new InvalidOperationInputError("Estimated minutes must be zero or more.");
|
|
2260
|
+
const operation = input.operationId ? await this.operations.get(input.operationId) : null;
|
|
2261
|
+
if (!operation) throw new OperationNotFoundError(input.operationId);
|
|
2262
|
+
const left = retained.get(operation.id) ?? 0;
|
|
2263
|
+
if (left > 0) retained.set(operation.id, left - 1);
|
|
2264
|
+
else if (!operation.isActive) throw new OperationRetiredError(operation.id);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Replace the bill's routing with `inputs`, numbered 1..n in the order given.
|
|
2269
|
+
* An empty list removes the routing.
|
|
2270
|
+
*
|
|
2271
|
+
* Throws {@link BomNotFoundError}, {@link OperationNotFoundError},
|
|
2272
|
+
* {@link OperationRetiredError} (a retired operation may stay on a routing
|
|
2273
|
+
* that already has it but cannot be newly added), or
|
|
2274
|
+
* {@link InvalidOperationInputError} for a negative or non-finite duration.
|
|
2275
|
+
*/
|
|
2276
|
+
async replaceRouting(bomId, inputs) {
|
|
2277
|
+
if (!bomId || !await this.boms.get(bomId)) throw new BomNotFoundError(bomId);
|
|
2278
|
+
const write = async (steps, boms) => {
|
|
2279
|
+
const bom = await boms.get(bomId);
|
|
2280
|
+
if (!bom) throw new BomNotFoundError(bomId);
|
|
2281
|
+
await bom.save();
|
|
2282
|
+
const current = await steps.findByBom(bomId);
|
|
2283
|
+
await this.validate(current, inputs);
|
|
2284
|
+
for (const step of current) await step.delete();
|
|
2285
|
+
const saved = [];
|
|
2286
|
+
for (const [index, input] of inputs.entries()) saved.push(await steps.create({
|
|
2287
|
+
bomId,
|
|
2288
|
+
operationId: input.operationId,
|
|
2289
|
+
sequence: index + 1,
|
|
2290
|
+
estimatedMinutes: Number(input.estimatedMinutes),
|
|
2291
|
+
notes: input.notes?.trim() ?? ""
|
|
2292
|
+
}));
|
|
2293
|
+
return saved;
|
|
2294
|
+
};
|
|
2295
|
+
const underlying = this.db;
|
|
2296
|
+
if (typeof underlying.transaction !== "function") throw new Error("RoutingService requires a database adapter with transaction() support.");
|
|
2297
|
+
return withRevisionRetry(() => underlying.transaction?.(async (txDb) => {
|
|
2298
|
+
const db = txDb;
|
|
2299
|
+
return write(await RoutingStepCollection.create({ db }), await BillOfMaterialsCollection.create({ db }));
|
|
2300
|
+
}));
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2303
|
+
async function withRevisionRetry(run) {
|
|
2304
|
+
for (let attempt = 1;; attempt++) try {
|
|
2305
|
+
return await run();
|
|
2306
|
+
} catch (error) {
|
|
2307
|
+
if (error?.code !== "RUNTIME_REVISION_CONFLICT" || attempt >= 5) throw error;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
async function createRoutingService(options) {
|
|
2311
|
+
return RoutingService.create(options);
|
|
2312
|
+
}
|
|
2313
|
+
//#endregion
|
|
2314
|
+
export { Assembly, AssemblyCollection, AssemblyService, BillOfMaterials, BillOfMaterialsCollection, BomCycleError, BomExplosionLimitError, BomLine, BomLineCollection, BomNotFoundError, BomService, BomStructureCycleError, DuplicateOperationCodeError, InvalidOperationInputError, InvalidProductionRunInputError, MAX_EXPLOSION_DEPTH, MAX_EXPLOSION_LINES, MAX_QUANTITY, NoActiveBomForProductError, Operation, OperationCollection, OperationNotFoundError, OperationRetiredError, OperationService, PRODUCTION_RUN_STATUSES, ProductionRun, ProductionRunCollection, ProductionRunCompletion, ProductionRunCompletionCollection, ProductionRunNotFoundError, ProductionRunOverCompletionError, ProductionRunService, ProductionRunStateError, ProductionService, QUANTITY_DECIMALS, RoutingService, RoutingStep, RoutingStepCollection, createAssemblyService, createBomService, createOperationService, createProductionRunService, createProductionService, createRoutingService, installManufacturingDispatchHandlers, roundQuantity };
|
|
697
2315
|
|
|
698
2316
|
//# sourceMappingURL=index.js.map
|