feat: enhance stock calculations by loading all variants and prices in product and raw material services
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (8.3) (push) Waiting to run
tests / ci (8.4) (push) Waiting to run
tests / ci (8.5) (push) Waiting to run

This commit is contained in:
Yoga Pangestu 2026-07-30 11:42:52 +07:00
parent 9089b32d7b
commit 636508c5c0
4 changed files with 102 additions and 58 deletions

View File

@ -65,27 +65,39 @@ protected function draft(Builder $query): void
public function totalRejectStockFormatted(): Attribute public function totalRejectStockFormatted(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: fn () => $this->relationLoaded('variants') get: function () {
? number_format($this->variants->sum('reject_stock'), 0, ',', '.') $allVariants = $this->getAttribute('all_variants');
: '-',
return $allVariants !== null
? number_format($allVariants->sum('reject_stock'), 0, ',', '.')
: '-';
},
); );
} }
public function totalRetailStockFormatted(): Attribute public function totalRetailStockFormatted(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: fn () => $this->relationLoaded('variants') get: function () {
? number_format($this->variants->sum('retail_stock'), 0, ',', '.') $allVariants = $this->getAttribute('all_variants');
: '-',
return $allVariants !== null
? number_format($allVariants->sum('retail_stock'), 0, ',', '.')
: '-';
},
); );
} }
public function totalStockFormatted(): Attribute public function totalStockFormatted(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: fn () => $this->relationLoaded('variants') get: function () {
? number_format($this->variants->sum('stock'), 0, ',', '.') $allVariants = $this->getAttribute('all_variants');
: '-',
return $allVariants !== null
? number_format($allVariants->sum('stock'), 0, ',', '.')
: '-';
},
); );
} }

View File

@ -66,14 +66,19 @@ public function unitLabel(): Attribute
public function totalInventoryValueFormatted(): Attribute public function totalInventoryValueFormatted(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: fn () => $this->relationLoaded('prices') get: function () {
$source = $this->getAttribute('all_prices')
?? ($this->relationLoaded('prices') ? $this->prices : null);
return $source !== null
? 'Rp '.number_format( ? 'Rp '.number_format(
$this->prices->sum(fn (RawMaterialPrice $price) => (float) $price->stock * (int) $price->price), $source->sum(fn (RawMaterialPrice $price) => (float) $price->stock * (int) $price->price),
0, 0,
',', ',',
'.', '.',
) )
: '-', : '-';
},
); );
} }
@ -81,10 +86,14 @@ public function totalStockFormatted(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: function () { get: function () {
if (! $this->relationLoaded('prices')) { $source = $this->getAttribute('all_prices')
?? ($this->relationLoaded('prices') ? $this->prices : null);
if ($source === null) {
return '-'; return '-';
} }
$total = $this->prices->sum(fn (RawMaterialPrice $price) => (float) $price->stock);
$total = $source->sum(fn (RawMaterialPrice $price) => (float) $price->stock);
$formatted = rtrim(rtrim(number_format($total, 4, ',', '.'), '0'), ','); $formatted = rtrim(rtrim(number_format($total, 4, ',', '.'), '0'), ',');
return "{$formatted} {$this->unit->abbreviation()}"; return "{$formatted} {$this->unit->abbreviation()}";

View File

@ -69,10 +69,21 @@ public function paginateForIndex(array $tableQuery, string $status, string $cate
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']); $this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query $paginator = $query
->paginate(25) ->paginate(25)
->withQueryString() ->withQueryString();
->through(function (Product $product) {
// Load ALL variants for total stock calculation (not affected by search)
$productIds = $paginator->pluck('id')->all();
$allVariantsByProduct = ProductVariant::whereIn('product_id', $productIds)
->get()
->groupBy('product_id');
return $paginator->through(function (Product $product) use ($allVariantsByProduct): Product {
// Set all variants for total stock calculation
$allVariants = $allVariantsByProduct->get($product->id, collect());
$product->setAttribute('all_variants', $allVariants);
$product->variants->each(function (ProductVariant $variant): void { $product->variants->each(function (ProductVariant $variant): void {
$variant->setAttribute( $variant->setAttribute(
'images', 'images',

View File

@ -88,10 +88,22 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $st
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']); $this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query $paginator = $query
->paginate(25) ->paginate(25)
->withQueryString() ->withQueryString();
->through(function (RawMaterial $rawMaterial) {
// Load ALL prices for total stock calculation (not affected by search)
$rawMaterialIds = $paginator->pluck('id')->all();
$allPricesByRawMaterial = RawMaterialPrice::whereIn('raw_material_id', $rawMaterialIds)
->orderBy('created_at')
->get()
->groupBy('raw_material_id');
return $paginator->through(function (RawMaterial $rawMaterial) use ($allPricesByRawMaterial): RawMaterial {
// Set all prices for total stock calculation
$allPrices = $allPricesByRawMaterial->get($rawMaterial->id, collect());
$rawMaterial->setAttribute('all_prices', $allPrices);
$rawMaterial->prices->each(function (RawMaterialPrice $price) use ($rawMaterial): void { $rawMaterial->prices->each(function (RawMaterialPrice $price) use ($rawMaterial): void {
$price->setAttribute('images', MediaPresenter::collection($price, 'images')); $price->setAttribute('images', MediaPresenter::collection($price, 'images'));
$price->setAttribute('unit_abbreviation', $rawMaterial->unit->abbreviation()); $price->setAttribute('unit_abbreviation', $rawMaterial->unit->abbreviation());