ultimatepos/Modules/IndustrialEngineering/Services/BomService.php

172 lines
6.4 KiB
PHP

<?php
namespace Modules\IndustrialEngineering\Services;
use Illuminate\Support\Collection;
use Modules\IndustrialEngineering\Models\Bom;
use Modules\IndustrialEngineering\Models\BomLine;
class BomService
{
/**
* Explode a multi-level BOM into a flat list with rolled-up quantities.
*/
public function explode(Bom $bom, float $parentQty = 1.0, ?int $parentLineId = null): Collection
{
$lines = $bom->lines()
->with(['itemMaster', 'alternates.alternateItem', 'children'])
->when($parentLineId === null, fn ($q) => $q->whereNull('parent_line_id'))
->when($parentLineId !== null, fn ($q) => $q->where('parent_line_id', $parentLineId))
->orderBy('sort_order')
->get();
$result = collect();
foreach ($lines as $line) {
$qty = (float) $line->quantity * $parentQty;
$result->push([
'bom_line_id' => $line->id,
'parent_line_id' => $line->parent_line_id,
'item_master_id' => $line->item_master_id,
'item_code' => $line->itemMaster?->item_code,
'item_name' => $line->itemMaster?->name,
'position_code' => $line->position_code,
'quantity' => $qty,
'unit_cost' => (float) ($line->standard_cost ?: $line->itemMaster?->standard_cost ?? 0),
'extended_cost' => $qty * (float) ($line->standard_cost ?: $line->itemMaster?->standard_cost ?? 0),
'alternate_group' => $line->alternate_group,
'alternates' => $line->alternates->map(fn ($alt) => [
'item_id' => $alt->alternate_item_id,
'item_code' => $alt->alternateItem?->item_code,
'item_name' => $alt->alternateItem?->name,
'priority' => $alt->priority,
'cost_delta' => (float) $alt->cost_delta,
'is_approved' => $alt->is_approved,
])->values(),
'is_serviceable' => $line->is_serviceable,
'lead_time_days' => $line->lead_time_days ?: $line->itemMaster?->lead_time_days ?? 0,
]);
$children = $this->explode($bom, $qty, $line->id);
$result = $result->merge($children);
}
return $result;
}
/**
* Explode BOM to leaf components only (no double-counting assemblies).
*/
public function explodeLeaves(Bom $bom, float $parentQty = 1.0, ?int $parentLineId = null): Collection
{
$lines = $bom->lines()
->with(['itemMaster', 'children'])
->when($parentLineId === null, fn ($q) => $q->whereNull('parent_line_id'))
->when($parentLineId !== null, fn ($q) => $q->where('parent_line_id', $parentLineId))
->orderBy('sort_order')
->get();
$result = collect();
foreach ($lines as $line) {
$qty = (float) $line->quantity * $parentQty;
if ($line->children()->exists()) {
$result = $result->merge($this->explodeLeaves($bom, $qty, $line->id));
} else {
$result->push([
'bom_line_id' => $line->id,
'item_master_id' => $line->item_master_id,
'item_code' => $line->itemMaster?->item_code,
'item_name' => $line->itemMaster?->name,
'quantity' => $qty,
'unit_cost' => (float) ($line->standard_cost ?: $line->itemMaster?->standard_cost ?? 0),
]);
}
}
return $result;
}
/**
* Build hierarchical tree for UI display.
*/
public function buildTree(Bom $bom): array
{
$bom->load(['rootLines.itemMaster', 'rootLines.alternates.alternateItem', 'rootLines.children']);
return $bom->rootLines->map(fn (BomLine $line) => $this->lineToTreeNode($line))->values()->all();
}
protected function lineToTreeNode(BomLine $line): array
{
$line->loadMissing(['itemMaster', 'alternates.alternateItem', 'children.itemMaster']);
return [
'id' => $line->id,
'item_code' => $line->itemMaster?->item_code,
'item_name' => $line->itemMaster?->name,
'position_code' => $line->position_code,
'quantity' => (float) $line->quantity,
'alternate_group' => $line->alternate_group,
'alternates' => $line->alternates,
'children' => $line->children->map(fn ($child) => $this->lineToTreeNode($child))->values()->all(),
];
}
/**
* Create SBOM from MBOM by filtering serviceable lines.
*/
public function deriveServiceBom(Bom $mbom, int $businessId, string $bomCode): Bom
{
$sbom = Bom::create([
'business_id' => $businessId,
'line_revision_id' => $mbom->line_revision_id,
'item_master_id' => $mbom->item_master_id,
'bom_type' => 'sbom',
'bom_code' => $bomCode,
'name' => $mbom->name.' (Service)',
'revision_code' => $mbom->revision_code,
'status' => 'draft',
]);
$this->copyServiceableLines($mbom, $sbom, null, null);
return $sbom->fresh(['lines']);
}
protected function copyServiceableLines(Bom $source, Bom $target, ?int $sourceParentId, ?int $targetParentId): void
{
$lines = BomLine::where('bom_id', $source->id)
->where('parent_line_id', $sourceParentId)
->where('is_serviceable', true)
->orderBy('sort_order')
->get();
foreach ($lines as $line) {
$newLine = $line->replicate(['bom_id', 'parent_line_id']);
$newLine->bom_id = $target->id;
$newLine->parent_line_id = $targetParentId;
$newLine->save();
foreach ($line->alternates as $alt) {
$alt->replicate(['bom_line_id'])->fill(['bom_line_id' => $newLine->id])->save();
}
$this->copyServiceableLines($source, $target, $line->id, $newLine->id);
}
}
public function snapshot(Bom $bom): array
{
return [
'bom_id' => $bom->id,
'bom_code' => $bom->bom_code,
'bom_type' => $bom->bom_type,
'revision_code' => $bom->revision_code,
'tree' => $this->buildTree($bom),
'flat' => $this->explode($bom)->values()->all(),
'snapshot_at' => now()->toIso8601String(),
];
}
}