Skip to content

feat(bom): multi-level BOMs with recursive requirement explosion - #211

Merged
jakub-przepiora merged 2 commits into
developfrom
feature/178-hierarchical-bom
Jul 28, 2026
Merged

feat(bom): multi-level BOMs with recursive requirement explosion#211
jakub-przepiora merged 2 commits into
developfrom
feature/178-hierarchical-bom

Conversation

@JanKolo04

@JanKolo04 JanKolo04 commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

A BOM line could only reference a purchased material, so products built from in-house subassemblies couldn't express
their real structure and MRP stopped at the first level.

Adds make-or-buy to the item master (materials.is_manufactured + producing_process_template_id): a line pointing
at a manufactured material expands into that material's own BOM, to any depth. bom_items is untouched, so existing
single-level BOMs and API clients keep working.

Quantities and scrap compound down the tree; a component reached by several paths is summed once. MRP now plans
against exploded leaves. Circular references are rejected at both entry points (BOM line and item master) as a 422,
with a depth backstop.

New App\Services\Material\BomExplosionServicetree() and leafRequirements().

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation
  • Other:

Related issue

Refs #178does not close it. Lands the model, recursion and safety rails. Deferred: UI tree, REST endpoints,
work-order snapshotting, cross-level genealogy, parent/child work orders.

Testing

14 new tests in tests/Feature/Material/HierarchicalBomTest.php: three levels created and exploded, leaf quantities
through the hierarchy, scrap compounding, shared components summed once, five acyclic levels, direct + indirect cycles
rejected, item-master mirror rejected, and a backward-compat check that a single-level BOM gives numbers identical to
calculateRequirements. Migration rollback and query cost verified separately.

  • Tested manually in browser — no UI in this PR
  • php artisan test passes — 1984 tests, 0 failures (via php -d memory_limit=2G vendor/bin/phpunit; the
    artisan test wrapper OOMs locally at the default 128M limit, unrelated to this change)
  • Tested as Operator / Supervisor / Admin role — n/a, no UI or route changes

Checklist

  • No .env secrets committed
  • Migration added if schema changed — additive, down() verified
  • $fillable updated if new model columns added
  • No raw SQL with user input
  • CSRF protection in place for any new forms — no new forms
  • composer audit clean — 37 pre-existing advisories; no dependencies added or changed here

Notes for reviewers

  • MRP resolves subassemblies into components rather than netting them against their own stock — deeper MRP concern,
    deliberately out of scope (noted in code).
  • The item-master cycle guard sits in a model saving hook so every write path is covered without duplicating it
    across four Form Requests.
  • materials was not added to the Electric shape — fine with no UI, but required when the tree view lands.

Summary by CodeRabbit

  • New Features

    • Added support for multi-level BOMs with manufactured subassemblies.
    • MRP now calculates requirements through subassemblies down to purchased materials.
    • Added make-or-buy material settings and production template associations.
    • BOM quantities and scrap are compounded across nested levels.
  • Bug Fixes

    • Circular BOM references are now rejected with localized validation messages.
    • Existing single-level BOM behavior remains compatible.

A BOM line could only reference a purchased material, so products built from
in-house subassemblies had no way to express their real structure and MRP
stopped at the first level.

Adds make-or-buy to the item master — materials.is_manufactured plus
producing_process_template_id — so a line pointing at a manufactured material
expands into that material's own BOM, to any depth. bom_items is untouched,
which is what keeps existing single-level BOMs and API clients working: a BOM
with no manufactured components explodes to exactly the numbers the flat
calculation produced (asserted).

Quantities and scrap compound down the tree (qty_per_unit x parent requirement
x (1 + scrap%)), and a component reached by several paths is summed once.
NetRequirementsService now plans against exploded leaves rather than level one.

Circular references are rejected at both entry points — a BOM line that would
make a template a component of itself, and an item master pointing its
producing template at a template that already consumes it — each as a 422 with
a translated message, plus a depth backstop so pre-existing bad data cannot
hang a request.

New App\Services\Material\BomExplosionService: tree() for the nested structure,
leafRequirements() for accumulated leaves.

Out of scope here (follow-ups): UI tree view, REST endpoints for the hierarchy,
work-order snapshotting of the structure, cross-level genealogy, and parent /
child work orders.

Refs #178

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YbLa6GeJZACX3tFCE7iNJq
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c70647d6-6b7d-4854-9b48-86041884bc5a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds hierarchical BOM support through manufactured-material metadata, recursive explosion with compounded quantities and scrap, cycle prevention, leaf-based MRP requirements, backward-compatible defaults, and feature coverage for nested structures and validation failures.

Changes

Hierarchical BOM support

