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 */ 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 */ 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 */ 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 */ public function locationsForBusiness(int $businessId): Collection { return BusinessLocation::where('business_id', $businessId)->orderBy('name')->get(); } }