Commit 280d6a36 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix stock count not updating inventory + add purchase order wizard

1. Stock Count fix: dispatch StockCountCompleted event after completing
   a count so the ProcessCountAdjustments listener creates inventory
   movements. Made listener synchronous for immediate feedback.

2. Stock Adjustment: route now points to the existing wizard component
   instead of the plain form.

3. Purchase Orders: new CreatePurchaseOrderWizard with 4-step flow
   (supplier → warehouse/dates → items → review & confirm).
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 9bf95b08
...@@ -6,10 +6,9 @@ ...@@ -6,10 +6,9 @@
use App\Domain\Inventory\Events\StockCountCompleted; use App\Domain\Inventory\Events\StockCountCompleted;
use App\Domain\Inventory\Models\Warehouse; use App\Domain\Inventory\Models\Warehouse;
use App\Domain\Inventory\Services\InventoryService; use App\Domain\Inventory\Services\InventoryService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
class ProcessCountAdjustments implements ShouldQueue class ProcessCountAdjustments
{ {
public function __construct( public function __construct(
private InventoryService $inventoryService, private InventoryService $inventoryService,
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
use App\Domain\Inventory\Enums\MovementType; use App\Domain\Inventory\Enums\MovementType;
use App\Domain\Inventory\Enums\StockCountItemStatus; use App\Domain\Inventory\Enums\StockCountItemStatus;
use App\Domain\Inventory\Enums\StockCountStatus; use App\Domain\Inventory\Enums\StockCountStatus;
use App\Domain\Inventory\Events\StockCountCompleted;
use App\Domain\Inventory\Models\InventoryLevel; use App\Domain\Inventory\Models\InventoryLevel;
use App\Domain\Inventory\Models\Product; use App\Domain\Inventory\Models\Product;
use App\Domain\Inventory\Models\StockCount; use App\Domain\Inventory\Models\StockCount;
...@@ -145,6 +146,8 @@ public function completeCount(StockCount $stockCount, User $actor): StockCount ...@@ -145,6 +146,8 @@ public function completeCount(StockCount $stockCount, User $actor): StockCount
'completed_by' => $actor->id, 'completed_by' => $actor->id,
]); ]);
StockCountCompleted::dispatch($stockCount, $actor);
return $stockCount->fresh('items'); return $stockCount->fresh('items');
}); });
} }
......
<?php
namespace App\Livewire\Inventory;
use App\Domain\Inventory\Models\Product;
use App\Domain\Inventory\Models\PurchaseOrder;
use App\Domain\Inventory\Models\Warehouse;
use App\Domain\Inventory\Services\PurchaseOrderService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('أمر شراء جديد')]
class CreatePurchaseOrderWizard extends Component
{
use UsesBranchScope;
public int $currentStep = 1;
public int $totalSteps = 4;
public bool $completed = false;
// Step 1: Supplier
public string $supplier_name = '';
public string $supplier_contact = '';
// Step 2: Warehouse & Dates
public ?int $warehouseId = null;
public ?array $selectedWarehouse = null;
public string $order_date = '';
public string $expected_delivery_date = '';
// Step 3: Items
public array $items = [];
// Step 4: Additional costs
public string $tax_amount = '0';
public string $shipping_cost = '0';
public string $notes = '';
// Result
public ?string $orderNumber = null;
public ?string $orderUuid = null;
public function mount(): void
{
$this->authorize('inventory.manage');
$this->order_date = now()->format('Y-m-d');
$this->items[] = ['product_id' => '', 'quantity_ordered' => '1', 'unit_cost' => '', 'notes' => ''];
}
public function getStepLabels(): array
{
return [
1 => 'المورد',
2 => 'المستودع والتاريخ',
3 => 'الأصناف',
4 => 'مراجعة وتأكيد',
];
}
public function selectWarehouse(int $id): void
{
$warehouse = Warehouse::findOrFail($id);
$this->warehouseId = $id;
$this->selectedWarehouse = [
'id' => $warehouse->id,
'name_ar' => $warehouse->name_ar,
];
}
public function addItem(): void
{
$this->items[] = ['product_id' => '', 'quantity_ordered' => '1', 'unit_cost' => '', 'notes' => ''];
}
public function removeItem(int $index): void
{
unset($this->items[$index]);
$this->items = array_values($this->items);
if (empty($this->items)) {
$this->addItem();
}
}
public function getItemsTotalProperty(): int
{
return (int) collect($this->items)->sum(function ($item) {
return ((int) ($item['quantity_ordered'] ?? 0)) * (int) round((float) ($item['unit_cost'] ?? 0) * 100);
});
}
public function getGrandTotalProperty(): int
{
return $this->getItemsTotalProperty()
+ (int) round((float) $this->tax_amount * 100)
+ (int) round((float) $this->shipping_cost * 100);
}
public function nextStep(): void
{
$this->validate($this->rulesForStep($this->currentStep));
if ($this->currentStep < $this->totalSteps) {
$this->currentStep++;
}
}
public function previousStep(): void
{
if ($this->currentStep > 1) {
$this->currentStep--;
}
}
public function goToStep(int $step): void
{
if ($step < $this->currentStep) {
$this->currentStep = $step;
}
}
public function confirm(): void
{
try {
$branchId = $this->getActiveBranchId() ?? auth()->user()->branch_id;
$orderNumber = 'PO-' . now()->format('Ymd') . '-' . str_pad(
PurchaseOrder::whereDate('created_at', today())->count() + 1,
3, '0', STR_PAD_LEFT
);
$data = [
'academy_id' => app('current_academy')->id,
'branch_id' => $branchId,
'warehouse_id' => $this->warehouseId,
'order_number' => $orderNumber,
'supplier_name' => $this->supplier_name,
'supplier_contact' => $this->supplier_contact ?: null,
'order_date' => $this->order_date,
'expected_delivery_date' => $this->expected_delivery_date ?: null,
'tax_amount' => (int) round((float) $this->tax_amount * 100),
'shipping_cost' => (int) round((float) $this->shipping_cost * 100),
'notes' => $this->notes ?: null,
'metadata' => [],
];
$items = collect($this->items)->map(fn ($item) => [
'product_id' => (int) $item['product_id'],
'quantity_ordered' => (int) $item['quantity_ordered'],
'unit_cost' => (int) round((float) $item['unit_cost'] * 100),
'notes' => $item['notes'] ?? null,
])->toArray();
$service = app(PurchaseOrderService::class);
$po = $service->create($data, $items, auth()->user());
$this->completed = true;
$this->orderNumber = $po->order_number;
$this->orderUuid = $po->uuid ?? $po->id;
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
private function rulesForStep(int $step): array
{
return match ($step) {
1 => [
'supplier_name' => 'required|string|max:255',
'supplier_contact' => 'nullable|string|max:255',
],
2 => [
'warehouseId' => 'required|integer|exists:warehouses,id',
'order_date' => 'required|date',
'expected_delivery_date' => 'nullable|date|after_or_equal:order_date',
],
3 => [
'items' => 'required|array|min:1',
'items.*.product_id' => 'required|exists:products,id',
'items.*.quantity_ordered' => 'required|integer|min:1',
'items.*.unit_cost' => 'required|numeric|min:0.01',
],
default => [],
};
}
public function messages(): array
{
return [
'supplier_name.required' => 'اسم المورد مطلوب',
'warehouseId.required' => 'يجب اختيار المستودع',
'warehouseId.exists' => 'المستودع غير موجود',
'order_date.required' => 'تاريخ الطلب مطلوب',
'expected_delivery_date.after_or_equal' => 'تاريخ التسليم يجب أن يكون بعد تاريخ الطلب',
'items.required' => 'يجب إضافة صنف واحد على الأقل',
'items.min' => 'يجب إضافة صنف واحد على الأقل',
'items.*.product_id.required' => 'اختر المنتج',
'items.*.product_id.exists' => 'المنتج غير موجود',
'items.*.quantity_ordered.required' => 'الكمية مطلوبة',
'items.*.quantity_ordered.min' => 'الكمية يجب أن تكون 1 على الأقل',
'items.*.unit_cost.required' => 'سعر الوحدة مطلوب',
'items.*.unit_cost.min' => 'سعر الوحدة يجب أن يكون أكبر من صفر',
];
}
public function render()
{
$branchId = $this->getActiveBranchId();
return view('livewire.inventory.create-purchase-order-wizard', [
'warehouses' => Warehouse::query()
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->where('is_active', true)
->orderBy('name_ar')
->get(['id', 'name_ar', 'code']),
'products' => Product::query()
->where('is_active', true)
->select('id', 'name_ar', 'sku')
->orderBy('name_ar')
->get(),
]);
}
}
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
use App\Livewire\Inventory\MovementList; use App\Livewire\Inventory\MovementList;
use App\Livewire\Inventory\ProductForm as InventoryProductForm; use App\Livewire\Inventory\ProductForm as InventoryProductForm;
use App\Livewire\Inventory\ProductList as InventoryProductList; use App\Livewire\Inventory\ProductList as InventoryProductList;
use App\Livewire\Inventory\StockAdjustment; use App\Livewire\Inventory\StockAdjustmentWizard;
use App\Livewire\Inventory\WarehouseForm as InventoryWarehouseForm; use App\Livewire\Inventory\WarehouseForm as InventoryWarehouseForm;
use App\Livewire\Inventory\WarehouseList as InventoryWarehouseList; use App\Livewire\Inventory\WarehouseList as InventoryWarehouseList;
use App\Livewire\Wallets\WalletList; use App\Livewire\Wallets\WalletList;
...@@ -355,9 +355,7 @@ ...@@ -355,9 +355,7 @@
->middleware('permission:inventory.update'); ->middleware('permission:inventory.update');
Route::get('/inventory/movements', MovementList::class)->name('inventory.movements') Route::get('/inventory/movements', MovementList::class)->name('inventory.movements')
->middleware('permission:inventory.list'); ->middleware('permission:inventory.list');
Route::get('/inventory/adjustments', StockAdjustment::class)->name('inventory.adjustments') Route::get('/inventory/adjustments', StockAdjustmentWizard::class)->name('inventory.adjustments')
->middleware('permission:inventory.adjust');
Route::get('/inventory/adjustments/wizard', \App\Livewire\Inventory\StockAdjustmentWizard::class)->name('inventory.adjustments.wizard')
->middleware('permission:inventory.adjust'); ->middleware('permission:inventory.adjust');
Route::get('/inventory/kits', \App\Livewire\Inventory\KitList::class)->name('inventory.kits') Route::get('/inventory/kits', \App\Livewire\Inventory\KitList::class)->name('inventory.kits')
->middleware('permission:inventory.manage'); ->middleware('permission:inventory.manage');
...@@ -373,7 +371,7 @@ ...@@ -373,7 +371,7 @@
->middleware('permission:inventory.manage'); ->middleware('permission:inventory.manage');
Route::get('/inventory/purchase-orders', \App\Livewire\Inventory\PurchaseOrderList::class)->name('inventory.purchase-orders') Route::get('/inventory/purchase-orders', \App\Livewire\Inventory\PurchaseOrderList::class)->name('inventory.purchase-orders')
->middleware('permission:inventory.manage'); ->middleware('permission:inventory.manage');
Route::get('/inventory/purchase-orders/create', \App\Livewire\Inventory\PurchaseOrderForm::class)->name('inventory.purchase-orders.create') Route::get('/inventory/purchase-orders/create', \App\Livewire\Inventory\CreatePurchaseOrderWizard::class)->name('inventory.purchase-orders.create')
->middleware('permission:inventory.manage'); ->middleware('permission:inventory.manage');
Route::get('/inventory/purchase-orders/{purchaseOrder}', \App\Livewire\Inventory\PurchaseOrderShow::class)->name('inventory.purchase-orders.show') Route::get('/inventory/purchase-orders/{purchaseOrder}', \App\Livewire\Inventory\PurchaseOrderShow::class)->name('inventory.purchase-orders.show')
->middleware('permission:inventory.list'); ->middleware('permission:inventory.list');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment