refactor(purchase): merubah total alur belanja, yang awalnya langsung masuk ke stok yang ada di outlet, sekarang masuk ke gudang terleih dahulu
This commit is contained in:
parent
8144b8bace
commit
a2efc0f349
@ -25,7 +25,7 @@ class PurchasesTable extends DataTableComponent
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
Column::make('Outlet', 'outlet.name')->searchable(),
|
||||
Column::make('Gudang', 'warehouse.name')->searchable(),
|
||||
|
||||
Column::make('Nomor Invoice', 'invoice_number')->searchable(),
|
||||
|
||||
@ -73,14 +73,13 @@ public function columns(): array
|
||||
return $actions;
|
||||
})
|
||||
->html()
|
||||
->hideIf(auth()->user()->cannot('update purchase')),
|
||||
->hideIf(auth()->user()->cannot('delete purchase')),
|
||||
];
|
||||
}
|
||||
|
||||
public function builder(): Builder
|
||||
{
|
||||
return Purchase::select('purchases.id', 'outlet_id', 'invoice_number', 'purchase_date', 'total')
|
||||
->with(['outlet', 'items', 'items.purchasable'])
|
||||
->whereIn('outlet_id', auth()->user()->outlets->pluck('id')->toArray());
|
||||
return Purchase::select('purchases.id', 'warehouse_id', 'invoice_number', 'purchase_date', 'total')
|
||||
->with(['warehouse', 'items', 'items.purchasable']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,14 +6,14 @@
|
||||
use App\Rules\UnsignedInteger;
|
||||
use App\Traits\Media\WithMediaHandler;
|
||||
use App\Traits\Purchase\WithPurchaseItems;
|
||||
use App\Traits\Purchase\WithUpdateStock;
|
||||
use App\Traits\Purchase\WithWarehouseStock;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Form;
|
||||
|
||||
class PurchaseForm extends Form
|
||||
{
|
||||
use WithMediaHandler, WithPurchaseItems, WithUpdateStock;
|
||||
use WithMediaHandler, WithPurchaseItems, WithWarehouseStock;
|
||||
|
||||
public ?Purchase $purchase = null;
|
||||
|
||||
@ -25,7 +25,7 @@ class PurchaseForm extends Form
|
||||
|
||||
public string $total = '';
|
||||
|
||||
public string $outlet_id = '';
|
||||
public string $warehouse_id = '';
|
||||
|
||||
public array $image = [];
|
||||
|
||||
@ -51,7 +51,7 @@ public function rules(): array
|
||||
'invoice_number' => ['nullable', 'string', 'max:50'],
|
||||
'note' => ['nullable', 'string', 'max:100'],
|
||||
'purchase_date' => ['required', 'date', 'before_or_equal:today'],
|
||||
'outlet_id' => ['required', Rule::exists('outlets', 'id')],
|
||||
'warehouse_id' => ['required', Rule::exists('warehouses', 'id')],
|
||||
'total' => ['required', new UnsignedInteger],
|
||||
'image' => ['nullable', 'array', 'max:1'],
|
||||
];
|
||||
@ -63,7 +63,7 @@ public function validationAttributes(): array
|
||||
'invoice_number' => 'nomor invoice',
|
||||
'note' => 'catatan',
|
||||
'purchase_date' => 'tanggal belanja',
|
||||
'outlet_id' => 'outlet',
|
||||
'warehouse_id' => 'gudang',
|
||||
'image' => 'gambar',
|
||||
];
|
||||
}
|
||||
@ -75,7 +75,7 @@ public function setPurchase(Purchase $purchase): void
|
||||
$this->invoice_number = $purchase->invoice_number;
|
||||
$this->note = $purchase->note;
|
||||
$this->purchase_date = $purchase->purchase_date;
|
||||
$this->outlet_id = $purchase->outlet_id;
|
||||
$this->warehouse_id = $purchase->warehouse_id;
|
||||
$this->total = $purchase->total;
|
||||
|
||||
$this->image = $this->mapMediaCollection($purchase->getMedia('image'));
|
||||
@ -101,7 +101,7 @@ public function store(): void
|
||||
|
||||
$purchase = Purchase::create($data);
|
||||
|
||||
$purchase->load('outlet');
|
||||
$purchase->load('warehouse');
|
||||
|
||||
$this->uploadMedia($this->image, $purchase, 'image');
|
||||
|
||||
@ -113,36 +113,17 @@ public function store(): void
|
||||
|
||||
$item->update(['purchase_id' => $purchase->id]);
|
||||
|
||||
$this->increaseOutletStock($purchase->outlet, $item);
|
||||
$this->increaseWarehouseStock($purchase->warehouse, $item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function update(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
// Ensure total calculation integrity from existing items
|
||||
$calculatedTotal = $this->purchase->items()->sum(DB::raw('unit_price * quantity'));
|
||||
$this->total = formatCurrencyNumber($calculatedTotal);
|
||||
|
||||
DB::transaction(function () use ($calculatedTotal) {
|
||||
$data = $this->prepareSavedData();
|
||||
$data['total'] = $calculatedTotal; // Enforce calculated total
|
||||
|
||||
$this->purchase->update($data);
|
||||
|
||||
$this->syncMedia($this->image, $this->purchase, 'image');
|
||||
$this->uploadMedia($this->image, $this->purchase, 'image');
|
||||
});
|
||||
}
|
||||
|
||||
private function prepareSavedData(): array
|
||||
{
|
||||
return [
|
||||
'invoice_number' => $this->invoice_number,
|
||||
'note' => $this->note,
|
||||
'outlet_id' => $this->outlet_id,
|
||||
'warehouse_id' => $this->warehouse_id,
|
||||
'purchase_date' => $this->purchase_date,
|
||||
'total' => parseRupiahToInt($this->total),
|
||||
];
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\Bottle;
|
||||
use App\Models\Perfume;
|
||||
use App\Models\Product;
|
||||
use App\Models\Warehouse as WarehouseModel;
|
||||
use App\Traits\Authorization\WithAuthorization;
|
||||
use App\Traits\Components\WithConfirmation;
|
||||
use App\Traits\Components\WithToast;
|
||||
@ -26,7 +27,7 @@ class Create extends Component
|
||||
|
||||
public PurchaseForm $form;
|
||||
|
||||
public array $outlets = [];
|
||||
public array $warehouses = [];
|
||||
|
||||
public array $perfumes = [];
|
||||
|
||||
@ -38,13 +39,13 @@ class Create extends Component
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->outlets = auth()->user()->outlets->pluck('name', 'id')->toArray();
|
||||
$this->warehouses = WarehouseModel::orderBy('name')->pluck('name', 'id')->toArray();
|
||||
|
||||
$this->perfumes = Perfume::whereHas('outlets', fn ($query) => $query->where('outlets.id', $this->form->outlet_id))->orderBy('name')->pluck('name', 'id')->toArray();
|
||||
$this->perfumes = Perfume::orderBy('name')->pluck('name', 'id')->toArray();
|
||||
|
||||
$this->bottles = Bottle::whereHas('outlets', fn ($query) => $query->where('outlets.id', $this->form->outlet_id))->orderBy('size')->pluck('name', 'id')->toArray();
|
||||
$this->bottles = Bottle::orderBy('name')->pluck('name', 'id')->toArray();
|
||||
|
||||
$this->products = Product::whereHas('outlets', fn ($query) => $query->where('outlets.id', $this->form->outlet_id))->orderBy('name')->pluck('name', 'id')->toArray();
|
||||
$this->products = Product::orderBy('name')->pluck('name', 'id')->toArray();
|
||||
|
||||
$this->purchaseItems = $this->loadPurchaseItems();
|
||||
|
||||
@ -68,13 +69,6 @@ public function save(): void
|
||||
], navigate: true);
|
||||
}
|
||||
|
||||
public function updatedFormOutletId($value): void
|
||||
{
|
||||
$this->perfumes = Perfume::whereHas('outlets', fn ($query) => $query->where('outlets.id', $value))->orderBy('name')->pluck('name', 'id')->toArray();
|
||||
$this->bottles = Bottle::whereHas('outlets', fn ($query) => $query->where('outlets.id', $value))->orderBy('size')->pluck('name', 'id')->toArray();
|
||||
$this->products = Product::whereHas('outlets', fn ($query) => $query->where('outlets.id', $value))->orderBy('name')->pluck('name', 'id')->toArray();
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.studio.manage.purchase.form', [
|
||||
|
||||
@ -27,7 +27,7 @@ public function delete(Purchase $purchase): void
|
||||
|
||||
DB::transaction(function () use ($purchase) {
|
||||
foreach ($purchase->items as $item) {
|
||||
$this->decreaseOutletStock($purchase->outlet, $item);
|
||||
$this->decreaseWarehouseStock($purchase->warehouse, $item);
|
||||
}
|
||||
|
||||
$purchase->delete();
|
||||
|
||||
@ -38,9 +38,9 @@ public function yesterday(Builder $query): void
|
||||
$query->whereDate('created_at', today()->subDay());
|
||||
}
|
||||
|
||||
public function outlet(): BelongsTo
|
||||
public function warehouse(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Outlet::class);
|
||||
return $this->belongsTo(Warehouse::class);
|
||||
}
|
||||
|
||||
public function items(): HasMany
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
|
||||
trait WithUpdateStock
|
||||
{
|
||||
public function increaseOutletStock($outlet, $item): void
|
||||
public function increaseWarehouseStock($warehouse, $item): void
|
||||
{
|
||||
$quantity = $item->quantity;
|
||||
|
||||
@ -28,7 +28,7 @@ public function increaseOutletStock($outlet, $item): void
|
||||
}
|
||||
|
||||
// Lock the row to prevent race conditions
|
||||
$existing = $outlet->{$relation}()
|
||||
$existing = $warehouse->{$relation}()
|
||||
->where("{$relation}.id", $item->purchasable_id)
|
||||
->lockForUpdate()
|
||||
->withPivot('stock')
|
||||
@ -36,22 +36,22 @@ public function increaseOutletStock($outlet, $item): void
|
||||
|
||||
if ($existing) {
|
||||
$previousStock = $existing->pivot->stock ?? 0;
|
||||
$outlet->{$relation}()->updateExistingPivot($item->purchasable_id, [
|
||||
$warehouse->{$relation}()->updateExistingPivot($item->purchasable_id, [
|
||||
'stock' => $previousStock + $quantity,
|
||||
]);
|
||||
} else {
|
||||
$previousStock = 0;
|
||||
$outlet->{$relation}()->attach($item->purchasable_id, [
|
||||
$warehouse->{$relation}()->attach($item->purchasable_id, [
|
||||
'stock' => $quantity,
|
||||
]);
|
||||
}
|
||||
|
||||
StockActivityLogService::log(
|
||||
StockActivityLogService::logWarehouse(
|
||||
logName: 'Belanja',
|
||||
event: 'Menambah',
|
||||
description: 'Stok '.$item->purchasable->name.' di '.$outlet->name.' bertambah '.formatCurrencyNumber($quantity),
|
||||
description: 'Stok '.$item->purchasable->name.' di '.$warehouse->name.' bertambah '.formatCurrencyNumber($quantity),
|
||||
performedOn: $item->purchasable,
|
||||
outlet: $outlet,
|
||||
warehouse: $warehouse,
|
||||
quantityChange: $quantity,
|
||||
previousStock: $previousStock,
|
||||
newStock: $previousStock + $quantity,
|
||||
@ -63,7 +63,7 @@ public function increaseOutletStock($outlet, $item): void
|
||||
);
|
||||
}
|
||||
|
||||
public function decreaseOutletStock($outlet, $item): void
|
||||
public function decreaseWarehouseStock($warehouse, $item): void
|
||||
{
|
||||
$quantity = $item->quantity;
|
||||
|
||||
@ -81,31 +81,32 @@ public function decreaseOutletStock($outlet, $item): void
|
||||
return;
|
||||
}
|
||||
|
||||
$existing = $outlet->{$relation}()
|
||||
$existing = $warehouse->{$relation}()
|
||||
->where("{$relation}.id", $item->purchasable_id)
|
||||
->lockForUpdate()
|
||||
->withPivot('stock')
|
||||
->first();
|
||||
|
||||
$currentStock = $existing?->pivot->stock ?? 0;
|
||||
$newStock = max(0, $currentStock - $quantity);
|
||||
|
||||
if ($existing) {
|
||||
$currentStock = $existing->pivot->stock ?? 0;
|
||||
$newStock = max(0, $currentStock - $quantity);
|
||||
$outlet->{$relation}()->updateExistingPivot($item->purchasable_id, [
|
||||
'stock' => $newStock,
|
||||
]);
|
||||
} else {
|
||||
// Handle case where stock doesn't exist but we are decreasing?
|
||||
// Should not happen in normal flow, but good to handle.
|
||||
$currentStock = 0;
|
||||
$newStock = 0;
|
||||
if ($newStock === 0) {
|
||||
$warehouse->{$relation}()->detach($item->purchasable_id);
|
||||
} else {
|
||||
$warehouse->{$relation}()->updateExistingPivot(
|
||||
$item->purchasable_id,
|
||||
['stock' => $newStock]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StockActivityLogService::log(
|
||||
StockActivityLogService::logWarehouse(
|
||||
logName: 'Belanja',
|
||||
event: 'Mengurangi',
|
||||
description: 'Stok '.$item->purchasable->name.' di '.$outlet->name.' berkurang '.$quantity,
|
||||
description: 'Stok '.$item->purchasable->name.' di '.$warehouse->name.' berkurang '.$quantity,
|
||||
performedOn: $item->purchasable,
|
||||
outlet: $outlet,
|
||||
warehouse: $warehouse,
|
||||
quantityChange: $quantity,
|
||||
previousStock: $currentStock,
|
||||
newStock: $newStock,
|
||||
|
||||
129
app/Traits/Purchase/WithWarehouseStock.php
Normal file
129
app/Traits/Purchase/WithWarehouseStock.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\Purchase;
|
||||
|
||||
use App\Models\Bottle;
|
||||
use App\Models\Perfume;
|
||||
use App\Models\Product;
|
||||
use App\Services\StockActivityLogService;
|
||||
|
||||
trait WithWarehouseStock
|
||||
{
|
||||
public function increaseWarehouseStock($warehouse, $item): void
|
||||
{
|
||||
$quantity = $item->quantity;
|
||||
|
||||
switch ($item->purchasable_type) {
|
||||
case Perfume::class:
|
||||
$relation = 'perfumes';
|
||||
break;
|
||||
case Product::class:
|
||||
$relation = 'products';
|
||||
break;
|
||||
case Bottle::class:
|
||||
$relation = 'bottles';
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// Lock the row to prevent race conditions
|
||||
$existing = $warehouse->{$relation}()
|
||||
->where("{$relation}.id", $item->purchasable_id)
|
||||
->lockForUpdate()
|
||||
->withPivot('stock')
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$previousStock = $existing->pivot->stock ?? 0;
|
||||
$warehouse->{$relation}()->updateExistingPivot($item->purchasable_id, [
|
||||
'stock' => $previousStock + $quantity,
|
||||
]);
|
||||
} else {
|
||||
$previousStock = 0;
|
||||
$warehouse->{$relation}()->attach($item->purchasable_id, [
|
||||
'stock' => $quantity,
|
||||
]);
|
||||
}
|
||||
|
||||
$purchase = $item->purchase ?? \App\Models\Purchase::find($item->purchase_id);
|
||||
|
||||
if ($purchase) {
|
||||
StockActivityLogService::purchase(
|
||||
purchase: $purchase,
|
||||
product: $item->purchasable,
|
||||
quantity: $quantity,
|
||||
previousStock: $previousStock,
|
||||
newStock: $previousStock + $quantity
|
||||
);
|
||||
} else {
|
||||
StockActivityLogService::logWarehouse(
|
||||
logName: StockActivityLogService::LOG_PURCHASE,
|
||||
event: 'increase',
|
||||
description: 'Stok '.$item->purchasable->name.' di gudang '.$warehouse->name.' bertambah '.formatCurrencyNumber($quantity),
|
||||
performedOn: $item->purchasable,
|
||||
warehouse: $warehouse,
|
||||
quantityChange: $quantity,
|
||||
previousStock: $previousStock,
|
||||
newStock: $previousStock + $quantity,
|
||||
extraProperties: [
|
||||
'purchasable_id' => $item->purchasable_id,
|
||||
'purchasable_type' => $item->purchasable_type,
|
||||
'purchase_id' => $item->purchase_id ?? null,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function decreaseWarehouseStock($warehouse, $item): void
|
||||
{
|
||||
$quantity = $item->quantity;
|
||||
|
||||
switch ($item->purchasable_type) {
|
||||
case Perfume::class:
|
||||
$relation = 'perfumes';
|
||||
break;
|
||||
case Product::class:
|
||||
$relation = 'products';
|
||||
break;
|
||||
case Bottle::class:
|
||||
$relation = 'bottles';
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
$existing = $warehouse->{$relation}()
|
||||
->where("{$relation}.id", $item->purchasable_id)
|
||||
->lockForUpdate()
|
||||
->withPivot('stock')
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$currentStock = $existing->pivot->stock ?? 0;
|
||||
$newStock = max(0, $currentStock - $quantity);
|
||||
$warehouse->{$relation}()->updateExistingPivot($item->purchasable_id, [
|
||||
'stock' => $newStock,
|
||||
]);
|
||||
} else {
|
||||
$currentStock = 0;
|
||||
$newStock = 0;
|
||||
}
|
||||
|
||||
StockActivityLogService::logWarehouse(
|
||||
logName: StockActivityLogService::LOG_PURCHASE,
|
||||
event: 'decrease',
|
||||
description: 'Stok '.$item->purchasable->name.' di gudang '.$warehouse->name.' berkurang '.$quantity,
|
||||
performedOn: $item->purchasable,
|
||||
warehouse: $warehouse,
|
||||
quantityChange: -1 * abs($quantity),
|
||||
previousStock: $currentStock,
|
||||
newStock: $newStock,
|
||||
extraProperties: [
|
||||
'purchasable_id' => $item->purchasable_id,
|
||||
'purchasable_type' => $item->purchasable_type,
|
||||
'purchase_id' => $item->purchase_id,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,7 @@ public function up(): void
|
||||
{
|
||||
Schema::create('purchases', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('outlet_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('warehouse_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('invoice_number', 50)->nullable();
|
||||
$table->date('purchase_date');
|
||||
$table->unsignedInteger('total');
|
||||
@ -48,15 +48,15 @@
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Outlet <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:select variant="listbox" searchable placeholder="Pilih Outlet"
|
||||
wire:model.live="form.outlet_id">
|
||||
@foreach ($outlets as $key => $value)
|
||||
<flux:label>Gudang <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:select variant="listbox" searchable placeholder="Pilih Gudang"
|
||||
wire:model.live="form.warehouse_id">
|
||||
@foreach ($warehouses as $key => $value)
|
||||
<flux:select.option value="{{ $key }}">{{ $value }}
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:error name="form.outlet_id" />
|
||||
<flux:error name="form.warehouse_id" />
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
@ -17,7 +17,7 @@ class="text-sm">
|
||||
<flux:callout icon="megaphone" color="blue" class="mb-6">
|
||||
<flux:callout.heading>Informasi</flux:callout.heading>
|
||||
<flux:callout.text>
|
||||
Menghapus data belanja otomatis akan menambah stok.
|
||||
Menghapus data belanja otomatis menghapus stok dari gudang.
|
||||
</flux:callout.text>
|
||||
</flux:callout>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user