194 lines
6.6 KiB
PHP
194 lines
6.6 KiB
PHP
<?php
|
|
|
|
namespace Modules\Maintenance\Services;
|
|
|
|
use App\BusinessLocation;
|
|
use App\HoldingEntity;
|
|
use App\PurchaseLine;
|
|
use App\Variation;
|
|
use App\VariationLocationDetails;
|
|
use Illuminate\Support\Collection;
|
|
use Modules\Maintenance\Models\MaterialLine;
|
|
use Modules\Maintenance\Models\SparePart;
|
|
use Modules\Maintenance\Utils\MaintenanceUtil;
|
|
|
|
class MaterialStockService
|
|
{
|
|
public function __construct(
|
|
protected MaintenanceUtil $maintenanceUtil,
|
|
protected SparePartInventoryService $inventoryService
|
|
) {}
|
|
|
|
public function resolveDestinationLocationId(int $businessId, ?int $override = null): ?int
|
|
{
|
|
if ($override) {
|
|
return $override;
|
|
}
|
|
|
|
$settings = $this->maintenanceUtil->getMaintenanceSettings($businessId);
|
|
|
|
return ! empty($settings['default_stock_location_id'])
|
|
? (int) $settings['default_stock_location_id']
|
|
: null;
|
|
}
|
|
|
|
/**
|
|
* @return array{product_id: ?int, variation_id: ?int}
|
|
*/
|
|
public function resolveProductIds(MaterialLine $line): array
|
|
{
|
|
if ($line->product_id && $line->variation_id) {
|
|
return [
|
|
'product_id' => (int) $line->product_id,
|
|
'variation_id' => (int) $line->variation_id,
|
|
];
|
|
}
|
|
|
|
if ($line->spare_part_id) {
|
|
$part = $line->sparePart ?? SparePart::find($line->spare_part_id);
|
|
if ($part && $part->product_id) {
|
|
$variationId = $this->inventoryService->resolveVariationId($part);
|
|
|
|
return [
|
|
'product_id' => (int) $part->product_id,
|
|
'variation_id' => $variationId,
|
|
];
|
|
}
|
|
}
|
|
|
|
return ['product_id' => null, 'variation_id' => null];
|
|
}
|
|
|
|
/**
|
|
* Stock per location for a material line across all business locations (holding warehouses).
|
|
*
|
|
* @return Collection<int, array{location_id: int, location_name: string, entity_name: ?string, qty: float}>
|
|
*/
|
|
public function stockByLocation(int $businessId, MaterialLine $line): Collection
|
|
{
|
|
$ids = $this->resolveProductIds($line);
|
|
if (! $ids['product_id'] || ! $ids['variation_id']) {
|
|
return collect();
|
|
}
|
|
|
|
$entityMap = $this->locationEntityMap($businessId);
|
|
|
|
return VariationLocationDetails::query()
|
|
->join('business_locations as bl', 'variation_location_details.location_id', '=', 'bl.id')
|
|
->where('variation_location_details.product_id', $ids['product_id'])
|
|
->where('variation_location_details.variation_id', $ids['variation_id'])
|
|
->where('bl.business_id', $businessId)
|
|
->where('variation_location_details.qty_available', '>', 0)
|
|
->select(
|
|
'variation_location_details.location_id',
|
|
'bl.name as location_name',
|
|
'variation_location_details.qty_available as qty'
|
|
)
|
|
->orderByDesc('variation_location_details.qty_available')
|
|
->get()
|
|
->map(function ($row) use ($entityMap) {
|
|
$locId = (int) $row->location_id;
|
|
|
|
return [
|
|
'location_id' => $locId,
|
|
'location_name' => $row->location_name,
|
|
'entity_name' => $entityMap[$locId] ?? null,
|
|
'qty' => (float) $row->qty,
|
|
];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function analyzeLine(int $businessId, MaterialLine $line, ?int $destinationLocationId = null): array
|
|
{
|
|
$destinationLocationId = $this->resolveDestinationLocationId($businessId, $destinationLocationId);
|
|
$stockRows = $this->stockByLocation($businessId, $line);
|
|
$required = (float) $line->quantity_required;
|
|
$fulfilled = (float) $line->quantity_fulfilled;
|
|
$remaining = max(0, $required - $fulfilled);
|
|
|
|
$localQty = 0.0;
|
|
$otherLocations = [];
|
|
|
|
foreach ($stockRows as $row) {
|
|
if ($destinationLocationId && $row['location_id'] === $destinationLocationId) {
|
|
$localQty = $row['qty'];
|
|
} else {
|
|
$otherLocations[] = $row;
|
|
}
|
|
}
|
|
|
|
$totalAvailable = $stockRows->sum('qty');
|
|
$shortfall = max(0, $remaining - $totalAvailable);
|
|
|
|
return [
|
|
'line_id' => $line->id,
|
|
'display_name' => $line->displayName(),
|
|
'part_number' => $line->part_number ?? $line->sparePart?->part_number,
|
|
'quantity_required' => $required,
|
|
'quantity_fulfilled' => $fulfilled,
|
|
'remaining' => $remaining,
|
|
'local_qty' => $localQty,
|
|
'total_available' => $totalAvailable,
|
|
'shortfall' => $shortfall,
|
|
'destination_location_id' => $destinationLocationId,
|
|
'stock_by_location' => $stockRows->values()->all(),
|
|
'other_locations' => $otherLocations,
|
|
'has_product_link' => (bool) $this->resolveProductIds($line)['product_id'],
|
|
'status' => $line->status,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
protected function locationEntityMap(int $businessId): array
|
|
{
|
|
$map = [];
|
|
$entities = HoldingEntity::where('business_id', $businessId)->where('is_active', true)->get();
|
|
|
|
foreach ($entities as $entity) {
|
|
$locId = data_get($entity->meta, 'business_location_id');
|
|
if ($locId) {
|
|
$map[(int) $locId] = $entity->name;
|
|
}
|
|
}
|
|
|
|
return $map;
|
|
}
|
|
|
|
public function getUnitPrice(int $variationId, ?int $locationId = null, ?int $supplierId = null): float
|
|
{
|
|
$variation = Variation::find($variationId);
|
|
if ($variation && $variation->default_purchase_price) {
|
|
return (float) $variation->default_purchase_price;
|
|
}
|
|
|
|
$query = PurchaseLine::join('transactions as t', 'purchase_lines.transaction_id', '=', 't.id')
|
|
->where('purchase_lines.variation_id', $variationId)
|
|
->where('t.type', 'purchase')
|
|
->where('t.status', 'received');
|
|
|
|
if ($locationId) {
|
|
$query->where('t.location_id', $locationId);
|
|
}
|
|
if ($supplierId) {
|
|
$query->where('t.contact_id', $supplierId);
|
|
}
|
|
|
|
$line = $query->orderByDesc('t.transaction_date')->select('purchase_lines.purchase_price')->first();
|
|
|
|
return $line ? (float) $line->purchase_price : 0.0;
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, BusinessLocation>
|
|
*/
|
|
public function locationsForBusiness(int $businessId): Collection
|
|
{
|
|
return BusinessLocation::where('business_id', $businessId)->orderBy('name')->get();
|
|
}
|
|
}
|