172 lines
6.4 KiB
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(),
|
|
];
|
|
}
|
|
}
|