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\Columns\TimestampColumns;
|
||||||
use App\Filament\Resources\Manage\Orders\Pages\ManageOrders;
|
use App\Filament\Resources\Manage\Orders\Pages\ManageOrders;
|
||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
|
use App\Models\Warehouse;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Actions\BulkActionGroup;
|
use Filament\Actions\BulkActionGroup;
|
||||||
use Filament\Forms\Components\DatePicker;
|
use Filament\Forms\Components\DatePicker;
|
||||||
@ -46,17 +47,39 @@ class OrderResource extends Resource
|
|||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
$recalculateTotal = function (callable $get, callable $set): void {
|
$unmaskQty = function ($state) {
|
||||||
$qty = $get('egg_quantity') ?? 0;
|
if (blank($state)) {
|
||||||
$quantity = str_contains((string) $qty, ',')
|
return 0;
|
||||||
? (float) str_replace(',', '.', str_replace('.', '', (string) $qty))
|
}
|
||||||
: (float) $qty;
|
|
||||||
|
|
||||||
$price = $get('unit_price') ?? 0;
|
if (is_int($state) || is_float($state)) {
|
||||||
$unitPrice = (int) str_replace('.', '', (string) $price);
|
return $state;
|
||||||
|
}
|
||||||
|
|
||||||
$discountRaw = $get('discount') ?? 0;
|
return (float) str($state)
|
||||||
$discount = (int) str_replace('.', '', (string) $discountRaw);
|
->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;
|
$subtotal = $quantity * $unitPrice;
|
||||||
$totalAmount = max(0, $subtotal - $discount);
|
$totalAmount = max(0, $subtotal - $discount);
|
||||||
@ -66,19 +89,32 @@ public static function form(Schema $schema): Schema
|
|||||||
|
|
||||||
return $schema
|
return $schema
|
||||||
->components([
|
->components([
|
||||||
DatePicker::make('order_date')
|
Grid::make(3)
|
||||||
->label('Tanggal Order')
|
->schema([
|
||||||
->default(now())
|
DatePicker::make('order_date')
|
||||||
->required()
|
->label('Tanggal Order')
|
||||||
->native(false)
|
->default(now())
|
||||||
->displayFormat('l, d F Y'),
|
->required()
|
||||||
|
->native(false)
|
||||||
|
->displayFormat('l, d F Y'),
|
||||||
|
|
||||||
Select::make('customer_id')
|
Select::make('warehouse_id')
|
||||||
->label('Pelanggan')
|
->label('Ambil Dari Gudang')
|
||||||
->relationship('customer', 'name')
|
->relationship('warehouse', 'name')
|
||||||
->searchable()
|
->searchable()
|
||||||
->preload()
|
->preload()
|
||||||
->native(false),
|
->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)
|
Grid::make(3)
|
||||||
->schema([
|
->schema([
|
||||||
@ -93,9 +129,8 @@ public static function form(Schema $schema): Schema
|
|||||||
$recalculateTotal($get, $set);
|
$recalculateTotal($get, $set);
|
||||||
})
|
})
|
||||||
->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 2)
|
->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 2)
|
||||||
->dehydrateStateUsing(fn ($state) => str_contains((string) $state, ',')
|
->formatStateUsing(fn ($state) => $state === null ? null : (float) $state)
|
||||||
? (float) str_replace(',', '.', str_replace('.', '', (string) $state))
|
->dehydrateStateUsing($unmaskQty),
|
||||||
: (float) $state),
|
|
||||||
|
|
||||||
TextInput::make('unit_price')
|
TextInput::make('unit_price')
|
||||||
->label('Harga per Satuan')
|
->label('Harga per Satuan')
|
||||||
@ -105,7 +140,7 @@ public static function form(Schema $schema): Schema
|
|||||||
->required()
|
->required()
|
||||||
->live()
|
->live()
|
||||||
->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 0)
|
->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 {
|
->afterStateUpdated(function ($state, callable $get, callable $set) use ($recalculateTotal): void {
|
||||||
$recalculateTotal($get, $set);
|
$recalculateTotal($get, $set);
|
||||||
}),
|
}),
|
||||||
@ -123,6 +158,7 @@ public static function form(Schema $schema): Schema
|
|||||||
TextInput::make('discount')
|
TextInput::make('discount')
|
||||||
->label('Diskon')
|
->label('Diskon')
|
||||||
->placeholder('0')
|
->placeholder('0')
|
||||||
|
->autocomplete(false)
|
||||||
->default(0)
|
->default(0)
|
||||||
->prefix('Rp')
|
->prefix('Rp')
|
||||||
->live()
|
->live()
|
||||||
@ -130,7 +166,7 @@ public static function form(Schema $schema): Schema
|
|||||||
$recalculateTotal($get, $set);
|
$recalculateTotal($get, $set);
|
||||||
})
|
})
|
||||||
->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 0)
|
->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 0)
|
||||||
->dehydrateStateUsing(fn ($state) => (int) str()->replace('.', '', (string) ($state ?? 0))),
|
->dehydrateStateUsing($unmaskMoney),
|
||||||
|
|
||||||
TextInput::make('total_amount')
|
TextInput::make('total_amount')
|
||||||
->label('Total')
|
->label('Total')
|
||||||
@ -138,10 +174,8 @@ public static function form(Schema $schema): Schema
|
|||||||
->readOnly()
|
->readOnly()
|
||||||
->prefix('Rp')
|
->prefix('Rp')
|
||||||
->required()
|
->required()
|
||||||
->formatStateUsing(
|
->formatStateUsing(fn ($state) => number_format(($state ?? 0), 0, ',', '.'))
|
||||||
fn ($state) => number_format((float) ($state ?? 0), 0, ',', '.')
|
->dehydrateStateUsing($unmaskMoney),
|
||||||
)
|
|
||||||
->dehydrateStateUsing(fn ($state) => (float) str_replace('.', '', (string) ($state ?? 0))),
|
|
||||||
|
|
||||||
Textarea::make('notes')
|
Textarea::make('notes')
|
||||||
->label('Catatan')
|
->label('Catatan')
|
||||||
@ -166,10 +200,16 @@ public static function table(Table $table): Table
|
|||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
|
|
||||||
|
TextColumn::make('warehouse.name')
|
||||||
|
->label('Gudang')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
TextColumn::make('egg_quantity')
|
TextColumn::make('egg_quantity')
|
||||||
->label('Jumlah')
|
->label('Jumlah')
|
||||||
->suffix(fn ($record) => ' '.$record->unit?->alias)
|
->getStateUsing(function ($record) {
|
||||||
->numeric(decimalPlaces: 2, decimalSeparator: ',', thousandsSeparator: '.')
|
return str_replace('.', ',', (float) $record->egg_quantity)." {$record->unit?->alias}";
|
||||||
|
})
|
||||||
->sortable(),
|
->sortable(),
|
||||||
|
|
||||||
TextColumn::make('unit_price')
|
TextColumn::make('unit_price')
|
||||||
|
|||||||
@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Observers\OrderObserver;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
#[ObservedBy(OrderObserver::class)]
|
||||||
|
|
||||||
class Order extends Model
|
class Order extends Model
|
||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes;
|
use HasFactory, SoftDeletes;
|
||||||
@ -19,8 +23,8 @@ protected function casts(): array
|
|||||||
'order_date' => 'date',
|
'order_date' => 'date',
|
||||||
'egg_quantity' => 'decimal:2',
|
'egg_quantity' => 'decimal:2',
|
||||||
'unit_price' => 'integer',
|
'unit_price' => 'integer',
|
||||||
'total_amount' => 'decimal:2',
|
'total_amount' => 'integer',
|
||||||
'discount' => 'decimal:2',
|
'discount' => 'integer',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,4 +37,9 @@ public function unit(): BelongsTo
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Unit::class);
|
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) {
|
Schema::create('orders', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('customer_id')->nullable()->constrained()->cascadeOnDelete();
|
$table->foreignId('customer_id')->nullable()->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('warehouse_id')->nullable()->constrained()->nullOnDelete();
|
||||||
$table->foreignId('unit_id')->constrained()->cascadeOnDelete();
|
$table->foreignId('unit_id')->constrained()->cascadeOnDelete();
|
||||||
$table->decimal('egg_quantity', 10, 2);
|
$table->decimal('egg_quantity', 12, 2);
|
||||||
$table->unsignedInteger('unit_price');
|
$table->unsignedInteger('unit_price');
|
||||||
$table->date('order_date');
|
$table->date('order_date');
|
||||||
$table->string('status', 20)->default('pending');
|
$table->string('status', 20)->default('pending');
|
||||||
$table->decimal('total_amount', 15, 2);
|
$table->unsignedBigInteger('total_amount');
|
||||||
$table->decimal('discount', 15, 2)->default(0);
|
$table->unsignedBigInteger('discount')->default(0);
|
||||||
$table->text('notes')->nullable();
|
$table->text('notes')->nullable();
|
||||||
$table->timestamp('created_at')->useCurrent();
|
$table->timestamp('created_at')->useCurrent();
|
||||||
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
|
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user