'Ciornă', 'ordered' => 'Comandată', 'partial' => 'Parțial recepționată', 'received' => 'Recepționată', 'cancelled' => 'Anulată', ]; protected $fillable = [ 'company_id', 'number', 'supplier_id', 'warehouse_id', 'order_date', 'expected_at', 'received_at', 'paid_at', 'status', 'total', 'notes', ]; protected $casts = [ 'order_date' => 'date', 'expected_at' => 'date', 'received_at' => 'date', 'paid_at' => 'date', 'total' => 'decimal:2', ]; public function supplier(): BelongsTo { return $this->belongsTo(Supplier::class); } public function warehouse(): BelongsTo { return $this->belongsTo(Warehouse::class); } public function items(): HasMany { return $this->hasMany(PurchaseItem::class); } public function recalcTotal(): void { $this->total = (float) $this->items()->sum('total'); $this->save(); } public static function generateNumber(int $companyId): string { $year = date('y'); $count = static::withoutGlobalScopes() ->where('company_id', $companyId) ->whereYear('created_at', date('Y')) ->count(); return sprintf('P-%s-%04d', $year, $count + 1); } /** * Receive a specific item — qty of buy_price unit cost into target warehouse. * Routes through WarehouseService so a batch is created + receipt event written. * Also records the supplier price for analytics. */ public function receiveItem(PurchaseItem $item, float $qty, ?Warehouse $warehouse = null): void { if ($qty <= 0) { throw new \InvalidArgumentException('Cantitatea de recepție trebuie să fie pozitivă.'); } $outstanding = (float) $item->qty - (float) $item->qty_received; if ($qty > $outstanding + 0.001) { throw new \InvalidArgumentException(sprintf( 'Cantitate prea mare: cerut %.2f, restanță %.2f', $qty, $outstanding )); } \Illuminate\Support\Facades\DB::transaction(function () use ($item, $qty, $warehouse) { $warehouse ??= $this->warehouse; if (! $warehouse) { $warehouse = app(\App\Services\Warehouse\WarehouseService::class) ->defaultWarehouse($this->company_id); } if ($item->part_id) { $part = Part::find($item->part_id); if ($part) { app(\App\Services\Warehouse\WarehouseService::class)->receive( part: $part, qty: $qty, buyPrice: (float) $item->buy_price, warehouse: $warehouse, supplier: $this->supplier, batchRef: $this->number, ref: $this, notes: "PO #{$this->number}", ); if ($this->supplier_id) { SupplierPartPrice::create([ 'supplier_id' => $this->supplier_id, 'part_id' => $part->id, 'purchase_id' => $this->id, 'price' => (float) $item->buy_price, 'currency' => 'MDL', 'observed_at' => now(), ]); } } } $item->qty_received = (float) $item->qty_received + $qty; if ((float) $item->qty_received >= (float) $item->qty) { $item->received = true; } $item->save(); $this->recomputeStatus(); }); } /** Convenience: receive every outstanding item in full. */ public function receiveAllRemaining(?Warehouse $warehouse = null): void { foreach ($this->items()->get() as $item) { $outstanding = (float) $item->qty - (float) $item->qty_received; if ($outstanding > 0) { $this->receiveItem($item, $outstanding, $warehouse); } } } /** Recalculate status based on item qty_received vs qty. */ public function recomputeStatus(): void { if ($this->status === 'cancelled' || $this->status === 'draft') { return; } $items = $this->items()->get(); if ($items->isEmpty()) return; $totals = $items->reduce(function ($carry, $i) { $carry['ordered'] += (float) $i->qty; $carry['received'] += (float) $i->qty_received; return $carry; }, ['ordered' => 0.0, 'received' => 0.0]); if ($totals['received'] <= 0) { $this->status = 'ordered'; } elseif ($totals['received'] + 0.001 < $totals['ordered']) { $this->status = 'partial'; } else { $this->status = 'received'; if (! $this->received_at) $this->received_at = now(); } $this->save(); } }