monkeybrain-runtime 1.0.0__py3-none-any.whl
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.
- monkeybrain_runtime-1.0.0.dist-info/METADATA +76 -0
- monkeybrain_runtime-1.0.0.dist-info/RECORD +838 -0
- monkeybrain_runtime-1.0.0.dist-info/WHEEL +5 -0
- monkeybrain_runtime-1.0.0.dist-info/entry_points.txt +3 -0
- monkeybrain_runtime-1.0.0.dist-info/top_level.txt +2 -0
- services/__init__.py +8 -0
- services/agentos/__init__.py +0 -0
- services/agentos/main.py +1 -0
- services/assets/helpers/__init__.py +12 -0
- services/assets/helpers/device.py +59 -0
- services/assets/helpers/equipment.py +179 -0
- services/assets/helpers/instruments.py +72 -0
- services/assets/helpers/machines.py +183 -0
- services/assets/helpers/materials.py +76 -0
- services/assets/helpers/parts.py +116 -0
- services/assets/helpers/plc.py +134 -0
- services/assets/helpers/tags.py +108 -0
- services/assets/helpers/tools.py +101 -0
- services/assets/main.py +75 -0
- services/assets/models/__init__.py +12 -0
- services/assets/models/device.py +79 -0
- services/assets/models/equipment.py +222 -0
- services/assets/models/instruments.py +85 -0
- services/assets/models/machines.py +230 -0
- services/assets/models/material.py +266 -0
- services/assets/models/parts.py +96 -0
- services/assets/models/plc.py +264 -0
- services/assets/models/tags.py +76 -0
- services/assets/models/tools.py +179 -0
- services/assets/routers/__init__.py +12 -0
- services/assets/routers/classes.py +65 -0
- services/assets/routers/device.py +86 -0
- services/assets/routers/equipment.py +145 -0
- services/assets/routers/families.py +61 -0
- services/assets/routers/instruments.py +70 -0
- services/assets/routers/machines.py +136 -0
- services/assets/routers/materials.py +105 -0
- services/assets/routers/parts.py +130 -0
- services/assets/routers/plc.py +94 -0
- services/assets/routers/subclasses.py +68 -0
- services/assets/routers/tags.py +138 -0
- services/assets/routers/tools.py +113 -0
- services/auth/helpers/__init__.py +13 -0
- services/auth/helpers/approval_decisions.py +261 -0
- services/auth/helpers/audit_elasticsearch_sync.py +350 -0
- services/auth/helpers/departments.py +53 -0
- services/auth/helpers/graph_store.py +848 -0
- services/auth/helpers/influx_store.py +280 -0
- services/auth/helpers/me.py +33 -0
- services/auth/helpers/nats_consumer.py +618 -0
- services/auth/helpers/nats_store.py +242 -0
- services/auth/helpers/permissions.py +62 -0
- services/auth/helpers/roles.py +87 -0
- services/auth/helpers/store.py +54 -0
- services/auth/helpers/team_members.py +155 -0
- services/auth/helpers/teams.py +87 -0
- services/auth/helpers/tokens.py +71 -0
- services/auth/helpers/users.py +119 -0
- services/auth/helpers/websocket_broadcast.py +41 -0
- services/auth/main.py +88 -0
- services/auth/models/__init__.py +12 -0
- services/auth/models/departments.py +55 -0
- services/auth/models/login.py +20 -0
- services/auth/models/permissions.py +61 -0
- services/auth/models/roles.py +53 -0
- services/auth/models/session.py +26 -0
- services/auth/models/teamMembers.py +59 -0
- services/auth/models/teams.py +56 -0
- services/auth/models/users.py +77 -0
- services/auth/routers/__init__.py +14 -0
- services/auth/routers/auth.py +3839 -0
- services/auth/routers/departments.py +84 -0
- services/auth/routers/integration_config.py +703 -0
- services/auth/routers/me.py +28 -0
- services/auth/routers/permissions.py +96 -0
- services/auth/routers/roles.py +139 -0
- services/auth/routers/session.py +224 -0
- services/auth/routers/team_members.py +152 -0
- services/auth/routers/teams.py +112 -0
- services/auth/routers/users.py +131 -0
- services/auth/routers/websocket_events.py +247 -0
- services/batch_execution/__init__.py +19 -0
- services/batch_execution/pipeline_triggers/__init__.py +53 -0
- services/batch_execution/pipeline_triggers/pipeline_parameter_adjuster.py +440 -0
- services/batch_execution/pipeline_triggers/pipeline_trigger_engine.py +445 -0
- services/batch_execution/pipeline_triggers/re_evaluation_queue.py +341 -0
- services/batch_execution/pipeline_triggers/test_phase3_implementation.py +553 -0
- services/batch_execution/pipeline_triggers/workflow_reevaluator.py +367 -0
- services/batch_execution/smoke_test_e2e_feedback_loop.py +704 -0
- services/batch_execution/workflow_executor.py +478 -0
- services/changeover/helpers/__init__.py +11 -0
- services/changeover/helpers/changeover.py +87 -0
- services/changeover/helpers/changeover_common.py +55 -0
- services/changeover/helpers/changeover_events.py +66 -0
- services/changeover/helpers/changeover_kpis.py +33 -0
- services/changeover/helpers/changeover_matrix.py +60 -0
- services/changeover/helpers/changeover_procedures.py +164 -0
- services/changeover/helpers/changeover_windows.py +96 -0
- services/changeover/main.py +52 -0
- services/changeover/models/__init__.py +11 -0
- services/changeover/models/changeover.py +73 -0
- services/changeover/models/changeover_events.py +142 -0
- services/changeover/models/changeover_kpis.py +75 -0
- services/changeover/models/changeover_matrix.py +63 -0
- services/changeover/models/changeover_procedures.py +108 -0
- services/changeover/models/changeover_tasks.py +87 -0
- services/changeover/models/changeover_windows.py +72 -0
- services/changeover/routers/__init__.py +9 -0
- services/changeover/routers/changeover_events.py +127 -0
- services/changeover/routers/changeover_kpis.py +80 -0
- services/changeover/routers/changeover_matrix.py +80 -0
- services/changeover/routers/changeover_procedures.py +118 -0
- services/changeover/routers/changeover_windows.py +98 -0
- services/common/__init__.py +2 -0
- services/common/approval_chains.py +648 -0
- services/common/auth.py +56 -0
- services/common/cdc.py +52 -0
- services/common/compat.py +217 -0
- services/common/compliance.py +562 -0
- services/common/config.py +134 -0
- services/common/cors.py +17 -0
- services/common/data_transformation.py +195 -0
- services/common/db.py +577 -0
- services/common/embeddings.py +97 -0
- services/common/event_reducers.py +194 -0
- services/common/event_types.py +51 -0
- services/common/integration_ingestion.py +169 -0
- services/common/logging.py +204 -0
- services/common/models/__init__.py +2 -0
- services/common/models/databricks.py +25 -0
- services/common/models/enums.py +64 -0
- services/common/module_control.py +422 -0
- services/common/mongo_cdc_watcher.py +106 -0
- services/common/n8n_auth.py +22 -0
- services/common/neo4j_mirror.py +1087 -0
- services/common/ontology_registry.py +110 -0
- services/common/reasoning_traces.py +52 -0
- services/common/supply_chain_cdc.py +555 -0
- services/common/tracing.py +159 -0
- services/common/utils.py +30 -0
- services/customers/helpers/__init__.py +8 -0
- services/customers/helpers/customer_details.py +64 -0
- services/customers/helpers/customer_metadata.py +64 -0
- services/customers/helpers/customer_order_metrics.py +67 -0
- services/customers/helpers/customer_payment_data.py +67 -0
- services/customers/main.py +50 -0
- services/customers/models/__init__.py +8 -0
- services/customers/models/customer_details.py +42 -0
- services/customers/models/customer_metadata.py +97 -0
- services/customers/models/customer_order_metrics.py +86 -0
- services/customers/models/customer_payment_data.py +60 -0
- services/customers/routers/__init__.py +8 -0
- services/customers/routers/customer_details.py +88 -0
- services/customers/routers/customer_metadata.py +88 -0
- services/customers/routers/customer_order_metrics.py +88 -0
- services/customers/routers/customer_payment_data.py +88 -0
- services/documents/__init__.py +1 -0
- services/documents/helpers/__init__.py +6 -0
- services/documents/helpers/document_metadata.py +569 -0
- services/documents/helpers/document_workflows.py +215 -0
- services/documents/helpers/report_templates.py +113 -0
- services/documents/main.py +49 -0
- services/documents/models/__init__.py +6 -0
- services/documents/models/document_metadata.py +215 -0
- services/documents/models/document_workflows.py +136 -0
- services/documents/models/report_templates.py +132 -0
- services/documents/routers/__init__.py +6 -0
- services/documents/routers/document_metadata.py +654 -0
- services/documents/routers/document_workflows.py +146 -0
- services/documents/routers/report_templates.py +86 -0
- services/events/helpers/__init__.py +5 -0
- services/events/helpers/events.py +394 -0
- services/events/main.py +40 -0
- services/events/models/__init__.py +5 -0
- services/events/models/events.py +50 -0
- services/events/routers/__init__.py +6 -0
- services/events/routers/count_events.py +109 -0
- services/events/routers/events.py +75 -0
- services/events/seed_events.py +196 -0
- services/facilities/helpers/__init__.py +8 -0
- services/facilities/helpers/lines.py +74 -0
- services/facilities/helpers/locations.py +231 -0
- services/facilities/helpers/plants.py +59 -0
- services/facilities/helpers/stages.py +110 -0
- services/facilities/helpers/workstation.py +213 -0
- services/facilities/main.py +60 -0
- services/facilities/models/__init__.py +10 -0
- services/facilities/models/industrialLine.py +72 -0
- services/facilities/models/industrialPlant.py +164 -0
- services/facilities/models/locations.py +74 -0
- services/facilities/models/stages.py +92 -0
- services/facilities/models/worker.py +73 -0
- services/facilities/models/workstation.py +117 -0
- services/facilities/models/workstation_live_state.py +59 -0
- services/facilities/routers/__init__.py +8 -0
- services/facilities/routers/bays.py +81 -0
- services/facilities/routers/buildings.py +92 -0
- services/facilities/routers/floors.py +81 -0
- services/facilities/routers/lines.py +154 -0
- services/facilities/routers/locations.py +208 -0
- services/facilities/routers/plant.py +203 -0
- services/facilities/routers/rooms.py +81 -0
- services/facilities/routers/stages.py +152 -0
- services/facilities/routers/workstation.py +173 -0
- services/file/backup.py +71 -0
- services/file/main.py +45 -0
- services/file/recieve.py +54 -0
- services/file/send.py +55 -0
- services/file/src/core/config.py +90 -0
- services/file/src/core/keycloak.py +152 -0
- services/file/src/core/logging_config.py +9 -0
- services/file/src/core/security.py +33 -0
- services/file/src/helpers/cad_conversion.py +331 -0
- services/file/src/helpers/helpers.py +825 -0
- services/file/src/routes/cad_conversion.py +26 -0
- services/file/src/routes/files.py +136 -0
- services/file/src/routes/presigned.py +154 -0
- services/file/src/services/websocket.py +293 -0
- services/floor_layout/helpers/__init__.py +8 -0
- services/floor_layout/helpers/bays.py +92 -0
- services/floor_layout/helpers/buildings.py +54 -0
- services/floor_layout/helpers/floors.py +65 -0
- services/floor_layout/helpers/rooms.py +76 -0
- services/floor_layout/main.py +52 -0
- services/floor_layout/models/__init__.py +8 -0
- services/floor_layout/models/bays.py +65 -0
- services/floor_layout/models/buildings.py +52 -0
- services/floor_layout/models/floors.py +45 -0
- services/floor_layout/models/rooms.py +61 -0
- services/floor_layout/routers/__init__.py +9 -0
- services/floor_layout/routers/bays.py +143 -0
- services/floor_layout/routers/buildings.py +116 -0
- services/floor_layout/routers/floors.py +89 -0
- services/floor_layout/routers/locations.py +80 -0
- services/floor_layout/routers/rooms.py +134 -0
- services/inventory/helpers/__init__.py +13 -0
- services/inventory/helpers/cycle_counts.py +124 -0
- services/inventory/helpers/inventory_allocations.py +134 -0
- services/inventory/helpers/inventory_item_counts.py +114 -0
- services/inventory/helpers/inventory_item_quantities.py +114 -0
- services/inventory/helpers/inventory_items.py +103 -0
- services/inventory/helpers/inventory_stage_outputs.py +134 -0
- services/inventory/helpers/inventory_transactions.py +112 -0
- services/inventory/helpers/stock_adjustment_requests.py +101 -0
- services/inventory/helpers/warehouse_cycle_counts.py +133 -0
- services/inventory/helpers/warehouse_locations.py +213 -0
- services/inventory/helpers/warehouse_regulated_records.py +123 -0
- services/inventory/main.py +62 -0
- services/inventory/models/__init__.py +17 -0
- services/inventory/models/cycle_counts.py +99 -0
- services/inventory/models/inventory_allocations.py +121 -0
- services/inventory/models/inventory_common.py +65 -0
- services/inventory/models/inventory_enums.py +21 -0
- services/inventory/models/inventory_item_count.py +65 -0
- services/inventory/models/inventory_item_quantity.py +82 -0
- services/inventory/models/inventory_items.py +168 -0
- services/inventory/models/inventory_responses.py +44 -0
- services/inventory/models/inventory_stage_outputs.py +96 -0
- services/inventory/models/inventory_state.py +15 -0
- services/inventory/models/inventory_transactions.py +80 -0
- services/inventory/models/stock_adjustment_requests.py +109 -0
- services/inventory/models/warehouse_cycle_counts.py +119 -0
- services/inventory/models/warehouse_location_models.py +708 -0
- services/inventory/models/warehouse_regulated_records.py +358 -0
- services/inventory/routers/__init__.py +13 -0
- services/inventory/routers/cycle_counts.py +106 -0
- services/inventory/routers/inventory_allocations.py +125 -0
- services/inventory/routers/inventory_item_counts.py +105 -0
- services/inventory/routers/inventory_item_quantities.py +105 -0
- services/inventory/routers/inventory_items.py +109 -0
- services/inventory/routers/inventory_stage_outputs.py +122 -0
- services/inventory/routers/inventory_transactions.py +96 -0
- services/inventory/routers/stock_adjustment_requests.py +124 -0
- services/inventory/routers/warehouse_cycle_counts.py +124 -0
- services/inventory/routers/warehouse_locations.py +426 -0
- services/inventory/routers/warehouse_regulated_records.py +273 -0
- services/iot/helpers/__init__.py +8 -0
- services/iot/helpers/ble_device.py +87 -0
- services/iot/helpers/mqtt_bridge.py +115 -0
- services/iot/helpers/sensor_readings.py +63 -0
- services/iot/helpers/sensors.py +77 -0
- services/iot/helpers/servers.py +72 -0
- services/iot/helpers/uwb_device.py +95 -0
- services/iot/main.py +53 -0
- services/iot/models/__init__.py +8 -0
- services/iot/models/ble_device.py +118 -0
- services/iot/models/sensors.py +256 -0
- services/iot/models/servers.py +206 -0
- services/iot/models/uwb_device.py +106 -0
- services/iot/routers/__init__.py +8 -0
- services/iot/routers/ble_device.py +110 -0
- services/iot/routers/sensors.py +144 -0
- services/iot/routers/servers.py +141 -0
- services/iot/routers/uwb_device.py +148 -0
- services/module_control/__init__.py +1 -0
- services/module_control/helpers/__init__.py +1 -0
- services/module_control/helpers/integration_config.py +243 -0
- services/module_control/helpers/security.py +104 -0
- services/module_control/main.py +44 -0
- services/module_control/models/__init__.py +1 -0
- services/module_control/models/module_control.py +65 -0
- services/module_control/routers/__init__.py +1 -0
- services/module_control/routers/module_control.py +219 -0
- services/orders/helpers/__init__.py +11 -0
- services/orders/helpers/invoices.py +123 -0
- services/orders/helpers/order_customer_metrics.py +61 -0
- services/orders/helpers/order_details.py +71 -0
- services/orders/helpers/order_metadata.py +61 -0
- services/orders/helpers/order_payment_metadata.py +74 -0
- services/orders/helpers/orders.py +119 -0
- services/orders/helpers/sales_orders.py +136 -0
- services/orders/main.py +56 -0
- services/orders/models/__init__.py +11 -0
- services/orders/models/invoices.py +415 -0
- services/orders/models/order_customer_metrics.py +78 -0
- services/orders/models/order_details.py +46 -0
- services/orders/models/order_metadata.py +60 -0
- services/orders/models/order_payment_metadata.py +63 -0
- services/orders/models/orders.py +64 -0
- services/orders/models/sales_orders.py +130 -0
- services/orders/routers/__init__.py +11 -0
- services/orders/routers/invoices.py +111 -0
- services/orders/routers/order_customer_metrics.py +87 -0
- services/orders/routers/order_details.py +87 -0
- services/orders/routers/order_metadata.py +87 -0
- services/orders/routers/order_payment_metadata.py +87 -0
- services/orders/routers/orders.py +74 -0
- services/orders/routers/sales_orders.py +111 -0
- services/pm/helpers/__init__.py +14 -0
- services/pm/helpers/calendar_bookings.py +114 -0
- services/pm/helpers/calibration_point.py +110 -0
- services/pm/helpers/calibrations.py +196 -0
- services/pm/helpers/checklists.py +318 -0
- services/pm/helpers/cleaning.py +333 -0
- services/pm/helpers/downtime.py +376 -0
- services/pm/helpers/kanban_boards.py +186 -0
- services/pm/helpers/maintainance.py +177 -0
- services/pm/helpers/sop.py +1155 -0
- services/pm/helpers/sop_cdc.py +324 -0
- services/pm/helpers/weekly_schedules.py +79 -0
- services/pm/main.py +62 -0
- services/pm/models/__init__.py +14 -0
- services/pm/models/calendar_booking.py +82 -0
- services/pm/models/calibration_point.py +44 -0
- services/pm/models/calibrations.py +167 -0
- services/pm/models/checklists.py +117 -0
- services/pm/models/cleaning.py +203 -0
- services/pm/models/downtime.py +109 -0
- services/pm/models/kanban_board.py +178 -0
- services/pm/models/maintainanceLog.py +148 -0
- services/pm/models/sop.py +152 -0
- services/pm/models/weekly_schedule.py +91 -0
- services/pm/routers/__init__.py +14 -0
- services/pm/routers/calendar_bookings.py +143 -0
- services/pm/routers/calibration_point.py +94 -0
- services/pm/routers/calibrations.py +232 -0
- services/pm/routers/checklists.py +188 -0
- services/pm/routers/cleaning.py +127 -0
- services/pm/routers/downtime.py +143 -0
- services/pm/routers/kanban_boards.py +283 -0
- services/pm/routers/maintainance.py +241 -0
- services/pm/routers/sop.py +437 -0
- services/pm/routers/weekly_schedules.py +108 -0
- services/process_definitions/helpers/__init__.py +11 -0
- services/process_definitions/helpers/cpp_cqa_registry.py +120 -0
- services/process_definitions/helpers/mbmr_templates.py +107 -0
- services/process_definitions/helpers/packing_instructions.py +113 -0
- services/process_definitions/helpers/process_constraints.py +495 -0
- services/process_definitions/helpers/process_corrections.py +279 -0
- services/process_definitions/helpers/process_definition.py +996 -0
- services/process_definitions/helpers/process_node_catalog.py +786 -0
- services/process_definitions/helpers/process_post_checks.py +441 -0
- services/process_definitions/helpers/process_pre_checks.py +351 -0
- services/process_definitions/helpers/process_steps.py +220 -0
- services/process_definitions/main.py +71 -0
- services/process_definitions/models/__init__.py +13 -0
- services/process_definitions/models/cpp_cqa_registry.py +145 -0
- services/process_definitions/models/gxp_change_controls.py +38 -0
- services/process_definitions/models/gxp_risk_assessments.py +30 -0
- services/process_definitions/models/gxp_validation_evidence.py +33 -0
- services/process_definitions/models/mbmr_templates.py +173 -0
- services/process_definitions/models/packing_instructions.py +176 -0
- services/process_definitions/models/process_constraints.py +159 -0
- services/process_definitions/models/process_corrections.py +118 -0
- services/process_definitions/models/process_definition.py +685 -0
- services/process_definitions/models/process_definition_common.py +48 -0
- services/process_definitions/models/process_node_catalog.py +25 -0
- services/process_definitions/models/process_post_checks.py +171 -0
- services/process_definitions/models/process_pre_checks.py +168 -0
- services/process_definitions/models/process_steps.py +170 -0
- services/process_definitions/node_services/__init__.py +8 -0
- services/process_definitions/node_services/common.py +95 -0
- services/process_definitions/node_services/executor.py +499 -0
- services/process_definitions/node_services/flow_simulator.py +733 -0
- services/process_definitions/node_services/functions.py +193 -0
- services/process_definitions/node_services/messaging.py +44 -0
- services/process_definitions/node_services/models.py +221 -0
- services/process_definitions/node_services/network.py +161 -0
- services/process_definitions/node_services/parsers.py +87 -0
- services/process_definitions/node_services/sequence.py +95 -0
- services/process_definitions/node_services/storage.py +50 -0
- services/process_definitions/node_services/webhooks.py +52 -0
- services/process_definitions/routers/__init__.py +10 -0
- services/process_definitions/routers/cpp_cqa_registry.py +86 -0
- services/process_definitions/routers/mbmr_templates.py +84 -0
- services/process_definitions/routers/packing_instructions.py +84 -0
- services/process_definitions/routers/process_constraints.py +564 -0
- services/process_definitions/routers/process_corrections.py +343 -0
- services/process_definitions/routers/process_definition.py +992 -0
- services/process_definitions/routers/process_post_checks.py +529 -0
- services/process_definitions/routers/process_pre_checks.py +435 -0
- services/process_definitions/routers/process_steps.py +274 -0
- services/procurement/helpers/__init__.py +9 -0
- services/procurement/helpers/goods_receipts.py +240 -0
- services/procurement/helpers/purchase_order_shipping_information.py +85 -0
- services/procurement/helpers/purchase_orders.py +68 -0
- services/procurement/helpers/quality_control.py +235 -0
- services/procurement/helpers/sampling.py +404 -0
- services/procurement/main.py +52 -0
- services/procurement/models/__init__.py +9 -0
- services/procurement/models/goods_receipts.py +165 -0
- services/procurement/models/purchase_orders.py +54 -0
- services/procurement/models/quality_control.py +464 -0
- services/procurement/models/reinspection_records.py +28 -0
- services/procurement/models/sampling.py +262 -0
- services/procurement/models/shipping_information.py +51 -0
- services/procurement/routers/__init__.py +9 -0
- services/procurement/routers/goods_receipts.py +201 -0
- services/procurement/routers/purchase_orders.py +106 -0
- services/procurement/routers/quality_control.py +386 -0
- services/procurement/routers/sampling.py +296 -0
- services/procurement/routers/shipping_information.py +97 -0
- services/production/__init__.py +1 -0
- services/production/agents/__init__.py +5 -0
- services/production/agents/batch_planning_agent.py +815 -0
- services/production/models/__init__.py +25 -0
- services/production/models/batch.py +253 -0
- services/products/helpers/__init__.py +10 -0
- services/products/helpers/boms.py +100 -0
- services/products/helpers/drug_research.py +644 -0
- services/products/helpers/product_component.py +168 -0
- services/products/helpers/product_inventory.py +221 -0
- services/products/helpers/product_pricing.py +123 -0
- services/products/helpers/product_utils.py +32 -0
- services/products/helpers/products.py +81 -0
- services/products/main.py +59 -0
- services/products/models/__init__.py +9 -0
- services/products/models/drug_research.py +138 -0
- services/products/models/product_common.py +60 -0
- services/products/models/product_component.py +1028 -0
- services/products/models/product_inventory.py +118 -0
- services/products/models/product_pricing.py +73 -0
- services/products/models/products.py +151 -0
- services/products/routers/__init__.py +9 -0
- services/products/routers/boms.py +116 -0
- services/products/routers/drug_research.py +115 -0
- services/products/routers/product_components.py +123 -0
- services/products/routers/product_inventory.py +185 -0
- services/products/routers/product_pricing.py +136 -0
- services/products/routers/products.py +165 -0
- services/replenishment/__init__.py +1 -0
- services/replenishment/main.py +46 -0
- services/replenishment/routers/__init__.py +1 -0
- services/replenishment/routers/replenishment.py +20 -0
- services/shifts/helpers/__init__.py +7 -0
- services/shifts/helpers/shift_templates.py +124 -0
- services/shifts/helpers/shifts.py +79 -0
- services/shifts/helpers/timesheets.py +137 -0
- services/shifts/main.py +48 -0
- services/shifts/models/__init__.py +8 -0
- services/shifts/models/shift.py +62 -0
- services/shifts/models/shift_template.py +82 -0
- services/shifts/models/time_range.py +31 -0
- services/shifts/models/timesheet.py +196 -0
- services/shifts/routers/__init__.py +7 -0
- services/shifts/routers/shift_templates.py +97 -0
- services/shifts/routers/shifts.py +117 -0
- services/shifts/routers/timesheets.py +117 -0
- services/shipping/helpers/__init__.py +15 -0
- services/shipping/helpers/carrier.py +78 -0
- services/shipping/helpers/customs_declaration.py +104 -0
- services/shipping/helpers/delivery_note.py +99 -0
- services/shipping/helpers/package.py +95 -0
- services/shipping/helpers/pallet.py +85 -0
- services/shipping/helpers/route.py +93 -0
- services/shipping/helpers/shipping_information.py +82 -0
- services/shipping/helpers/shipping_provider_details.py +59 -0
- services/shipping/helpers/shipping_provider_metadata.py +59 -0
- services/shipping/helpers/vehicle.py +85 -0
- services/shipping/helpers/waybill.py +86 -0
- services/shipping/main.py +64 -0
- services/shipping/models/__init__.py +15 -0
- services/shipping/models/carrier.py +97 -0
- services/shipping/models/customs_declaration.py +138 -0
- services/shipping/models/delivery_note.py +163 -0
- services/shipping/models/package.py +152 -0
- services/shipping/models/pallet.py +137 -0
- services/shipping/models/route.py +120 -0
- services/shipping/models/shipping_information.py +55 -0
- services/shipping/models/shipping_provider_details.py +42 -0
- services/shipping/models/shipping_provider_metadata.py +54 -0
- services/shipping/models/vehicle.py +129 -0
- services/shipping/models/waybill.py +189 -0
- services/shipping/routers/__init__.py +15 -0
- services/shipping/routers/carrier.py +99 -0
- services/shipping/routers/customs_declaration.py +132 -0
- services/shipping/routers/delivery_note.py +150 -0
- services/shipping/routers/package.py +141 -0
- services/shipping/routers/pallet.py +108 -0
- services/shipping/routers/route.py +128 -0
- services/shipping/routers/shipping_information.py +97 -0
- services/shipping/routers/shipping_provider_details.py +80 -0
- services/shipping/routers/shipping_provider_metadata.py +80 -0
- services/shipping/routers/vehicle.py +117 -0
- services/shipping/routers/waybill.py +119 -0
- services/suppliers/helpers/__init__.py +13 -0
- services/suppliers/helpers/supplier_capabilities.py +58 -0
- services/suppliers/helpers/supplier_certifications.py +67 -0
- services/suppliers/helpers/supplier_details.py +58 -0
- services/suppliers/helpers/supplier_financials.py +58 -0
- services/suppliers/helpers/supplier_inventory.py +74 -0
- services/suppliers/helpers/supplier_locations.py +60 -0
- services/suppliers/helpers/supplier_pricing.py +69 -0
- services/suppliers/helpers/supplier_quality.py +69 -0
- services/suppliers/helpers/supplier_shipping.py +69 -0
- services/suppliers/main.py +60 -0
- services/suppliers/models/__init__.py +13 -0
- services/suppliers/models/supplier_capabilities.py +70 -0
- services/suppliers/models/supplier_certifications.py +64 -0
- services/suppliers/models/supplier_details.py +75 -0
- services/suppliers/models/supplier_financials.py +69 -0
- services/suppliers/models/supplier_inventory.py +76 -0
- services/suppliers/models/supplier_locations.py +70 -0
- services/suppliers/models/supplier_pricing.py +74 -0
- services/suppliers/models/supplier_quality.py +74 -0
- services/suppliers/models/supplier_shipping.py +76 -0
- services/suppliers/routers/__init__.py +13 -0
- services/suppliers/routers/supplier_capabilities.py +88 -0
- services/suppliers/routers/supplier_certifications.py +87 -0
- services/suppliers/routers/supplier_details.py +83 -0
- services/suppliers/routers/supplier_financials.py +83 -0
- services/suppliers/routers/supplier_inventory.py +105 -0
- services/suppliers/routers/supplier_locations.py +89 -0
- services/suppliers/routers/supplier_pricing.py +96 -0
- services/suppliers/routers/supplier_quality.py +96 -0
- services/suppliers/routers/supplier_shipping.py +96 -0
- services/supply_allocation/main.py +46 -0
- services/supply_allocation/routers/__init__.py +1 -0
- services/supply_allocation/routers/allocation.py +20 -0
- services/taxonomy/helpers/__init__.py +7 -0
- services/taxonomy/helpers/classes.py +48 -0
- services/taxonomy/helpers/family.py +53 -0
- services/taxonomy/helpers/subclass.py +58 -0
- services/taxonomy/main.py +48 -0
- services/taxonomy/models/__init__.py +7 -0
- services/taxonomy/models/classes.py +52 -0
- services/taxonomy/models/family.py +60 -0
- services/taxonomy/models/subclass.py +50 -0
- services/taxonomy/routers/__init__.py +7 -0
- services/taxonomy/routers/classes.py +78 -0
- services/taxonomy/routers/family.py +77 -0
- services/taxonomy/routers/subclass.py +82 -0
- services/warehouse_execution/__init__.py +1 -0
- services/warehouse_execution/main.py +46 -0
- services/warehouse_execution/routers/__init__.py +1 -0
- services/warehouse_execution/routers/execution.py +21 -0
- services/work_order_agent/__init__.py +17 -0
- services/work_order_agent/agent/__init__.py +17 -0
- services/work_order_agent/agent/work_order_agent.py +658 -0
- services/work_order_agent/tracking/__init__.py +101 -0
- services/work_order_agent/tracking/event_system.py +182 -0
- services/work_order_agent/tracking/state_machine.py +163 -0
- services/work_order_agent/tracking/state_machine_integrator.py +295 -0
- services/work_order_agent/tracking/test_phase2_implementation.py +302 -0
- services/work_order_agent/tracking/time_analysis.py +301 -0
- services/work_order_agent/tracking/tracked_work_order.py +255 -0
- services/work_order_agent/tracking/work_order_adapter.py +367 -0
- services/work_order_agent/tracking/work_order_batch_manager.py +406 -0
- services/work_order_agent/tracking/work_order_repository.py +431 -0
- services/workorders/helpers/__init__.py +5 -0
- services/workorders/helpers/area_room_usage_ledger.py +139 -0
- services/workorders/helpers/batch_execution_records.py +265 -0
- services/workorders/helpers/batch_release_workflows.py +158 -0
- services/workorders/helpers/batch_step_executions.py +145 -0
- services/workorders/helpers/equipment_usage_ledger.py +209 -0
- services/workorders/helpers/executed_bmr_records.py +170 -0
- services/workorders/helpers/executed_bpr_records.py +170 -0
- services/workorders/helpers/executed_instruction_evidence.py +155 -0
- services/workorders/helpers/ipc_result_records.py +134 -0
- services/workorders/helpers/production_batches.py +117 -0
- services/workorders/helpers/work_orders.py +367 -0
- services/workorders/helpers/yield_reconciliation_records.py +158 -0
- services/workorders/main.py +110 -0
- services/workorders/models/__init__.py +5 -0
- services/workorders/models/area_room_usage_ledger.py +154 -0
- services/workorders/models/batch_execution_records.py +575 -0
- services/workorders/models/batch_release_workflows.py +190 -0
- services/workorders/models/batch_step_executions.py +142 -0
- services/workorders/models/equipment_usage_ledger.py +144 -0
- services/workorders/models/executed_bmr_records.py +220 -0
- services/workorders/models/executed_bpr_records.py +220 -0
- services/workorders/models/executed_instruction_evidence.py +128 -0
- services/workorders/models/ipc_result_records.py +164 -0
- services/workorders/models/production_batches.py +181 -0
- services/workorders/models/work_orders.py +255 -0
- services/workorders/models/yield_reconciliation_records.py +175 -0
- services/workorders/routers/__init__.py +5 -0
- services/workorders/routers/area_room_usage_ledger.py +117 -0
- services/workorders/routers/batch_execution_records.py +103 -0
- services/workorders/routers/batch_release_workflows.py +86 -0
- services/workorders/routers/batch_step_executions.py +88 -0
- services/workorders/routers/equipment_usage_ledger.py +115 -0
- services/workorders/routers/executed_bmr_records.py +86 -0
- services/workorders/routers/executed_bpr_records.py +86 -0
- services/workorders/routers/executed_instruction_evidence.py +86 -0
- services/workorders/routers/ipc_result_records.py +86 -0
- services/workorders/routers/production_batches.py +86 -0
- services/workorders/routers/work_orders.py +257 -0
- services/workorders/routers/yield_reconciliation_records.py +86 -0
- src/broca/__init__.py +5 -0
- src/broca/agent.py +201 -0
- src/cerebellum/__init__.py +0 -0
- src/cerebellum/adapter.py +84 -0
- src/cerebellum/capabilities/__init__.py +0 -0
- src/cerebellum/capabilities/agent/__init__.py +0 -0
- src/cerebellum/capabilities/agent/agents.py +65 -0
- src/cerebellum/capabilities/ai/__init__.py +0 -0
- src/cerebellum/capabilities/ai/providers.py +106 -0
- src/cerebellum/capabilities/api/__init__.py +0 -0
- src/cerebellum/capabilities/api/graphql.py +35 -0
- src/cerebellum/capabilities/api/rest_api.py +45 -0
- src/cerebellum/capabilities/api/webhook.py +30 -0
- src/cerebellum/capabilities/browser/__init__.py +0 -0
- src/cerebellum/capabilities/browser/browsers.py +27 -0
- src/cerebellum/capabilities/cloud/__init__.py +0 -0
- src/cerebellum/capabilities/cloud/cloud.py +46 -0
- src/cerebellum/capabilities/communication/__init__.py +0 -0
- src/cerebellum/capabilities/communication/communication.py +62 -0
- src/cerebellum/capabilities/database/__init__.py +0 -0
- src/cerebellum/capabilities/database/elasticsearch.py +40 -0
- src/cerebellum/capabilities/database/influxdb.py +37 -0
- src/cerebellum/capabilities/database/mongodb.py +50 -0
- src/cerebellum/capabilities/database/neo4j.py +32 -0
- src/cerebellum/capabilities/database/redis.py +44 -0
- src/cerebellum/capabilities/enterprise/__init__.py +0 -0
- src/cerebellum/capabilities/enterprise/opensource.py +180 -0
- src/cerebellum/capabilities/enterprise/proprietary.py +313 -0
- src/cerebellum/capabilities/event_streaming/__init__.py +0 -0
- src/cerebellum/capabilities/event_streaming/streaming.py +38 -0
- src/cerebellum/capabilities/infrastructure/__init__.py +0 -0
- src/cerebellum/capabilities/infrastructure/infrastructure.py +30 -0
- src/cerebellum/capabilities/productivity/__init__.py +0 -0
- src/cerebellum/capabilities/productivity/productivity.py +158 -0
- src/cerebellum/capabilities/robotics/__init__.py +0 -0
- src/cerebellum/capabilities/robotics/robotics.py +124 -0
- src/cerebellum/capabilities/runtime/__init__.py +0 -0
- src/cerebellum/capabilities/runtime/runtimes.py +92 -0
- src/cerebellum/capabilities/search/__init__.py +0 -0
- src/cerebellum/capabilities/search/search.py +63 -0
- src/cerebellum/capabilities/source_control/__init__.py +0 -0
- src/cerebellum/capabilities/source_control/source_control.py +113 -0
- src/cerebellum/capabilities/storage/__init__.py +0 -0
- src/cerebellum/capabilities/storage/storage.py +94 -0
- src/cerebellum/capabilities/workflow/__init__.py +0 -0
- src/cerebellum/capabilities/workflow/workflows.py +49 -0
- src/cerebellum/capability.py +108 -0
- src/cerebellum/config.py +157 -0
- src/cerebellum/fallback.py +147 -0
- src/cerebellum/fallback_engine.py +121 -0
- src/cerebellum/key_manager.py +129 -0
- src/cerebellum/keystore.py +179 -0
- src/cerebellum/lifecycle.py +54 -0
- src/cerebellum/metadata.py +61 -0
- src/cerebellum/operator/base.py +25 -0
- src/cerebellum/peripheral.py +92 -0
- src/cerebellum/registry.py +98 -0
- src/cerebellum/resolve_entity_capability.py +259 -0
- src/cingulate/benchmark/__init__.py +23 -0
- src/cingulate/benchmark/reporter.py +102 -0
- src/cingulate/benchmark/runner.py +159 -0
- src/cingulate/benchmark/scenario_runner.py +150 -0
- src/cingulate/benchmark/validator.py +102 -0
- src/cingulate/governance/__init__.py +21 -0
- src/cingulate/governance/architecture_validator.py +194 -0
- src/cingulate/governance/compliance.py +104 -0
- src/cingulate/governance/governance.py +77 -0
- src/cingulate/governance/policy_registry.py +91 -0
- src/cortex/__init__.py +33 -0
- src/cortex/cost.py +71 -0
- src/cortex/counterfactual.py +162 -0
- src/cortex/digital_twin.py +90 -0
- src/cortex/experience.py +83 -0
- src/cortex/feedback.py +144 -0
- src/cortex/loss.py +116 -0
- src/cortex/prediction.py +142 -0
- src/cortex/replay.py +130 -0
- src/cortex/reward.py +113 -0
- src/cortex/simulator.py +102 -0
- src/cortex/world_model.py +180 -0
- src/cortex/world_model_simulation.py +1591 -0
- src/cortex/world_state.py +121 -0
- src/cortex/xavier.py +250 -0
- src/deepdive/__init__.py +29 -0
- src/deepdive/aggregation.py +113 -0
- src/deepdive/digital_twin_aggregator.py +128 -0
- src/deepdive/elasticsearch_adapter.py +110 -0
- src/deepdive/fleet_analytics.py +131 -0
- src/deepdive/knowledge_aggregator.py +130 -0
- src/homeostasis/__init__.py +19 -0
- src/homeostasis/control_plane.py +159 -0
- src/introspection/__init__.py +38 -0
- src/introspection/alerting.py +142 -0
- src/introspection/health.py +101 -0
- src/introspection/lemon.py +243 -0
- src/introspection/logging.py +147 -0
- src/introspection/metrics.py +106 -0
- src/introspection/tracing.py +162 -0
- src/monkey_brain/__init__.py +1 -0
- src/monkey_brain/api/main.py +148 -0
- src/monkey_brain/api/models.py +81 -0
- src/monkey_brain/api/routes/routes/keys.py +106 -0
- src/monkey_brain/api/routes/routes/run.py +169 -0
- src/monkey_brain/api/routes/routes/simulate.py +485 -0
- src/monkey_brain/dlm/__init__.py +44 -0
- src/monkey_brain/dlm/dlm.py +139 -0
- src/monkey_brain/dlm/gc.py +115 -0
- src/monkey_brain/dlm/lifecycle.py +149 -0
- src/monkey_brain/dlm/orphans.py +99 -0
- src/monkey_brain/dlm/storage.py +149 -0
- src/monkey_brain/dlm/ttl.py +140 -0
- src/monkey_brain/documents/__init__.py +0 -0
- src/monkey_brain/documents/document_ocr.py +6 -0
- src/monkey_brain/kernel/__init__.py +53 -0
- src/monkey_brain/kernel/capability_interface.py +144 -0
- src/monkey_brain/kernel/classifier/__init__.py +1 -0
- src/monkey_brain/kernel/classifier/embed_classifier.py +125 -0
- src/monkey_brain/kernel/classifier/intent_examples.py +106 -0
- src/monkey_brain/kernel/dag.py +23 -0
- src/monkey_brain/kernel/execution_state.py +257 -0
- src/monkey_brain/kernel/goal_planner.py +85 -0
- src/monkey_brain/kernel/goal_router.py +20 -0
- src/monkey_brain/kernel/goals/__init__.py +1 -0
- src/monkey_brain/kernel/goals/goal.py +130 -0
- src/monkey_brain/kernel/goals/goal_bootstrap.py +38 -0
- src/monkey_brain/kernel/goals/goal_classifier.py +132 -0
- src/monkey_brain/kernel/goals/goal_registry.py +75 -0
- src/monkey_brain/kernel/intents/__init__.py +1 -0
- src/monkey_brain/kernel/intents/event_adapter.py +246 -0
- src/monkey_brain/kernel/intents/helpers.py +13 -0
- src/monkey_brain/kernel/intents/intent_registry.py +705 -0
- src/monkey_brain/kernel/intents/intent_router.py +102 -0
- src/monkey_brain/kernel/intents/predicates/approval_create.py +9 -0
- src/monkey_brain/kernel/intents/predicates/approval_decision.py +9 -0
- src/monkey_brain/kernel/intents/predicates/approval_hold.py +9 -0
- src/monkey_brain/kernel/intents/predicates/approval_query.py +9 -0
- src/monkey_brain/kernel/intents/predicates/batch_close.py +9 -0
- src/monkey_brain/kernel/intents/predicates/batch_creation.py +9 -0
- src/monkey_brain/kernel/intents/predicates/batch_delete.py +9 -0
- src/monkey_brain/kernel/intents/predicates/batch_hold.py +9 -0
- src/monkey_brain/kernel/intents/predicates/batch_record.py +9 -0
- src/monkey_brain/kernel/intents/predicates/batch_update.py +9 -0
- src/monkey_brain/kernel/intents/predicates/change_control.py +49 -0
- src/monkey_brain/kernel/intents/predicates/compliance_audit.py +14 -0
- src/monkey_brain/kernel/intents/predicates/decision_intelligence.py +9 -0
- src/monkey_brain/kernel/intents/predicates/drug_research.py +9 -0
- src/monkey_brain/kernel/intents/predicates/fuzzy_match.py +19 -0
- src/monkey_brain/kernel/intents/predicates/production_kpi.py +9 -0
- src/monkey_brain/kernel/intents/predicates/sop_create.py +9 -0
- src/monkey_brain/kernel/intents/predicates/sop_query.py +9 -0
- src/monkey_brain/kernel/intents/predicates/sop_update.py +9 -0
- src/monkey_brain/kernel/intents/predicates/warehouse_shipping.py +9 -0
- src/monkey_brain/kernel/intents/predicates/work_order_create.py +9 -0
- src/monkey_brain/kernel/intents/predicates/work_order_delete.py +9 -0
- src/monkey_brain/kernel/intents/predicates/work_order_hold.py +9 -0
- src/monkey_brain/kernel/intents/predicates/work_order_query.py +9 -0
- src/monkey_brain/kernel/intents/predicates/work_order_status.py +9 -0
- src/monkey_brain/kernel/intents/predicates/work_order_update.py +9 -0
- src/monkey_brain/kernel/intents/predicates/worker.py +9 -0
- src/monkey_brain/kernel/intents/telemetry_adapter.py +274 -0
- src/monkey_brain/kernel/intents/utils.py +68 -0
- src/monkey_brain/kernel/learning.py +98 -0
- src/monkey_brain/kernel/llm_explorer.py +188 -0
- src/monkey_brain/kernel/loss.py +81 -0
- src/monkey_brain/kernel/nlp/__init__.py +1 -0
- src/monkey_brain/kernel/nlp/compat.py +23 -0
- src/monkey_brain/kernel/nlp/models.py +10 -0
- src/monkey_brain/kernel/nlp/question_analyzer.py +203 -0
- src/monkey_brain/kernel/nlp/spacy_parser.py +53 -0
- src/monkey_brain/kernel/observer.py +97 -0
- src/monkey_brain/kernel/parser/__init__.py +3 -0
- src/monkey_brain/kernel/parser/ast.py +28 -0
- src/monkey_brain/kernel/parser/extractors/__init__.py +11 -0
- src/monkey_brain/kernel/parser/extractors/entities.py +21 -0
- src/monkey_brain/kernel/parser/extractors/filters.py +16 -0
- src/monkey_brain/kernel/parser/extractors/projections.py +36 -0
- src/monkey_brain/kernel/parser/extractors/verbs.py +31 -0
- src/monkey_brain/kernel/parser/parser.py +57 -0
- src/monkey_brain/kernel/parser/rules.py +75 -0
- src/monkey_brain/kernel/pipeline.py +44 -0
- src/monkey_brain/kernel/planner.py +57 -0
- src/monkey_brain/kernel/rl/__init__.py +33 -0
- src/monkey_brain/kernel/rl/learner.py +98 -0
- src/monkey_brain/kernel/rl/policy.py +254 -0
- src/monkey_brain/kernel/rl/reward.py +117 -0
- src/monkey_brain/kernel/rl/transition.py +112 -0
- src/monkey_brain/persistence/__init__.py +47 -0
- src/monkey_brain/persistence/adapters.py +49 -0
- src/monkey_brain/persistence/events.py +105 -0
- src/monkey_brain/persistence/manager.py +124 -0
- src/monkey_brain/persistence/mongodb_adapter.py +91 -0
- src/monkey_brain/persistence/redis_adapter.py +93 -0
- src/monkey_brain/persistence/reducer.py +111 -0
- src/monkey_brain/runtime/__init__.py +49 -0
- src/monkey_brain/runtime/depedencies.py +8 -0
- src/monkey_brain/runtime/engine.py +183 -0
- src/monkey_brain/runtime/message_bus.py +82 -0
- src/monkey_brain/runtime/process.py +144 -0
- src/monkey_brain/runtime/resource_manager.py +100 -0
- src/monkey_brain/runtime/routers.py +8 -0
- src/monkey_brain/runtime/runtime.py +199 -0
- src/monkey_brain/runtime/scheduler.py +165 -0
- src/monkey_brain/runtime/supervisor.py +133 -0
- src/monkey_brain/runtime/worker_pool.py +111 -0
- src/plasticity/seed/__init__.py +30 -0
- src/plasticity/seed/benchmark_generator.py +105 -0
- src/plasticity/seed/event_generator.py +122 -0
- src/plasticity/seed/scenario_builder.py +134 -0
- src/plasticity/seed/seed_data.py +206 -0
- src/plasticity/seed/seeder.py +122 -0
- src/plasticity/testing/__init__.py +28 -0
- src/plasticity/testing/performance.py +131 -0
- src/plasticity/testing/profiler.py +255 -0
- src/plasticity/testing/reporter.py +84 -0
- src/plasticity/testing/runner.py +209 -0
- src/sync/__init__.py +12 -0
- src/sync/cloud_aggregator.py +63 -0
- src/sync/edge_node.py +51 -0
- src/sync/sync_manager.py +74 -0
|
@@ -0,0 +1,1028 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field, model_validator
|
|
7
|
+
|
|
8
|
+
from services.products.models.product_common import ComponentType, UnitOfMeasure, utc_now, ensure_utc
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
12
|
+
from typing import Optional, List
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from datetime import date, datetime, timezone
|
|
15
|
+
from decimal import Decimal
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UnitOfMeasure(str, Enum):
|
|
19
|
+
PIECE = "piece"
|
|
20
|
+
KILOGRAM = "kg"
|
|
21
|
+
GRAM = "g"
|
|
22
|
+
LITER = "liter"
|
|
23
|
+
METER = "meter"
|
|
24
|
+
SQUARE_METER = "sq_meter"
|
|
25
|
+
CUBIC_METER = "cu_meter"
|
|
26
|
+
INCH = "inch"
|
|
27
|
+
FOOT = "foot"
|
|
28
|
+
POUND = "pound"
|
|
29
|
+
OUNCE = "ounce"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Currency(str, Enum):
|
|
33
|
+
USD = "USD"
|
|
34
|
+
EUR = "EUR"
|
|
35
|
+
GBP = "GBP"
|
|
36
|
+
INR = "INR"
|
|
37
|
+
JPY = "JPY"
|
|
38
|
+
CNY = "CNY"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PartCategory(str, Enum):
|
|
42
|
+
RAW_MATERIAL = "raw_material"
|
|
43
|
+
COMPONENT = "component"
|
|
44
|
+
SUBASSEMBLY = "subassembly"
|
|
45
|
+
CONSUMABLE = "consumable"
|
|
46
|
+
PACKAGING = "packaging"
|
|
47
|
+
FASTENER = "fastener"
|
|
48
|
+
ELECTRONIC = "electronic"
|
|
49
|
+
OTHER = "other"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ECNStatus(str, Enum):
|
|
53
|
+
DRAFT = "draft"
|
|
54
|
+
UNDER_REVIEW = "under_review"
|
|
55
|
+
APPROVED = "approved"
|
|
56
|
+
RELEASED = "released"
|
|
57
|
+
REJECTED = "rejected"
|
|
58
|
+
OBSOLETE = "obsolete"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class HazardClass(str, Enum):
|
|
62
|
+
FLAMMABLE = "flammable"
|
|
63
|
+
CORROSIVE = "corrosive"
|
|
64
|
+
TOXIC = "toxic"
|
|
65
|
+
OXIDIZER = "oxidizer"
|
|
66
|
+
EXPLOSIVE = "explosive"
|
|
67
|
+
RADIOACTIVE = "radioactive"
|
|
68
|
+
IRRITANT = "irritant"
|
|
69
|
+
CARCINOGEN = "carcinogen"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class BOMRelationshipType(str, Enum):
|
|
73
|
+
ASSEMBLY = "assembly"
|
|
74
|
+
SUBASSEMBLY = "subassembly"
|
|
75
|
+
PHANTOM = "phantom" # Transient assembly — not stocked
|
|
76
|
+
REFERENCE = "reference" # Informational only, not consumed
|
|
77
|
+
CO_PRODUCT = "co_product" # By-product produced alongside main output
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class BOMComponentType(str, Enum):
|
|
81
|
+
RAW_MATERIAL = "Raw Material"
|
|
82
|
+
COMPONENT = "Component"
|
|
83
|
+
SUBASSEMBLY = "Subassembly"
|
|
84
|
+
FINISHED_GOOD = "Finished Good"
|
|
85
|
+
PACKAGING = "Packaging"
|
|
86
|
+
CONSUMABLE = "Consumable"
|
|
87
|
+
OTHER = "Other"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class UsageType(str, Enum):
|
|
91
|
+
STANDARD = "Standard"
|
|
92
|
+
OPTIONAL = "Optional"
|
|
93
|
+
ALTERNATE = "Alternate"
|
|
94
|
+
SUBSTITUTE = "Substitute"
|
|
95
|
+
PHANTOM = "Phantom"
|
|
96
|
+
REFERENCE = "Reference"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class BOMType(str, Enum):
|
|
100
|
+
ENGINEERING = "Engineering"
|
|
101
|
+
MANUFACTURING = "Manufacturing"
|
|
102
|
+
SERVICE = "Service"
|
|
103
|
+
SALES = "Sales"
|
|
104
|
+
CONFIGURABLE = "Configurable"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class BOMStatus(str, Enum):
|
|
108
|
+
DRAFT = "Draft"
|
|
109
|
+
ACTIVE = "Active"
|
|
110
|
+
INACTIVE = "Inactive"
|
|
111
|
+
OBSOLETE = "Obsolete"
|
|
112
|
+
ARCHIVED = "Archived"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class LeadTimeStatus(str, Enum):
|
|
116
|
+
IN_STOCK = "In Stock"
|
|
117
|
+
SHORT = "Short"
|
|
118
|
+
MEDIUM = "Medium"
|
|
119
|
+
LONG = "Long"
|
|
120
|
+
CRITICAL = "Critical"
|
|
121
|
+
UNKNOWN = "Unknown"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Supplier(BaseModel):
|
|
125
|
+
supplier_id: Optional[str] = None
|
|
126
|
+
supplier_name: Optional[str] = None
|
|
127
|
+
lead_time_days: Optional[int] = Field(None, ge=0)
|
|
128
|
+
preferred: bool = False
|
|
129
|
+
remarks: Optional[str] = None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ScrapAndYield(BaseModel):
|
|
133
|
+
"""Scrap factors and yield rates for manufacturing consumption calculations."""
|
|
134
|
+
|
|
135
|
+
scrap_factor_percent: Decimal = Field(
|
|
136
|
+
default=Decimal("0"),
|
|
137
|
+
ge=0,
|
|
138
|
+
le=100,
|
|
139
|
+
description=(
|
|
140
|
+
"Extra material ordered/consumed to cover expected scrap. "
|
|
141
|
+
"e.g. 5 means order 5 % more than BOM quantity."
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
process_yield_percent: Decimal = Field(
|
|
145
|
+
default=Decimal("100"),
|
|
146
|
+
gt=0,
|
|
147
|
+
le=100,
|
|
148
|
+
description=(
|
|
149
|
+
"Expected good-unit yield from this process step. "
|
|
150
|
+
"e.g. 95 means 5 % of units are rejected."
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
shrinkage_factor_percent: Decimal = Field(
|
|
154
|
+
default=Decimal("0"),
|
|
155
|
+
ge=0,
|
|
156
|
+
le=100,
|
|
157
|
+
description="Material lost due to shrinkage, evaporation, or trimming.",
|
|
158
|
+
)
|
|
159
|
+
operation_scrap_code: Optional[str] = Field(
|
|
160
|
+
None,
|
|
161
|
+
max_length=20,
|
|
162
|
+
description="ERP/MES scrap reason code (e.g. 'TRIM', 'DEFECT', 'EVAP').",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def gross_quantity_multiplier(self) -> Decimal:
|
|
167
|
+
"""
|
|
168
|
+
Combined multiplier to apply to the BOM net quantity so the work order
|
|
169
|
+
issues enough material to yield the required good output.
|
|
170
|
+
|
|
171
|
+
gross_qty = net_qty × gross_quantity_multiplier
|
|
172
|
+
"""
|
|
173
|
+
scrap = Decimal("1") + (self.scrap_factor_percent / Decimal("100"))
|
|
174
|
+
shrink = Decimal("1") + (self.shrinkage_factor_percent / Decimal("100"))
|
|
175
|
+
yield_ = self.process_yield_percent / Decimal("100")
|
|
176
|
+
return (scrap * shrink) / yield_
|
|
177
|
+
|
|
178
|
+
def gross_quantity_for(self, net_quantity: Decimal) -> Decimal:
|
|
179
|
+
"""Returns the gross quantity needed to deliver `net_quantity` good units."""
|
|
180
|
+
return (net_quantity * self.gross_quantity_multiplier).quantize(Decimal("0.0001"))
|
|
181
|
+
|
|
182
|
+
class ReferenceDesignator(BaseModel):
|
|
183
|
+
"""
|
|
184
|
+
PCB/electronics reference designator entry.
|
|
185
|
+
Each instance maps one refdes (e.g. 'C12') to this part on a specific board.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
designator: str = Field(
|
|
189
|
+
...,
|
|
190
|
+
max_length=20,
|
|
191
|
+
pattern=r"^[A-Z]{1,5}\d+[A-Z]?$",
|
|
192
|
+
description="IPC-7711 reference designator, e.g. R1, C12, U3A, TP101.",
|
|
193
|
+
)
|
|
194
|
+
board_id: str = Field(..., max_length=50, description="PCB assembly part number or ID.")
|
|
195
|
+
schematic_page: Optional[int] = Field(None, ge=1, description="Schematic sheet number.")
|
|
196
|
+
x_mm: Optional[Decimal] = Field(None, description="X centroid on board (mm).")
|
|
197
|
+
y_mm: Optional[Decimal] = Field(None, description="Y centroid on board (mm).")
|
|
198
|
+
rotation_deg: Optional[Decimal] = Field(None, ge=0, lt=360, description="Placement rotation (°).")
|
|
199
|
+
side: Optional[str] = Field(None, pattern=r"^(top|bottom)$", description="'top' or 'bottom'.")
|
|
200
|
+
dnp: bool = Field(
|
|
201
|
+
default=False,
|
|
202
|
+
description="Do Not Place — part is listed but intentionally unpopulated.",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
@field_validator("designator", mode="before")
|
|
206
|
+
@classmethod
|
|
207
|
+
def uppercase_designator(cls, v: str) -> str:
|
|
208
|
+
return v.upper() if isinstance(v, str) else v
|
|
209
|
+
|
|
210
|
+
class ShelfLifeInfo(BaseModel):
|
|
211
|
+
"""Shelf-life tracking for moisture-sensitive, chemical, or perishable parts."""
|
|
212
|
+
|
|
213
|
+
shelf_life_days: Optional[int] = Field(
|
|
214
|
+
None, gt=0, description="Total shelf life from manufacture date (days)."
|
|
215
|
+
)
|
|
216
|
+
floor_life_hours: Optional[int] = Field(
|
|
217
|
+
None, gt=0,
|
|
218
|
+
description=(
|
|
219
|
+
"IPC/JEDEC MSL floor life once the original packaging is opened (hours). "
|
|
220
|
+
"Relevant for moisture-sensitive devices (MSDs)."
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
msl_level: Optional[int] = Field(
|
|
224
|
+
None, ge=1, le=6,
|
|
225
|
+
description="IPC/JEDEC J-STD-020 Moisture Sensitivity Level (1–6).",
|
|
226
|
+
)
|
|
227
|
+
storage_temperature_min_c: Optional[Decimal] = Field(
|
|
228
|
+
None, description="Minimum storage temperature (°C)."
|
|
229
|
+
)
|
|
230
|
+
storage_temperature_max_c: Optional[Decimal] = Field(
|
|
231
|
+
None, description="Maximum storage temperature (°C)."
|
|
232
|
+
)
|
|
233
|
+
storage_humidity_max_pct: Optional[Decimal] = Field(
|
|
234
|
+
None, ge=0, le=100, description="Maximum relative humidity for storage (%)."
|
|
235
|
+
)
|
|
236
|
+
requires_dry_pack: bool = Field(
|
|
237
|
+
default=False, description="Must be stored/shipped in a sealed dry-pack bag."
|
|
238
|
+
)
|
|
239
|
+
requires_cold_chain: bool = Field(
|
|
240
|
+
default=False, description="Must remain in a refrigerated/frozen supply chain."
|
|
241
|
+
)
|
|
242
|
+
date_code_format: Optional[str] = Field(
|
|
243
|
+
None, max_length=20,
|
|
244
|
+
description="Expected date-code format on the part label, e.g. 'YYWW' or 'YYYYMMDD'.",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
@model_validator(mode="after")
|
|
248
|
+
def validate_temp_range(self) -> "ShelfLifeInfo":
|
|
249
|
+
lo = self.storage_temperature_min_c
|
|
250
|
+
hi = self.storage_temperature_max_c
|
|
251
|
+
if lo is not None and hi is not None and lo >= hi:
|
|
252
|
+
raise ValueError("storage_temperature_min_c must be less than storage_temperature_max_c")
|
|
253
|
+
return self
|
|
254
|
+
|
|
255
|
+
def is_expired(self, manufacture_date: date) -> bool:
|
|
256
|
+
"""True if shelf life has elapsed since manufacture_date."""
|
|
257
|
+
if self.shelf_life_days is None:
|
|
258
|
+
return False
|
|
259
|
+
age = (date.today() - manufacture_date).days
|
|
260
|
+
return age > self.shelf_life_days
|
|
261
|
+
|
|
262
|
+
def remaining_shelf_life_days(self, manufacture_date: date) -> Optional[int]:
|
|
263
|
+
if self.shelf_life_days is None:
|
|
264
|
+
return None
|
|
265
|
+
return max(0, self.shelf_life_days - (date.today() - manufacture_date).days)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class HazmatInfo(BaseModel):
|
|
269
|
+
batch_tracking_required: bool = False
|
|
270
|
+
serial_tracking_required: bool = False
|
|
271
|
+
expiry_tracking_required: bool = False
|
|
272
|
+
hazmat: bool = False
|
|
273
|
+
temp_sensitive: bool = False
|
|
274
|
+
controlled_substance: bool = False
|
|
275
|
+
regulatory_certifications: Optional[list[str]] = None
|
|
276
|
+
msds_url: Optional[str] = None
|
|
277
|
+
|
|
278
|
+
model_config = {"str_strip_whitespace": True}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class ECNRecord(BaseModel):
|
|
282
|
+
"""A single Engineering Change Notice affecting this part."""
|
|
283
|
+
|
|
284
|
+
ecn_number: str = Field(
|
|
285
|
+
...,
|
|
286
|
+
max_length=30,
|
|
287
|
+
pattern=r"^ECN-\d{4,10}$",
|
|
288
|
+
description="ECN identifier, e.g. 'ECN-20240312'.",
|
|
289
|
+
)
|
|
290
|
+
title: str = Field(..., max_length=200)
|
|
291
|
+
description: str = Field(..., max_length=2000)
|
|
292
|
+
status: ECNStatus = Field(default=ECNStatus.DRAFT)
|
|
293
|
+
initiated_by: str = Field(..., max_length=100, description="Engineer or team who raised the ECN.")
|
|
294
|
+
approved_by: Optional[str] = Field(None, max_length=100)
|
|
295
|
+
date_initiated: date = Field(default_factory=date.today)
|
|
296
|
+
date_approved: Optional[date] = None
|
|
297
|
+
date_effective: Optional[date] = None
|
|
298
|
+
affected_revision_from: Optional[str] = Field(
|
|
299
|
+
None, max_length=10, description="Previous part revision this ECN supersedes."
|
|
300
|
+
)
|
|
301
|
+
affected_revision_to: Optional[str] = Field(
|
|
302
|
+
None, max_length=10, description="New part revision introduced by this ECN."
|
|
303
|
+
)
|
|
304
|
+
reason_code: Optional[str] = Field(
|
|
305
|
+
None, max_length=50,
|
|
306
|
+
description="Reason category, e.g. 'COST_REDUCTION', 'SAFETY', 'SUPPLIER_CHANGE'.",
|
|
307
|
+
)
|
|
308
|
+
linked_documents: List[str] = Field(
|
|
309
|
+
default_factory=list,
|
|
310
|
+
description="Document IDs (drawings, test reports) related to this ECN.",
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
@model_validator(mode="after")
|
|
314
|
+
def validate_approval_flow(self) -> "ECNRecord":
|
|
315
|
+
if self.status in (ECNStatus.APPROVED, ECNStatus.RELEASED) and not self.approved_by:
|
|
316
|
+
raise ValueError("approved_by is required when status is APPROVED or RELEASED")
|
|
317
|
+
if self.date_approved and self.date_approved < self.date_initiated:
|
|
318
|
+
raise ValueError("date_approved cannot precede date_initiated")
|
|
319
|
+
if self.date_effective and self.date_approved and self.date_effective < self.date_approved:
|
|
320
|
+
raise ValueError("date_effective cannot precede date_approved")
|
|
321
|
+
return self
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def is_released(self) -> bool:
|
|
325
|
+
return self.status == ECNStatus.RELEASED
|
|
326
|
+
|
|
327
|
+
@property
|
|
328
|
+
def days_open(self) -> int:
|
|
329
|
+
end = self.date_approved or date.today()
|
|
330
|
+
return (end - self.date_initiated).days
|
|
331
|
+
|
|
332
|
+
class BOMLink(BaseModel):
|
|
333
|
+
"""
|
|
334
|
+
Defines this part's position inside a multi-level Bill of Materials.
|
|
335
|
+
One part may appear in multiple BOMs (shared components), so a list of
|
|
336
|
+
BOMLink objects is attached to the part rather than a single parent ID.
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
parent_bom_id: str = Field(
|
|
340
|
+
..., max_length=50,
|
|
341
|
+
description="Part number of the immediate parent assembly in the BOM.",
|
|
342
|
+
)
|
|
343
|
+
bom_level: int = Field(
|
|
344
|
+
..., ge=1,
|
|
345
|
+
description="BOM depth level (1 = top-level assembly, 2 = subassembly, …).",
|
|
346
|
+
)
|
|
347
|
+
quantity_per_assembly: Decimal = Field(
|
|
348
|
+
..., gt=0,
|
|
349
|
+
description="Net quantity of this part required per one parent assembly.",
|
|
350
|
+
)
|
|
351
|
+
find_number: Optional[int] = Field(
|
|
352
|
+
None, ge=1,
|
|
353
|
+
description="Find number / item sequence on the engineering drawing BOM table.",
|
|
354
|
+
)
|
|
355
|
+
relationship_type: BOMRelationshipType = Field(default=BOMRelationshipType.ASSEMBLY)
|
|
356
|
+
is_configurable: bool = Field(
|
|
357
|
+
default=False,
|
|
358
|
+
description="Part may be substituted based on product configuration options.",
|
|
359
|
+
)
|
|
360
|
+
substitute_part_numbers: List[str] = Field(
|
|
361
|
+
default_factory=list,
|
|
362
|
+
description="Approved alternate part numbers that may be used in place of this part.",
|
|
363
|
+
)
|
|
364
|
+
effective_date_start: Optional[date] = Field(
|
|
365
|
+
None, description="Date from which this BOM relationship is valid."
|
|
366
|
+
)
|
|
367
|
+
effective_date_end: Optional[date] = Field(
|
|
368
|
+
None, description="Date after which this BOM relationship is superseded."
|
|
369
|
+
)
|
|
370
|
+
notes: Optional[str] = Field(None, max_length=500)
|
|
371
|
+
|
|
372
|
+
@model_validator(mode="after")
|
|
373
|
+
def validate_effectivity(self) -> "BOMLink":
|
|
374
|
+
s, e = self.effective_date_start, self.effective_date_end
|
|
375
|
+
if s and e and s >= e:
|
|
376
|
+
raise ValueError("effective_date_start must be earlier than effective_date_end")
|
|
377
|
+
return self
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def is_currently_effective(self) -> bool:
|
|
381
|
+
today = date.today()
|
|
382
|
+
after_start = (self.effective_date_start is None) or (today >= self.effective_date_start)
|
|
383
|
+
before_end = (self.effective_date_end is None) or (today < self.effective_date_end)
|
|
384
|
+
return after_start and before_end
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class CostInfo(BaseModel):
|
|
388
|
+
unit_cost: Decimal = Field(..., gt=0, decimal_places=4)
|
|
389
|
+
currency: Currency = Field(default=Currency.USD)
|
|
390
|
+
minimum_order_quantity: Decimal = Field(..., gt=0)
|
|
391
|
+
bulk_discount_threshold: Optional[Decimal] = Field(None, gt=0)
|
|
392
|
+
bulk_discount_percent: Optional[Decimal] = Field(None, ge=0, le=100)
|
|
393
|
+
last_price_update: Optional[date] = None
|
|
394
|
+
|
|
395
|
+
@model_validator(mode="after")
|
|
396
|
+
def validate_bulk_discount(self) -> "CostInfo":
|
|
397
|
+
has_threshold = self.bulk_discount_threshold is not None
|
|
398
|
+
has_discount = self.bulk_discount_percent is not None
|
|
399
|
+
if has_threshold != has_discount:
|
|
400
|
+
raise ValueError(
|
|
401
|
+
"Both bulk_discount_threshold and bulk_discount_percent must be provided together"
|
|
402
|
+
)
|
|
403
|
+
return self
|
|
404
|
+
|
|
405
|
+
def effective_unit_cost(self, quantity: Decimal) -> Decimal:
|
|
406
|
+
"""Returns unit cost after applying bulk discount if applicable."""
|
|
407
|
+
if (
|
|
408
|
+
self.bulk_discount_threshold
|
|
409
|
+
and self.bulk_discount_percent
|
|
410
|
+
and quantity >= self.bulk_discount_threshold
|
|
411
|
+
):
|
|
412
|
+
discount = self.unit_cost * (self.bulk_discount_percent / Decimal("100"))
|
|
413
|
+
return self.unit_cost - discount
|
|
414
|
+
return self.unit_cost
|
|
415
|
+
|
|
416
|
+
class QuantityInfo(BaseModel):
|
|
417
|
+
base_unit_of_measure: UnitOfMeasure = "Each"
|
|
418
|
+
packaging_type: Optional[str] = None
|
|
419
|
+
units_per_case: Optional[float] = Field(default=None, gt=0)
|
|
420
|
+
units_per_pallet: Optional[float] = Field(default=None, gt=0)
|
|
421
|
+
alternate_uoms: Optional[list[Any]] = None # AlternateUom handled via dict in DB
|
|
422
|
+
|
|
423
|
+
model_config = {"str_strip_whitespace": True}
|
|
424
|
+
|
|
425
|
+
class MaterialSpec(BaseModel):
|
|
426
|
+
weight_kg: Optional[float] = Field(default=None, ge=0)
|
|
427
|
+
volume_liters: Optional[float] = Field(default=None, ge=0)
|
|
428
|
+
length_cm: Optional[float] = Field(default=None, ge=0)
|
|
429
|
+
width_cm: Optional[float] = Field(default=None, ge=0)
|
|
430
|
+
height_cm: Optional[float] = Field(default=None, ge=0)
|
|
431
|
+
|
|
432
|
+
model_config = {"str_strip_whitespace": True}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
class ProductComponent(BaseModel):
|
|
436
|
+
component_id: Optional[str] = Field(
|
|
437
|
+
None,
|
|
438
|
+
min_length=1,
|
|
439
|
+
description="System-generated product component identifier.",
|
|
440
|
+
)
|
|
441
|
+
product_id: Optional[str] = Field(
|
|
442
|
+
None,
|
|
443
|
+
min_length=1,
|
|
444
|
+
description="Parent product identifier for this component entry.",
|
|
445
|
+
)
|
|
446
|
+
part_number: str = Field(
|
|
447
|
+
...,
|
|
448
|
+
min_length=3,
|
|
449
|
+
max_length=50,
|
|
450
|
+
pattern=r"^[A-Z0-9\-_]+$",
|
|
451
|
+
description="Unique alphanumeric part identifier (uppercase, hyphens, underscores allowed)",
|
|
452
|
+
)
|
|
453
|
+
part_name: str = Field(..., min_length=2, max_length=200)
|
|
454
|
+
description: Optional[str] = Field(None, max_length=1000)
|
|
455
|
+
category: PartCategory
|
|
456
|
+
is_active: bool = Field(default=True)
|
|
457
|
+
created_date: date = Field(default_factory=date.today)
|
|
458
|
+
notes: Optional[str] = Field(None, max_length=2000)
|
|
459
|
+
|
|
460
|
+
# asset tag
|
|
461
|
+
asset_tag: Optional[str] = None
|
|
462
|
+
sku: Optional[str] = None
|
|
463
|
+
gtin: Optional[str] = None
|
|
464
|
+
upc: Optional[str] = None
|
|
465
|
+
ean: Optional[str] = None
|
|
466
|
+
cas_number: Optional[str] = None
|
|
467
|
+
|
|
468
|
+
# drawing association
|
|
469
|
+
revision: str = Field(default="A", max_length=10, description="Part revision/version")
|
|
470
|
+
drawing_number: Optional[str] = Field(None, max_length=50)
|
|
471
|
+
|
|
472
|
+
quantity: QuantityInfo
|
|
473
|
+
cost: CostInfo
|
|
474
|
+
supplier_name: str
|
|
475
|
+
material_spec: MaterialSpec
|
|
476
|
+
|
|
477
|
+
# ── Manufacturing-specific fields ─────────────────────────────────────────
|
|
478
|
+
scrap_and_yield: ScrapAndYield = Field(
|
|
479
|
+
default_factory=ScrapAndYield,
|
|
480
|
+
description="Scrap factors and process yield rates for consumption calculations.",
|
|
481
|
+
)
|
|
482
|
+
reference_designators: List[ReferenceDesignator] = Field(
|
|
483
|
+
default_factory=list,
|
|
484
|
+
description="PCB/electronics reference designators (e.g. R1, C12, U3A).",
|
|
485
|
+
)
|
|
486
|
+
shelf_life: Optional[ShelfLifeInfo] = Field(
|
|
487
|
+
None,
|
|
488
|
+
description="Shelf-life, floor-life, MSL, and storage condition requirements.",
|
|
489
|
+
)
|
|
490
|
+
hazmat: HazmatInfo = Field(
|
|
491
|
+
default_factory=HazmatInfo,
|
|
492
|
+
description="Hazardous material classification and RoHS/REACH compliance flags.",
|
|
493
|
+
)
|
|
494
|
+
ecn_history: List[ECNRecord] = Field(
|
|
495
|
+
default_factory=list,
|
|
496
|
+
description="Engineering Change Notices that have affected this part.",
|
|
497
|
+
)
|
|
498
|
+
parent_bom_id: Optional[str] = Field(
|
|
499
|
+
None,
|
|
500
|
+
max_length=50,
|
|
501
|
+
description="Part number of the immediate parent assembly in the BOM.",
|
|
502
|
+
)
|
|
503
|
+
bom_level: Optional[int] = Field(
|
|
504
|
+
None,
|
|
505
|
+
ge=1,
|
|
506
|
+
description="BOM depth level (1 = top-level assembly, 2 = subassembly, ...).",
|
|
507
|
+
)
|
|
508
|
+
quantity_per_assembly: Optional[Decimal] = Field(
|
|
509
|
+
None,
|
|
510
|
+
gt=0,
|
|
511
|
+
description="Net quantity of this part required per one parent assembly.",
|
|
512
|
+
)
|
|
513
|
+
find_number: Optional[int] = Field(
|
|
514
|
+
None,
|
|
515
|
+
ge=1,
|
|
516
|
+
description="Find number / item sequence on the engineering drawing BOM table.",
|
|
517
|
+
)
|
|
518
|
+
relationship_type: BOMRelationshipType = Field(default=BOMRelationshipType.ASSEMBLY)
|
|
519
|
+
is_configurable: bool = Field(
|
|
520
|
+
default=False,
|
|
521
|
+
description="Part may be substituted based on product configuration options.",
|
|
522
|
+
)
|
|
523
|
+
substitute_part_numbers: List[str] = Field(
|
|
524
|
+
default_factory=list,
|
|
525
|
+
description="Approved alternate part numbers that may be used in place of this part.",
|
|
526
|
+
)
|
|
527
|
+
effective_date_start: Optional[date] = Field(
|
|
528
|
+
None,
|
|
529
|
+
description="Date from which this BOM relationship is valid.",
|
|
530
|
+
)
|
|
531
|
+
effective_date_end: Optional[date] = Field(
|
|
532
|
+
None,
|
|
533
|
+
description="Date after which this BOM relationship is superseded.",
|
|
534
|
+
)
|
|
535
|
+
bom_notes: Optional[str] = Field(None, max_length=500)
|
|
536
|
+
|
|
537
|
+
model_config = {"str_strip_whitespace": True}
|
|
538
|
+
|
|
539
|
+
@field_validator("part_number", mode="before")
|
|
540
|
+
@classmethod
|
|
541
|
+
def uppercase_part_number(cls, v: str) -> str:
|
|
542
|
+
return v.upper() if isinstance(v, str) else v
|
|
543
|
+
|
|
544
|
+
@model_validator(mode="after")
|
|
545
|
+
def validate_bom_effectivity(self) -> "ProductComponent":
|
|
546
|
+
s, e = self.effective_date_start, self.effective_date_end
|
|
547
|
+
if s and e and s >= e:
|
|
548
|
+
raise ValueError("effective_date_start must be earlier than effective_date_end")
|
|
549
|
+
return self
|
|
550
|
+
|
|
551
|
+
# ── Helpers ───────────────────────────────────────────────────────────────
|
|
552
|
+
|
|
553
|
+
def total_stock_value(self) -> Decimal:
|
|
554
|
+
"""Returns total value of on-hand inventory."""
|
|
555
|
+
return self.quantity.quantity_on_hand * self.cost.unit_cost
|
|
556
|
+
|
|
557
|
+
def reorder_cost_estimate(self) -> Decimal:
|
|
558
|
+
"""Returns estimated cost of a reorder."""
|
|
559
|
+
qty = self.quantity.reorder_quantity
|
|
560
|
+
unit = self.cost.effective_unit_cost(qty)
|
|
561
|
+
return qty * unit
|
|
562
|
+
|
|
563
|
+
def gross_quantity_needed(self, net_quantity: Decimal) -> Decimal:
|
|
564
|
+
"""
|
|
565
|
+
Returns how much of this part must be issued to a work order
|
|
566
|
+
to obtain `net_quantity` good units, after scrap and yield losses.
|
|
567
|
+
"""
|
|
568
|
+
return self.scrap_and_yield.gross_quantity_for(net_quantity)
|
|
569
|
+
|
|
570
|
+
@property
|
|
571
|
+
def quantity_per_parent(self) -> float:
|
|
572
|
+
"""Compatibility alias for BOM node quantity resolution."""
|
|
573
|
+
if self.quantity_per_assembly is None:
|
|
574
|
+
return 1.0
|
|
575
|
+
return float(self.quantity_per_assembly)
|
|
576
|
+
|
|
577
|
+
@property
|
|
578
|
+
def latest_ecn(self) -> Optional[ECNRecord]:
|
|
579
|
+
"""Most recently initiated ECN, or None."""
|
|
580
|
+
if not self.ecn_history:
|
|
581
|
+
return None
|
|
582
|
+
return max(self.ecn_history, key=lambda e: e.date_initiated)
|
|
583
|
+
|
|
584
|
+
@property
|
|
585
|
+
def active_bom_links(self) -> List[BOMLink]:
|
|
586
|
+
"""Compatibility view over the flattened BOM input fields."""
|
|
587
|
+
if not (
|
|
588
|
+
self.parent_bom_id
|
|
589
|
+
and self.bom_level is not None
|
|
590
|
+
and self.quantity_per_assembly is not None
|
|
591
|
+
):
|
|
592
|
+
return []
|
|
593
|
+
link = BOMLink(
|
|
594
|
+
parent_bom_id=self.parent_bom_id,
|
|
595
|
+
bom_level=self.bom_level,
|
|
596
|
+
quantity_per_assembly=self.quantity_per_assembly,
|
|
597
|
+
find_number=self.find_number,
|
|
598
|
+
relationship_type=self.relationship_type,
|
|
599
|
+
is_configurable=self.is_configurable,
|
|
600
|
+
substitute_part_numbers=self.substitute_part_numbers,
|
|
601
|
+
effective_date_start=self.effective_date_start,
|
|
602
|
+
effective_date_end=self.effective_date_end,
|
|
603
|
+
notes=self.bom_notes,
|
|
604
|
+
)
|
|
605
|
+
return [link] if link.is_currently_effective else []
|
|
606
|
+
|
|
607
|
+
@property
|
|
608
|
+
def is_hazardous(self) -> bool:
|
|
609
|
+
return self.hazmat.is_hazardous
|
|
610
|
+
|
|
611
|
+
@property
|
|
612
|
+
def has_reference_designators(self) -> bool:
|
|
613
|
+
return len(self.reference_designators) > 0
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
class BOMComponent(BaseModel):
|
|
617
|
+
"""
|
|
618
|
+
A single line-item inside a BOM.
|
|
619
|
+
|
|
620
|
+
Fields merged from the production schema, multilevel tree support,
|
|
621
|
+
and MBOM enrichment fields.
|
|
622
|
+
"""
|
|
623
|
+
|
|
624
|
+
line_id: str = Field(..., min_length=1, description="Unique line identifier within this BOM")
|
|
625
|
+
component_product_id: str = Field(..., min_length=1, description="FK to the product catalogue")
|
|
626
|
+
component_sku: str = Field(default="")
|
|
627
|
+
component_name: str = Field(default="")
|
|
628
|
+
component_type: BOMComponentType
|
|
629
|
+
quantity_per_parent: float = Field(..., gt=0)
|
|
630
|
+
uom: str = Field(..., min_length=1, description="Unit of measure, e.g. 'Each', 'kg'")
|
|
631
|
+
usage_type: UsageType = UsageType.STANDARD
|
|
632
|
+
scrap_rate_pct: Optional[float] = Field(default=None, ge=0)
|
|
633
|
+
sequence_number: int = Field(..., ge=0, description="Assembly sequence order")
|
|
634
|
+
operation_step: Optional[int] = Field(default=None, ge=0, description="Work-centre operation ref")
|
|
635
|
+
effective_from: Optional[date] = None
|
|
636
|
+
effective_to: Optional[date] = None
|
|
637
|
+
material: Optional[str] = Field(
|
|
638
|
+
default=None,
|
|
639
|
+
description="Raw material spec, e.g. 'Western Red Cedar (1/4\" thick)'",
|
|
640
|
+
)
|
|
641
|
+
lead_time: Optional[LeadTimeStatus] = None
|
|
642
|
+
supplier: Optional[Supplier] = None
|
|
643
|
+
notes: Optional[str] = None
|
|
644
|
+
children: list["BOMNode"] = Field(
|
|
645
|
+
default_factory=list,
|
|
646
|
+
description="Sub-components; non-empty means this component is itself an assembly.",
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
@model_validator(mode="after")
|
|
650
|
+
def effective_to_after_from(self) -> "BOMComponent":
|
|
651
|
+
if self.effective_from and self.effective_to and self.effective_to < self.effective_from:
|
|
652
|
+
raise ValueError("effective_to must be on or after effective_from")
|
|
653
|
+
return self
|
|
654
|
+
|
|
655
|
+
@property
|
|
656
|
+
def is_assembly(self) -> bool:
|
|
657
|
+
"""True when this component contains sub-components (non-leaf node)."""
|
|
658
|
+
return len(self.children) > 0
|
|
659
|
+
|
|
660
|
+
@property
|
|
661
|
+
def net_quantity(self) -> float:
|
|
662
|
+
"""
|
|
663
|
+
Quantity adjusted for scrap rate:
|
|
664
|
+
net = quantity_per_parent / (1 - scrap_rate_pct / 100)
|
|
665
|
+
Falls back to quantity_per_parent when scrap_rate_pct is unset.
|
|
666
|
+
"""
|
|
667
|
+
if self.scrap_rate_pct:
|
|
668
|
+
return self.quantity_per_parent / (1 - self.scrap_rate_pct / 100)
|
|
669
|
+
return self.quantity_per_parent
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
class BOMNode(BaseModel):
|
|
673
|
+
"""
|
|
674
|
+
Positional wrapper around a BOMComponent.
|
|
675
|
+
|
|
676
|
+
Allows the same component to appear at multiple positions in the tree
|
|
677
|
+
with independent position labels and quantity overrides without
|
|
678
|
+
duplicating the component definition.
|
|
679
|
+
"""
|
|
680
|
+
|
|
681
|
+
component: BOMComponent
|
|
682
|
+
level: int = Field(default=0, ge=0, description="Depth in the BOM tree (0 = root)")
|
|
683
|
+
position: Optional[str] = Field(
|
|
684
|
+
default=None,
|
|
685
|
+
description="Dotted find-number within the parent assembly, e.g. '1.2.3'",
|
|
686
|
+
)
|
|
687
|
+
quantity_override: Optional[float] = Field(
|
|
688
|
+
default=None,
|
|
689
|
+
gt=0,
|
|
690
|
+
description=(
|
|
691
|
+
"Overrides component.quantity_per_parent for this specific "
|
|
692
|
+
"parent-context without modifying the master component record."
|
|
693
|
+
),
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
@property
|
|
697
|
+
def effective_quantity(self) -> float:
|
|
698
|
+
"""Resolved quantity at this node position."""
|
|
699
|
+
return (
|
|
700
|
+
self.quantity_override
|
|
701
|
+
if self.quantity_override is not None
|
|
702
|
+
else self.component.quantity_per_parent
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
class BOMBase(BaseModel):
|
|
707
|
+
"""Core BOM fields shared by Create, Update, and Record variants."""
|
|
708
|
+
|
|
709
|
+
bom_id: str = Field(..., min_length=1)
|
|
710
|
+
bom_code: str = Field(..., min_length=1)
|
|
711
|
+
version: str = Field(default="1.0", min_length=1)
|
|
712
|
+
parent_product_id: str = Field(..., min_length=1)
|
|
713
|
+
parent_product_name: str = ""
|
|
714
|
+
parent_sku: str = ""
|
|
715
|
+
bom_type: BOMType = BOMType.MANUFACTURING
|
|
716
|
+
status: BOMStatus = BOMStatus.DRAFT
|
|
717
|
+
effective_date: Optional[date] = None
|
|
718
|
+
expiry_date: Optional[date] = None
|
|
719
|
+
base_quantity: float = Field(default=1, gt=0)
|
|
720
|
+
base_uom: str = Field(default="Each", min_length=1)
|
|
721
|
+
routing_id: Optional[str] = None
|
|
722
|
+
approved_process_definition_id: Optional[str] = Field(
|
|
723
|
+
default=None,
|
|
724
|
+
description="Approved manufacturing/packaging process definition this BOM is authorized for.",
|
|
725
|
+
)
|
|
726
|
+
approved_process_definition_revision: Optional[str] = Field(
|
|
727
|
+
default=None,
|
|
728
|
+
description="Revision of the approved process definition captured when this BOM was released.",
|
|
729
|
+
)
|
|
730
|
+
approved_process_route_id: Optional[str] = Field(
|
|
731
|
+
default=None,
|
|
732
|
+
description="Approved process route/version used with this BOM.",
|
|
733
|
+
)
|
|
734
|
+
mbmr_template_id: Optional[str] = Field(
|
|
735
|
+
default=None,
|
|
736
|
+
description="Master Batch Manufacturing Record template bound to this BOM.",
|
|
737
|
+
)
|
|
738
|
+
bmr_template_id: Optional[str] = Field(
|
|
739
|
+
default=None,
|
|
740
|
+
description="Batch Manufacturing Record template generated/executed from this BOM.",
|
|
741
|
+
)
|
|
742
|
+
bpr_template_id: Optional[str] = Field(
|
|
743
|
+
default=None,
|
|
744
|
+
description="Batch Packaging Record template used when this BOM covers packaging.",
|
|
745
|
+
)
|
|
746
|
+
batch_execution_record_ids: list[str] = Field(
|
|
747
|
+
default_factory=list,
|
|
748
|
+
description="Batch execution records that have executed or instantiated this BOM.",
|
|
749
|
+
)
|
|
750
|
+
standard_work_hours: Optional[float] = Field(default=None, ge=0)
|
|
751
|
+
overall_scrap_rate_pct: Optional[float] = Field(default=None, ge=0)
|
|
752
|
+
expected_yield_pct: Optional[float] = Field(default=None, ge=0, le=100)
|
|
753
|
+
components: list[BOMComponent] = Field(default_factory=list)
|
|
754
|
+
revision_notes: Optional[str] = None
|
|
755
|
+
engineering_change_no: Optional[str] = None
|
|
756
|
+
remarks: Optional[str] = None
|
|
757
|
+
attachments: list[str] = Field(default_factory=list)
|
|
758
|
+
|
|
759
|
+
@model_validator(mode="after")
|
|
760
|
+
def expiry_after_effective(self) -> "BOMBase":
|
|
761
|
+
if self.effective_date and self.expiry_date and self.expiry_date < self.effective_date:
|
|
762
|
+
raise ValueError("expiry_date must be on or after effective_date")
|
|
763
|
+
if self.status == BOMStatus.ACTIVE:
|
|
764
|
+
if not (self.approved_process_definition_id or self.approved_process_route_id or self.routing_id):
|
|
765
|
+
raise ValueError("active BOMs must be bound to an approved process definition or route")
|
|
766
|
+
if not (self.mbmr_template_id or self.bmr_template_id or self.bpr_template_id):
|
|
767
|
+
raise ValueError("active BOMs must be bound to an MBMR, BMR, or BPR template")
|
|
768
|
+
return self
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
class BOMRecord(BOMBase):
|
|
772
|
+
"""
|
|
773
|
+
Persisted BOM document with audit data and computed tree metrics.
|
|
774
|
+
"""
|
|
775
|
+
|
|
776
|
+
is_current_version: bool = False
|
|
777
|
+
approval_date: Optional[datetime] = None
|
|
778
|
+
approved_by: Optional[str] = None
|
|
779
|
+
created_by: Optional[str] = None
|
|
780
|
+
updated_by: Optional[str] = None
|
|
781
|
+
created_at: datetime = Field(default_factory=utc_now)
|
|
782
|
+
updated_at: datetime = Field(default_factory=utc_now)
|
|
783
|
+
estimated_material_cost: Optional[float] = None
|
|
784
|
+
total_component_lines: int = 0
|
|
785
|
+
is_multi_level: bool = False
|
|
786
|
+
has_alternates: bool = False
|
|
787
|
+
is_valid: bool = False
|
|
788
|
+
is_production_bound: bool = False
|
|
789
|
+
raw_materials: list[BOMComponent] = Field(
|
|
790
|
+
default_factory=list,
|
|
791
|
+
description="All raw material component lines in this BOM, collected across every BOM level.",
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
@model_validator(mode="after")
|
|
795
|
+
def normalize_and_compute(self) -> "BOMRecord":
|
|
796
|
+
self.created_at = ensure_utc(self.created_at)
|
|
797
|
+
self.updated_at = ensure_utc(self.updated_at)
|
|
798
|
+
self.approval_date = ensure_utc(self.approval_date)
|
|
799
|
+
|
|
800
|
+
all_components = self._walk_all_components()
|
|
801
|
+
self.total_component_lines = len(all_components)
|
|
802
|
+
self.is_multi_level = any(c.is_assembly for c in self.components)
|
|
803
|
+
self.has_alternates = any(
|
|
804
|
+
c.usage_type in {UsageType.ALTERNATE, UsageType.SUBSTITUTE}
|
|
805
|
+
for c in all_components
|
|
806
|
+
)
|
|
807
|
+
self.raw_materials = [
|
|
808
|
+
component
|
|
809
|
+
for component in all_components
|
|
810
|
+
if component.component_type == BOMComponentType.RAW_MATERIAL
|
|
811
|
+
]
|
|
812
|
+
|
|
813
|
+
today = datetime.now(timezone.utc).date()
|
|
814
|
+
effective = self.effective_date is None or self.effective_date <= today
|
|
815
|
+
unexpired = self.expiry_date is None or self.expiry_date >= today
|
|
816
|
+
self.is_valid = self.status == BOMStatus.ACTIVE and effective and unexpired
|
|
817
|
+
self.is_production_bound = bool(
|
|
818
|
+
self.approved_process_definition_id
|
|
819
|
+
or self.approved_process_route_id
|
|
820
|
+
or self.routing_id
|
|
821
|
+
) and bool(
|
|
822
|
+
self.mbmr_template_id
|
|
823
|
+
or self.bmr_template_id
|
|
824
|
+
or self.bpr_template_id
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
return self
|
|
828
|
+
|
|
829
|
+
def _walk_all_components(self) -> list[BOMComponent]:
|
|
830
|
+
"""DFS walk returning every component in the BOM tree."""
|
|
831
|
+
result: list[BOMComponent] = []
|
|
832
|
+
|
|
833
|
+
def _recurse(component: BOMComponent) -> None:
|
|
834
|
+
result.append(component)
|
|
835
|
+
for node in component.children:
|
|
836
|
+
_recurse(node.component)
|
|
837
|
+
|
|
838
|
+
for component in self.components:
|
|
839
|
+
_recurse(component)
|
|
840
|
+
return result
|
|
841
|
+
|
|
842
|
+
def flat_list(self) -> list[tuple[int, str, BOMComponent]]:
|
|
843
|
+
"""DFS walk as (level, position, component) tuples."""
|
|
844
|
+
result: list[tuple[int, str, BOMComponent]] = []
|
|
845
|
+
|
|
846
|
+
def _recurse(component: BOMComponent, level: int, position: str) -> None:
|
|
847
|
+
result.append((level, position, component))
|
|
848
|
+
for idx, node in enumerate(component.children, start=1):
|
|
849
|
+
child_position = node.position or f"{position}.{idx}"
|
|
850
|
+
_recurse(node.component, level + 1, child_position)
|
|
851
|
+
|
|
852
|
+
for idx, component in enumerate(self.components, start=1):
|
|
853
|
+
_recurse(component, 0, str(idx))
|
|
854
|
+
return result
|
|
855
|
+
|
|
856
|
+
def find_component(self, *, product_id: str) -> list[BOMComponent]:
|
|
857
|
+
"""Find every component with the given product id, regardless of depth."""
|
|
858
|
+
return [
|
|
859
|
+
component
|
|
860
|
+
for component in self._walk_all_components()
|
|
861
|
+
if component.component_product_id == product_id
|
|
862
|
+
]
|
|
863
|
+
|
|
864
|
+
def max_depth(self) -> int:
|
|
865
|
+
"""Maximum nesting depth, where 0 is a flat BOM."""
|
|
866
|
+
depths: list[int] = []
|
|
867
|
+
|
|
868
|
+
def _recurse(component: BOMComponent, depth: int) -> None:
|
|
869
|
+
depths.append(depth)
|
|
870
|
+
for node in component.children:
|
|
871
|
+
_recurse(node.component, depth + 1)
|
|
872
|
+
|
|
873
|
+
for component in self.components:
|
|
874
|
+
_recurse(component, 0)
|
|
875
|
+
return max(depths, default=0)
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
class BOMCreate(BOMBase):
|
|
879
|
+
"""Payload for creating a new BOM."""
|
|
880
|
+
|
|
881
|
+
created_by: Optional[str] = None
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
class BOMUpdate(BaseModel):
|
|
885
|
+
"""Partial-update payload for stored BOM records."""
|
|
886
|
+
|
|
887
|
+
bom_code: Optional[str] = None
|
|
888
|
+
version: Optional[str] = None
|
|
889
|
+
parent_product_id: Optional[str] = None
|
|
890
|
+
parent_product_name: Optional[str] = None
|
|
891
|
+
parent_sku: Optional[str] = None
|
|
892
|
+
bom_type: Optional[BOMType] = None
|
|
893
|
+
status: Optional[BOMStatus] = None
|
|
894
|
+
is_current_version: Optional[bool] = None
|
|
895
|
+
effective_date: Optional[date] = None
|
|
896
|
+
expiry_date: Optional[date] = None
|
|
897
|
+
approval_date: Optional[datetime] = None
|
|
898
|
+
approved_by: Optional[str] = None
|
|
899
|
+
base_quantity: Optional[float] = Field(default=None, gt=0)
|
|
900
|
+
base_uom: Optional[str] = None
|
|
901
|
+
routing_id: Optional[str] = None
|
|
902
|
+
approved_process_definition_id: Optional[str] = None
|
|
903
|
+
approved_process_definition_revision: Optional[str] = None
|
|
904
|
+
approved_process_route_id: Optional[str] = None
|
|
905
|
+
mbmr_template_id: Optional[str] = None
|
|
906
|
+
bmr_template_id: Optional[str] = None
|
|
907
|
+
bpr_template_id: Optional[str] = None
|
|
908
|
+
batch_execution_record_ids: Optional[list[str]] = None
|
|
909
|
+
standard_work_hours: Optional[float] = Field(default=None, ge=0)
|
|
910
|
+
overall_scrap_rate_pct: Optional[float] = Field(default=None, ge=0)
|
|
911
|
+
expected_yield_pct: Optional[float] = Field(default=None, ge=0, le=100)
|
|
912
|
+
components: Optional[list[BOMComponent]] = None
|
|
913
|
+
revision_notes: Optional[str] = None
|
|
914
|
+
engineering_change_no: Optional[str] = None
|
|
915
|
+
remarks: Optional[str] = None
|
|
916
|
+
attachments: Optional[list[str]] = None
|
|
917
|
+
estimated_material_cost: Optional[float] = None
|
|
918
|
+
updated_by: Optional[str] = None
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
class PaginatedBOMResponse(BaseModel):
|
|
922
|
+
total: int
|
|
923
|
+
page: int
|
|
924
|
+
page_size: int
|
|
925
|
+
results: list[BOMRecord]
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
class ProductComponentCreate(ProductComponent):
|
|
929
|
+
pass
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
class ProductComponentUpdate(BaseModel):
|
|
933
|
+
product_id: Optional[str] = Field(None, min_length=1)
|
|
934
|
+
part_number: Optional[str] = Field(
|
|
935
|
+
None,
|
|
936
|
+
min_length=3,
|
|
937
|
+
max_length=50,
|
|
938
|
+
pattern=r"^[A-Z0-9\-_]+$",
|
|
939
|
+
description="Unique alphanumeric part identifier (uppercase, hyphens, underscores allowed)",
|
|
940
|
+
)
|
|
941
|
+
part_name: Optional[str] = Field(None, min_length=2, max_length=200)
|
|
942
|
+
description: Optional[str] = Field(None, max_length=1000)
|
|
943
|
+
category: Optional[PartCategory] = None
|
|
944
|
+
is_active: Optional[bool] = None
|
|
945
|
+
created_date: Optional[date] = None
|
|
946
|
+
notes: Optional[str] = None
|
|
947
|
+
asset_tag: Optional[str] = None
|
|
948
|
+
sku: Optional[str] = None
|
|
949
|
+
gtin: Optional[str] = None
|
|
950
|
+
upc: Optional[str] = None
|
|
951
|
+
ean: Optional[str] = None
|
|
952
|
+
cas_number: Optional[str] = None
|
|
953
|
+
revision: Optional[str] = Field(None, max_length=10)
|
|
954
|
+
drawing_number: Optional[str] = Field(None, max_length=50)
|
|
955
|
+
quantity: Optional[QuantityInfo] = None
|
|
956
|
+
cost: Optional[CostInfo] = None
|
|
957
|
+
supplier_name: Optional[str] = None
|
|
958
|
+
material_spec: Optional[MaterialSpec] = None
|
|
959
|
+
scrap_and_yield: Optional[ScrapAndYield] = None
|
|
960
|
+
reference_designators: Optional[List[ReferenceDesignator]] = Field(
|
|
961
|
+
None,
|
|
962
|
+
description="PCB/electronics reference designators (e.g. R1, C12, U3A).",
|
|
963
|
+
)
|
|
964
|
+
shelf_life: Optional[ShelfLifeInfo] = None
|
|
965
|
+
hazmat: Optional[HazmatInfo] = None
|
|
966
|
+
ecn_history: Optional[List[ECNRecord]] = Field(
|
|
967
|
+
None,
|
|
968
|
+
description="Engineering Change Notices that have affected this part.",
|
|
969
|
+
)
|
|
970
|
+
parent_bom_id: Optional[str] = Field(
|
|
971
|
+
None,
|
|
972
|
+
max_length=50,
|
|
973
|
+
description="Part number of the immediate parent assembly in the BOM.",
|
|
974
|
+
)
|
|
975
|
+
bom_level: Optional[int] = Field(
|
|
976
|
+
None,
|
|
977
|
+
ge=1,
|
|
978
|
+
description="BOM depth level (1 = top-level assembly, 2 = subassembly, ...).",
|
|
979
|
+
)
|
|
980
|
+
quantity_per_assembly: Optional[Decimal] = Field(
|
|
981
|
+
None,
|
|
982
|
+
gt=0,
|
|
983
|
+
description="Net quantity of this part required per one parent assembly.",
|
|
984
|
+
)
|
|
985
|
+
find_number: Optional[int] = Field(
|
|
986
|
+
None,
|
|
987
|
+
ge=1,
|
|
988
|
+
description="Find number / item sequence on the engineering drawing BOM table.",
|
|
989
|
+
)
|
|
990
|
+
relationship_type: Optional[BOMRelationshipType] = None
|
|
991
|
+
is_configurable: Optional[bool] = Field(
|
|
992
|
+
None,
|
|
993
|
+
description="Part may be substituted based on product configuration options.",
|
|
994
|
+
)
|
|
995
|
+
substitute_part_numbers: Optional[List[str]] = Field(
|
|
996
|
+
None,
|
|
997
|
+
description="Approved alternate part numbers that may be used in place of this part.",
|
|
998
|
+
)
|
|
999
|
+
effective_date_start: Optional[date] = Field(
|
|
1000
|
+
None,
|
|
1001
|
+
description="Date from which this BOM relationship is valid.",
|
|
1002
|
+
)
|
|
1003
|
+
effective_date_end: Optional[date] = Field(
|
|
1004
|
+
None,
|
|
1005
|
+
description="Date after which this BOM relationship is superseded.",
|
|
1006
|
+
)
|
|
1007
|
+
bom_notes: Optional[str] = Field(None, max_length=500)
|
|
1008
|
+
|
|
1009
|
+
@field_validator("part_number", mode="before")
|
|
1010
|
+
@classmethod
|
|
1011
|
+
def uppercase_part_number(cls, v: Optional[str]) -> Optional[str]:
|
|
1012
|
+
return v.upper() if isinstance(v, str) else v
|
|
1013
|
+
|
|
1014
|
+
@model_validator(mode="after")
|
|
1015
|
+
def validate_bom_effectivity(self) -> "ProductComponentUpdate":
|
|
1016
|
+
s, e = self.effective_date_start, self.effective_date_end
|
|
1017
|
+
if s and e and s >= e:
|
|
1018
|
+
raise ValueError("effective_date_start must be earlier than effective_date_end")
|
|
1019
|
+
return self
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
class PaginatedProductComponentResponse(BaseModel):
|
|
1023
|
+
total: int
|
|
1024
|
+
page: int
|
|
1025
|
+
page_size: int
|
|
1026
|
+
results: list[ProductComponent]
|
|
1027
|
+
|
|
1028
|
+
ProductComponentRecord = ProductComponent
|