- Documentation
- /
- Work Management
- /
- 03 — Integration
03 — Integration
Work Management is a member of the R9 Work & Supply network (message protocol v1). Its
contract is written in schema/work_management.integration.dsl (authoring spec:
docs/implementation/integration_authoring_spec.md) and turned into code by
python -m codegen.cli integration work_management --network work_supply; the business
logic for each message is hand-written in integration/handlers.py.
The whole network — every message with its publisher, subscribers and implementation
status, generated from the contracts — is in networks/work_supply/NETWORK_MAP.md
(python -m codegen.cli network-docs work_supply).
What it owns
| Network entity | Model | Business reference |
|---|---|---|
work.work_order |
WorkOrder | work_order_number (WO-…) |
work.material_requirement |
WorkMaterialRequirement | requirement_number (MR-…) |
work.removed_component / work.installed_component |
RemovedComponent / InstalledComponent | — |
work.maintenance_template |
MaintenanceTemplate | code (found with work.search_maintenance_templates) |
Every owned record has a network id (cid, assigned on create, never changes). Other
applications refer to it by that id and show its business reference.
Messages
| Kind | Message | When / what | Status |
|---|---|---|---|
| publishes | work.work_order_created |
a work order is created | ✅ automatic |
| publishes | work.material_requirement_created |
a material requirement is created | ✅ automatic |
| publishes | work.material_requirement_changed |
a requirement's status changes | ✅ automatic |
| publishes | work.work_started / work_suspended / work_closed |
work order status moves | ✅ automatic |
| publishes | work.late_cost_recorded |
a cost line added to an order that already closed (parts, bought material, contractor work or a late time entry): the order, the cost type, the amount | ✅ automatic |
| publishes | work.returned_to_service / work_verified |
completion records | ✅ automatic |
| publishes | work.component_removed / work.component_installed |
a removal / an installation, with the date and the unit's main SMU (main_smu_value, main_smu_unit, main_smu_metric_cid/ref; blank = the last known reading applies). The removal also keeps the old removal_usage_value / removal_usage_unit keys for one release |
✅ automatic |
| publishes | work.work_closed |
also says oil_changed (the order came from an oil_change template) with the template's SMCS code and the template reference, and the cost: actual_cost split into cost_labour, cost_parts, cost_purchased, cost_contractor (cost_currency) |
✅ automatic |
| consumes | inventory.item_reserved |
set the requirement reserved, fill in the reservation | ✅ |
| consumes | inventory.item_issued |
a parts cost line (quantity x the stock's average cost) and the requirement becomes issued | ✅ |
| consumes | inventory.item_unavailable |
set it unavailable; hold a critical order awaiting_parts | ✅ |
| consumes | purchasing.delivery_commitment_changed / delivery_delayed |
expected date, readiness | ⏳ R10 |
| consumes | warranty.candidate_detected, governance.obligation_assigned |
warranty flag, external dependency | ⏳ later |
| consumes | workforce.employee_registered / employee_changed |
create or update the read-only Person copy (same cid as the employee: name, number, position, trade, status); for a contractor's person find or create the contractor Organisation carrying the supplier number (supplier_cid / supplier_ref, code = the supplier number) |
✅ |
| consumes | workforce.employee_left |
the Person copy becomes inactive (nothing is deleted) | ✅ |
| accepts | work.create_material_requirement |
create a requirement (rejects closed / unknown orders) | ✅ |
| accepts | work.create_work_order |
with a template, the work type comes from it, the priority defaults to planned, and its tasks and tools are copied | ✅ |
| accepts | work.record_component_failure, work.record_component_installation |
a removal / an installation against the order | ✅ |
| accepts | work.verify_and_close |
record the verification; when accepted and the closure guards hold (the order is in progress, in test, returned to service or ready for verification, and no task is in progress or failed) write the closeout and close the order | ✅ |
| accepts | work.start_work |
start the work: in progress, start date set (publishes work_started); already started is answered as it is; awaiting parts, closed or cancelled cannot start |
✅ |
| accepts | work.record_time |
time worked on the order, by person (employee number or name), hours, time type and trade (trade_cid or the trade's code trade_code; none: the person's trade from the copy), stamped on the entry; negative hours correct an earlier entry; on a closed order it is a late cost line; an unknown trade is rejected (unknown_trade) |
✅ |
| accepts | work.reopen_work_order |
a closed order goes back to in progress (rejects not closed); closing it again re-costs it and Finance posts the difference as an amendment | ✅ |
| accepts | work.record_job_cost |
one purchased or contractor cost line (Purchasing sends one per received order line); a source already recorded is not recorded twice | ✅ |
| accepts | review_material_delay, update_external_constraint, return_to_service |
⏳ not yet (answers handler not found) | |
| answers | work.open_work_orders |
orders not closed or cancelled (or in one status), oldest first, at most 200 | ✅ |
| answers | work.work_order_status, work.material_requirement_status, work.search_maintenance_templates |
read-only status; the template lookup | ✅ |
| answers | work.readiness_status, work.asset_work_history |
⏳ not yet |
Published events are written in the same transaction as the business change (CRUD screen or message handler alike) and delivered afterwards by the publisher, so nothing is lost or sent twice. Inbound messages are de-duplicated by message id; commands also by their idempotency key.
References to other applications
| Points at | Owner | Fields |
|---|---|---|
asset.asset (units) and asset.component (serialised components) |
asset register | asset_cid / asset_ref; component_cid / component_ref (sent in messages as component_asset_cid / component_asset_ref) … |
asset.asset_group, classification.smcs, classification.modifier, lifecycle.usage_metric_type |
asset register, classification, lifecycle | template group and SMCS; fitment SMCS and modifier; the SMU metric (main_smu_usage_metric_type_cid, sent as main_smu_metric_cid) |
catalogue.item |
catalogue (R10) | catalogue_item_cid / catalogue_item_ref |
inventory.reservation |
inventory | inventory_reservation_cid / inventory_reservation_ref — linked ↗ |
purchasing.purchase_requirement |
purchasing (R10) | purchase_requirement_cid / purchase_requirement_ref |
warranty.candidate |
warranty (later) | warranty_candidate_cid / warranty_candidate_ref |
workforce.trade |
workforce (W1) | trade_cid / trade_ref on Person (the copy), WorkAssignment and WorkTimeEntry — picked with workforce.search_trades; the code follows corrections |
supplier.supplier |
supplier | supplier_cid / supplier_ref on Organisation: the supplier number of a contractor company |
People: a read-only copy of Workforce (W1)
Work's Person table is Workforce's employees, kept by the employee events: a Person's cid is the employee's cid, so every
…_person_id in Work (planner, supervisor, assigned, approved by, verified by …) keeps pointing at a local row while the person is owned by
Workforce. Person and Organisation are view-only on Work's screens (config/model_permissions.yaml); only the events change them. The trade a
person worked in is stamped on each time entry, so a later change of trade never changes a past cost. Until W2 the labour rate is looked up in
Work's Labour Rate table with the trade's code (lower case) where it used to take the labour class; the figures are unchanged.
Endpoints
| Endpoint | For | Protection |
|---|---|---|
PUT /integration/messages/<id> |
the orchestrator delivering messages | service token |
GET /resolve/<cid> |
people following a ↗ link from another app | normal login |
Settings (applications.yaml env:)
INTEGRATION_PUBLISHER=1, ORCHESTRATOR_URL=http://localhost:5089 (the R9 orchestrator);
ORCHESTRATOR_SERVICE_TOKEN from .env. The publisher starts on the app's first request.