This commit is contained in:
2026-11-02 08:08:05 +02:00
parent 13a6cb9922
commit 504b01b244
246 changed files with 24499 additions and 185 deletions
+32 -6
View File
@@ -14,6 +14,7 @@ use App\Models\Warehouse;
use Brick\Math\BigDecimal;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
@@ -26,14 +27,17 @@ class ReceiptWorkflow
return BigDecimal::of($quantity)->multipliedBy(1000)->toScale(0)->toInt();
}
private function recordedQuantity(int $lineId, string $column): int
public function recordedQuantity(int $lineId, string $column): int
{
if (! in_array($column, ['received_milli', 'accepted_milli'], true)) {
throw new \InvalidArgumentException('Quantidade de recepção desconhecida.');
}
$sum = GoodsReceiptItem::query()->where('purchase_order_item_id', $lineId)->sum($column);
if (! is_int($sum) && ! is_string($sum)) {
throw new \LogicException('A soma das quantidades deve ser um inteiro exacto.');
}
return BigDecimal::of($sum)->toInt();
return BigDecimal::of($sum)->minus(app(InventoryReversals::class)->voidedReceiptQuantity($lineId, $column))->toInt();
}
/** @return array{lines: list<array<string, mixed>>, state: string, has_divergences: bool} */
@@ -48,13 +52,16 @@ class ReceiptWorkflow
$ordered = self::milli($item->quantity);
$received = $this->recordedQuantity($item->id, 'received_milli');
$accepted = $this->recordedQuantity($item->id, 'accepted_milli');
$returned = app(InventoryDispositions::class)->returnedForOrderLine($item->id);
$effective = $accepted - $returned;
$lines[] = [
...$item->only(['id', 'description', 'unit', 'purchase_type']),
...(isset($order->snapshot['catalogue_sources'][$item->requisition_item_id]['stock_item_id']) ? ['stock_item_id' => $order->snapshot['catalogue_sources'][$item->requisition_item_id]['stock_item_id']] : []),
'ordered_milli' => $ordered, 'received_milli' => $received, 'accepted_milli' => $accepted,
'rejected_milli' => $received - $accepted, 'delivery_pending_milli' => $ordered - $received, 'acceptance_pending_milli' => $ordered - $accepted,
'rejected_milli' => $received - $accepted, 'delivery_pending_milli' => $ordered - $received, 'acceptance_pending_milli' => $ordered - $effective,
...($returned > 0 ? ['supplier_returned_milli' => $returned, 'effective_accepted_milli' => $effective] : []),
];
$complete = $complete && $accepted === $ordered;
$complete = $complete && $effective === $ordered;
$delivered = $delivered && $received === $ordered;
$started = $started || $received > 0;
$divergences = $divergences || GoodsReceiptItem::query()->where('purchase_order_item_id', $item->id)->where('quality_status', '!=', 'compliant')->exists();
@@ -76,9 +83,18 @@ class ReceiptWorkflow
throw ValidationException::withMessages(['delivery_note' => 'Indique a guia de remessa do fornecedor.']);
}
$inputs = [];
/** @var list<array{purchase_order_item_id: int, received_quantity: string, accepted_quantity: string, quality_status: string, stock_item_id?: int|null, notes?: string|null}> $requested */
/** @var list<array{purchase_order_item_id: int, received_quantity: string, accepted_quantity: string, quality_status: string, stock_item_id?: int|null, notes?: string|null, lots?:list<array<string,mixed>>}> $requested */
$requested = $data['items'];
$supplyMappings = Schema::hasTable('supply_records')
? app(SupplyPlanning::class)->latest('delivery')->where('purchase_order_id', $order->id)->get() : collect();
foreach ($requested as $line) {
$mapping = $supplyMappings->first(fn ($record) => $record->data['purchase_order_item_id'] == $line['purchase_order_item_id']);
if ($mapping !== null) {
if ((! empty($line['stock_item_id']) && (int) $line['stock_item_id'] !== $mapping->stock_item_id) || (int) ($data['warehouse_id'] ?? 0) !== $mapping->warehouse_id) {
throw ValidationException::withMessages(['items' => 'Reutilize o artigo e armazém já associados à confirmação desta linha.']);
}
$line['stock_item_id'] = $mapping->stock_item_id;
}
if (! empty($order->snapshot['catalogue_sources']) && empty($line['stock_item_id'])) {
$orderLine = $order->items()->whereKey($line['purchase_order_item_id'])->first();
if ($orderLine !== null && isset($order->snapshot['catalogue_sources'][$orderLine->requisition_item_id]['stock_item_id'])) {
@@ -90,6 +106,7 @@ class ReceiptWorkflow
}
}
$inputs[] = [
...(! empty($line['lots']) ? ['lots' => $line['lots']] : []),
'purchase_order_item_id' => $line['purchase_order_item_id'],
'received_milli' => self::milli($line['received_quantity']), 'accepted_milli' => self::milli($line['accepted_quantity']),
'quality_status' => $line['quality_status'], 'stock_item_id' => $line['stock_item_id'] ?? null,
@@ -117,6 +134,12 @@ class ReceiptWorkflow
if (GoodsReceipt::query()->where('purchase_order_id', $order->id)->where('delivery_note', $deliveryNote)->exists()) {
throw ValidationException::withMessages(['delivery_note' => 'Esta guia já foi confirmada nesta ordem. Consulte o histórico em vez de repetir a recepção.']);
}
if (Schema::hasTable('supply_records')) {
$import = app(SupplyPlanning::class)->latest('import')->where('purchase_order_id', $order->id)->first();
if ($import !== null && $import->state !== 'released') {
throw ValidationException::withMessages(['items' => 'A importação ainda não tem libertação aduaneira comprovada. A chegada ou o pagamento não autorizam entrada utilizável.']);
}
}
if ($order->status !== 'issued' || $data['received_on'] < substr((string) $order->getRawOriginal('issued_at'), 0, 10)) {
throw ValidationException::withMessages(['received_on' => 'A recepção deve referir uma ordem emitida e não pode anteceder a sua emissão.']);
}
@@ -193,12 +216,15 @@ class ReceiptWorkflow
foreach ($lines as $line) {
$item = $receipt->items()->create($line);
if ($line['accepted_milli'] > 0 && $line['snapshot']['physical']) {
StockMovement::query()->create([
$movement = StockMovement::query()->create([
'tenant_id' => $user->tenant_id, 'goods_receipt_item_id' => $item->id, 'stock_item_id' => $line['stock_item_id'],
'warehouse_id' => $warehouse?->id, 'moved_by' => $user->id, 'quantity_milli' => $line['accepted_milli'], 'moved_at' => now(),
'equipment_id' => $line['snapshot']['equipment_id'], 'spare_part_id' => $line['snapshot']['spare_part_id'],
'snapshot' => [...$line['snapshot'], 'receipt_id' => $receipt->id, 'order_number' => $order->number, 'delivery_note' => $deliveryNote],
]);
$sourceInput = collect($inputs)->firstWhere('purchase_order_item_id', $line['purchase_order_item_id']);
app(InventoryLots::class)->receive($user, $movement, $sourceInput['lots'] ?? [], $data['received_on']);
app(AutomaticPurchases::class)->signal($user,$line['stock_item_id'],$warehouse->id);
}
}
$progress = $this->progress($order);