feat: Implement decimal precision for quantities and monetary values across feed purchases, delayed goods, and related calculations.
This commit is contained in:
parent
0a643efbb6
commit
48b182cd4b
@ -11,6 +11,7 @@
|
||||
use App\Filament\Columns\TimestampColumns;
|
||||
use App\Filament\Resources\Manage\Orders\Pages\ManageOrders;
|
||||
use App\Models\Order;
|
||||
use App\Models\Warehouse;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
@ -46,17 +47,39 @@ class OrderResource extends Resource
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
$recalculateTotal = function (callable $get, callable $set): void {
|
||||
$qty = $get('egg_quantity') ?? 0;
|
||||
$quantity = str_contains((string) $qty, ',')
|
||||
? (float) str_replace(',', '.', str_replace('.', '', (string) $qty))
|
||||
: (float) $qty;
|
||||
$unmaskQty = function ($state) {
|
||||
if (blank($state)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$price = $get('unit_price') ?? 0;
|
||||
$unitPrice = (int) str_replace('.', '', (string) $price);
|
||||
if (is_int($state) || is_float($state)) {
|
||||
return $state;
|
||||
}
|
||||
|
||||
$discountRaw = $get('discount') ?? 0;
|
||||
$discount = (int) str_replace('.', '', (string) $discountRaw);
|
||||
return (float) str($state)
|
||||
->replace(',', '.')
|
||||
->toString();
|
||||
};
|
||||
|
||||
$unmaskMoney = function ($state) {
|
||||
if (blank($state)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (is_int($state) || is_float($state)) {
|
||||
return $state;
|
||||
}
|
||||
|
||||
return (float) str($state)
|
||||
->replace('.', '')
|
||||
->replace(',', '.')
|
||||
->toString();
|
||||
};
|
||||
|
||||
$recalculateTotal = function (callable $get, callable $set) use ($unmaskMoney, $unmaskQty): void {
|
||||
$quantity = $unmaskQty($get('egg_quantity'));
|
||||
$unitPrice = $unmaskMoney($get('unit_price'));
|
||||
$discount = $unmaskMoney($get('discount'));
|
||||
|
||||
$subtotal = $quantity * $unitPrice;
|
||||
$totalAmount = max(0, $subtotal - $discount);
|
||||
@ -66,19 +89,32 @@ public static function form(Schema $schema): Schema
|
||||
|
||||
return $schema
|
||||
->components([
|
||||
DatePicker::make('order_date')
|
||||
->label('Tanggal Order')
|
||||
->default(now())
|
||||
->required()
|
||||
->native(false)
|
||||
->displayFormat('l, d F Y'),
|
||||
Grid::make(3)
|
||||
->schema([
|
||||
DatePicker::make('order_date')
|
||||
->label('Tanggal Order')
|
||||
->default(now())
|
||||
->required()
|
||||
->native(false)
|
||||
->displayFormat('l, d F Y'),
|
||||
|
||||
Select::make('customer_id')
|
||||
->label('Pelanggan')
|
||||
->relationship('customer', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->native(false),
|
||||
Select::make('warehouse_id')
|
||||
->label('Ambil Dari Gudang')
|
||||
->relationship('warehouse', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->native(false)
|
||||
->required()
|
||||
->default(fn () => Warehouse::first()?->id),
|
||||
|
||||
Select::make('customer_id')
|
||||
->label('Pelanggan')
|
||||
->relationship('customer', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->native(false),
|
||||
])
|
||||
->columnSpanFull(),
|
||||
|
||||
Grid::make(3)
|
||||
->schema([
|
||||
@ -93,9 +129,8 @@ public static function form(Schema $schema): Schema
|
||||
$recalculateTotal($get, $set);
|
||||
})
|
||||
->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 2)
|
||||
->dehydrateStateUsing(fn ($state) => str_contains((string) $state, ',')
|
||||
? (float) str_replace(',', '.', str_replace('.', '', (string) $state))
|
||||
: (float) $state),
|
||||
->formatStateUsing(fn ($state) => $state === null ? null : (float) $state)
|
||||
->dehydrateStateUsing($unmaskQty),
|
||||
|
||||
TextInput::make('unit_price')
|
||||
->label('Harga per Satuan')
|
||||
@ -105,7 +140,7 @@ public static function form(Schema $schema): Schema
|
||||
->required()
|
||||
->live()
|
||||
->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 0)
|
||||
->dehydrateStateUsing(fn ($state) => (int) str()->replace('.', '', (string) ($state ?? 0)))
|
||||
->dehydrateStateUsing($unmaskMoney)
|
||||
->afterStateUpdated(function ($state, callable $get, callable $set) use ($recalculateTotal): void {
|
||||
$recalculateTotal($get, $set);
|
||||
}),
|
||||
@ -123,6 +158,7 @@ public static function form(Schema $schema): Schema
|
||||
TextInput::make('discount')
|
||||
->label('Diskon')
|
||||
->placeholder('0')
|
||||
->autocomplete(false)
|
||||
->default(0)
|
||||
->prefix('Rp')
|
||||
->live()
|
||||
@ -130,7 +166,7 @@ public static function form(Schema $schema): Schema
|
||||
$recalculateTotal($get, $set);
|
||||
})
|
||||
->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 0)
|
||||
->dehydrateStateUsing(fn ($state) => (int) str()->replace('.', '', (string) ($state ?? 0))),
|
||||
->dehydrateStateUsing($unmaskMoney),
|
||||
|
||||
TextInput::make('total_amount')
|
||||
->label('Total')
|
||||
@ -138,10 +174,8 @@ public static function form(Schema $schema): Schema
|
||||
->readOnly()
|
||||
->prefix('Rp')
|
||||
->required()
|
||||
->formatStateUsing(
|
||||
fn ($state) => number_format((float) ($state ?? 0), 0, ',', '.')
|
||||
)
|
||||
->dehydrateStateUsing(fn ($state) => (float) str_replace('.', '', (string) ($state ?? 0))),
|
||||
->formatStateUsing(fn ($state) => number_format(($state ?? 0), 0, ',', '.'))
|
||||
->dehydrateStateUsing($unmaskMoney),
|
||||
|
||||
Textarea::make('notes')
|
||||
->label('Catatan')
|
||||
@ -166,10 +200,16 @@ public static function table(Table $table): Table
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('warehouse.name')
|
||||
->label('Gudang')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('egg_quantity')
|
||||
->label('Jumlah')
|
||||
->suffix(fn ($record) => ' '.$record->unit?->alias)
|
||||
->numeric(decimalPlaces: 2, decimalSeparator: ',', thousandsSeparator: '.')
|
||||
->getStateUsing(function ($record) {
|
||||
return str_replace('.', ',', (float) $record->egg_quantity)." {$record->unit?->alias}";
|
||||
})
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('unit_price')
|
||||
|
||||
@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Observers\OrderObserver;
|
||||
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
#[ObservedBy(OrderObserver::class)]
|
||||
|
||||
class Order extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
@ -19,8 +23,8 @@ protected function casts(): array
|
||||
'order_date' => 'date',
|
||||
'egg_quantity' => 'decimal:2',
|
||||
'unit_price' => 'integer',
|
||||
'total_amount' => 'decimal:2',
|
||||
'discount' => 'decimal:2',
|
||||
'total_amount' => 'integer',
|
||||
'discount' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
@ -33,4 +37,9 @@ public function unit(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Unit::class);
|
||||
}
|
||||
|
||||
public function warehouse(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Warehouse::class);
|
||||
}
|
||||
}
|
||||
|
||||
65
app/Observers/OrderObserver.php
Normal file
65
app/Observers/OrderObserver.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\Order;
|
||||
use App\Models\WarehouseStock;
|
||||
|
||||
class OrderObserver
|
||||
{
|
||||
/**
|
||||
* Handle the Order "created" event.
|
||||
*/
|
||||
public function created(Order $order): void
|
||||
{
|
||||
if ($order->warehouse_id) {
|
||||
WarehouseStock::adjustStock(
|
||||
$order->warehouse_id,
|
||||
$order->unit_id,
|
||||
false, // Assume sold eggs are not broken
|
||||
-$order->egg_quantity
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Order "updated" event.
|
||||
*/
|
||||
public function updated(Order $order): void
|
||||
{
|
||||
// Revert old stock if warehouse_id, unit_id, egg_quantity, or deleted_at changed
|
||||
if ($order->isDirty(['warehouse_id', 'unit_id', 'egg_quantity'])) {
|
||||
$oldWarehouseId = $order->getOriginal('warehouse_id');
|
||||
$oldUnitId = $order->getOriginal('unit_id');
|
||||
$oldQuantity = $order->getOriginal('egg_quantity');
|
||||
|
||||
if ($oldWarehouseId) {
|
||||
WarehouseStock::adjustStock($oldWarehouseId, $oldUnitId, false, $oldQuantity);
|
||||
}
|
||||
|
||||
if ($order->warehouse_id) {
|
||||
WarehouseStock::adjustStock($order->warehouse_id, $order->unit_id, false, -$order->egg_quantity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Order "deleted" event.
|
||||
*/
|
||||
public function deleted(Order $order): void
|
||||
{
|
||||
if (! $order->isForceDeleting() && $order->warehouse_id) {
|
||||
WarehouseStock::adjustStock($order->warehouse_id, $order->unit_id, false, $order->egg_quantity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Order "restored" event.
|
||||
*/
|
||||
public function restored(Order $order): void
|
||||
{
|
||||
if ($order->warehouse_id) {
|
||||
WarehouseStock::adjustStock($order->warehouse_id, $order->unit_id, false, -$order->egg_quantity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,13 +11,14 @@ public function up(): void
|
||||
Schema::create('orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('customer_id')->nullable()->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('warehouse_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->foreignId('unit_id')->constrained()->cascadeOnDelete();
|
||||
$table->decimal('egg_quantity', 10, 2);
|
||||
$table->decimal('egg_quantity', 12, 2);
|
||||
$table->unsignedInteger('unit_price');
|
||||
$table->date('order_date');
|
||||
$table->string('status', 20)->default('pending');
|
||||
$table->decimal('total_amount', 15, 2);
|
||||
$table->decimal('discount', 15, 2)->default(0);
|
||||
$table->unsignedBigInteger('total_amount');
|
||||
$table->unsignedBigInteger('discount')->default(0);
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user