Layer / File(s) Summary
Material manufacturing contracts
backend/app/Models/Material.php, backend/app/Http/Requests/.../MaterialRequest.php, backend/database/migrations/*, backend/database/factories/MaterialFactory.php
Material validation, persistence, defaults, relationships, scopes, and factory states now support manufactured materials linked to producing process templates.
Recursive BOM explosion and cycle detection
backend/app/Services/Material/BomExplosionService.php
Adds nested BOM trees, aggregated leaf requirements, compounded quantities and scrap, bounded recursion, and template cycle checks.
BOM validation and MRP integration
backend/app/Services/Material/BomService.php, backend/app/Services/Material/NetRequirementsService.php, backend/lang/*.json
BOM item changes reject circular references, and MRP requirements are calculated from exploded leaf materials.
Hierarchical BOM coverage
backend/tests/Feature/Material/HierarchicalBomTest.php, CHANGELOG.md
Tests cover nesting, quantities, scrap, shared leaves, cycle rejection, legacy defaults, flat-BOM compatibility, and MRP propagation; the feature is documented in the changelog.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProcessTemplate
  participant BomExplosionService
  participant Material
  participant NetRequirementsService
  ProcessTemplate->>BomExplosionService: expand BOM tree
  BomExplosionService->>Material: resolve manufactured components
  BomExplosionService-->>NetRequirementsService: aggregated leaf requirements
  NetRequirementsService-->>ProcessTemplate: planned leaf material demand
Loading

Possibly related issues

  • [feat] Hierarchical BOM #178 — Covers the hierarchical BOM requirements implemented by this pull request, including recursive explosion, cycle detection, scrap propagation, MRP leaf requirements, and compatibility.

Possibly related PRs

  • Mes-Open/OpenMes#139 — Directly relates to using BomExplosionService->leafRequirements() in NetRequirementsService.

Suggested reviewers: jakub-przepiora, svannte

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: multi-level BOM support with recursive requirement explosion.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/178-hierarchical-bom

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@JanKolo04

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/app/Models/Material.php (1)

21-45: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Register the new manufacturing columns on materials in ShapeRegistry.php.

Backend\App\Sync\ShapeRegistry maps materials only through existing columns; is_manufactured and producing_process_template_id are missing from its columns list, so synced clients won’t be able to read/write them. Add both columns there, or remove/ignore the PR guidance if materials is not a shape-synced entity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/Models/Material.php` around lines 21 - 45, Update
Backend\App\Sync\ShapeRegistry’s materials column mapping to include both
is_manufactured and producing_process_template_id, ensuring shape-synced clients
can read and write the new manufacturing fields. If materials is intentionally
not shape-synced, remove or ignore the related PR guidance instead.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/app/Http/Requests/Api/V1/StoreMaterialRequest.php`:
- Around line 25-26: Enforce the make-or-buy invariant in the material
validation rules: a non-null producing_process_template_id must require an
effective is_manufactured=true value, or normalize/reject the relationship when
manufacturing is false or absent. Apply this to creation in
backend/app/Http/Requests/Api/V1/StoreMaterialRequest.php lines 25-26 and
backend/app/Http/Requests/Web/Admin/StoreMaterialRequest.php lines 47-48; for
partial updates, use the existing material state in
backend/app/Http/Requests/Api/V1/UpdateMaterialRequest.php lines 26-27 and
backend/app/Http/Requests/Web/Admin/UpdateMaterialRequest.php lines 51-52.

In `@backend/app/Models/Material.php`:
- Around line 139-156: Update Material::isExplodable() to return true only when
the material is manufactured, has a producing_process_template_id, and
producingTemplate resolves to a non-deleted ProcessTemplate; add regression
coverage confirming materials referencing soft-deleted templates are treated as
non-explodable and do not reach explode(ProcessTemplate $template) with null.

---

Outside diff comments:
In `@backend/app/Models/Material.php`:
- Around line 21-45: Update Backend\App\Sync\ShapeRegistry’s materials column
mapping to include both is_manufactured and producing_process_template_id,
ensuring shape-synced clients can read and write the new manufacturing fields.
If materials is intentionally not shape-synced, remove or ignore the related PR
guidance instead.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c05bcce-ad20-4ec9-abb7-6df0a474d566

📥 Commits

Reviewing files that changed from the base of the PR and between c8e30ee and 56131f0.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • backend/app/Http/Requests/Api/V1/StoreMaterialRequest.php
  • backend/app/Http/Requests/Api/V1/UpdateMaterialRequest.php
  • backend/app/Http/Requests/Web/Admin/StoreMaterialRequest.php
  • backend/app/Http/Requests/Web/Admin/UpdateMaterialRequest.php
  • backend/app/Models/Material.php
  • backend/app/Services/Material/BomExplosionService.php
  • backend/app/Services/Material/BomService.php
  • backend/app/Services/Material/NetRequirementsService.php
  • backend/database/factories/MaterialFactory.php
  • backend/database/migrations/2026_07_26_100000_add_manufacturing_to_materials_table.php
  • backend/lang/en.json
  • backend/lang/pl.json
  • backend/tests/Feature/Material/HierarchicalBomTest.php

Comment thread backend/app/Http/Requests/Api/V1/StoreMaterialRequest.php
Comment thread backend/app/Models/Material.php
- isExplodable() only checked the FK, but process templates soft-delete and
  nullOnDelete doesn't fire on a soft delete — so a soft-deleted producing
  template left the id set while the relation resolved to null, and explosion
  descended into a null template (TypeError). It now requires the relation to
  resolve; such a material falls back to a leaf. Reproduced with a failing test
  first, which is in this commit.
- Added is_manufactured and producing_process_template_id to the materials
  Electric shape, per the project rule that a new column on a synced table must
  be allowlisted or the UI never sees it. Guarded by a test.

Not applied as suggested: CodeRabbit wanted the four material Form Requests to
reject a producing template when is_manufactured is false. That combination is
meaningful — a make-or-buy item can be bought for now while its routing stays
on file — and explosion requires both halves, so neither on its own yields an
inconsistent state. Documented the intent in the migration and locked it with a
test instead of adding validation that would break the dual-source case.

Refs #178

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YbLa6GeJZACX3tFCE7iNJq
@JanKolo04

Copy link
Copy Markdown
Collaborator Author

@jakub-przepiora ready to review

@jakub-przepiora
jakub-przepiora merged commit ca0258d into develop Jul 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants