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
{
return Attribute::make(
get: fn () => $this->relationLoaded('variants')
? number_format($this->variants->sum('reject_stock'), 0, ',', '.')
: '-',
get: function () {
$allVariants = $this->getAttribute('all_variants');
return $allVariants !== null
? number_format($allVariants->sum('reject_stock'), 0, ',', '.')
: '-';
},
);
}
public function totalRetailStockFormatted(): Attribute
{
return Attribute::make(
get: fn () => $this->relationLoaded('variants')
? number_format($this->variants->sum('retail_stock'), 0, ',', '.')
: '-',
get: function () {
$allVariants = $this->getAttribute('all_variants');
return $allVariants !== null
? number_format($allVariants->sum('retail_stock'), 0, ',', '.')
: '-';
},
);
}
public function totalStockFormatted(): Attribute
{
return Attribute::make(
get: fn () => $this->relationLoaded('variants')
? number_format($this->variants->sum('stock'), 0, ',', '.')
: '-',
get: function () {
$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
{
return Attribute::make(
get: fn () => $this->relationLoaded('prices')
? 'Rp '.number_format(
$this->prices->sum(fn (RawMaterialPrice $price) => (float) $price->stock * (int) $price->price),
0,
',',
'.',
)
: '-',
get: function () {
$source = $this->getAttribute('all_prices')
?? ($this->relationLoaded('prices') ? $this->prices : null);
return $source !== null
? 'Rp '.number_format(
$source->sum(fn (RawMaterialPrice $price) => (float) $price->stock * (int) $price->price),
0,
',',
'.',
)
: '-';
},
);
}
@ -81,10 +86,14 @@ public function totalStockFormatted(): Attribute
{
return Attribute::make(
get: function () {
if (! $this->relationLoaded('prices')) {
$source = $this->getAttribute('all_prices')
?? ($this->relationLoaded('prices') ? $this->prices : null);
if ($source === null) {
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'), ',');
return "{$formatted} {$this->unit->abbreviation()}";

View File

@ -69,42 +69,53 @@ public function paginateForIndex(array $tableQuery, string $status, string $cate
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
$paginator = $query
->paginate(25)
->withQueryString()
->through(function (Product $product) {
$product->variants->each(function (ProductVariant $variant): void {
$variant->setAttribute(
'images',
MediaPresenter::collection($variant, 'images'),
);
});
->withQueryString();
$pendingRequest = $product->pendingOwnerVerificationRequest;
// 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');
// Also check for pending retail stock transfer on variants
if ($pendingRequest === null) {
$pendingRequest = OwnerVerificationRequest::query()
->where('subject_type', ProductVariant::class)
->whereIn('subject_id', $product->variants->pluck('id'))
->where('action', OwnerVerificationAction::RETAIL_STOCK_TRANSFER)
->pending()
->latest()
->first();
}
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->setAttribute('has_pending_request', $pendingRequest !== null);
$product->setAttribute('pending_request_id', $pendingRequest?->id);
$product->setAttribute('pending_request_action', $pendingRequest?->action->value);
$product->setAttribute('pending_request_action_label', $pendingRequest?->action->label());
$product->setAttribute('pending_request_submitted_by_name', $pendingRequest?->submittedBy?->profile?->full_name ?? $pendingRequest?->submittedBy?->username);
$product->setAttribute(
'display_status',
$pendingRequest?->pendingToggleStatus() ?? $product->status->value,
$product->variants->each(function (ProductVariant $variant): void {
$variant->setAttribute(
'images',
MediaPresenter::collection($variant, 'images'),
);
return $product;
});
$pendingRequest = $product->pendingOwnerVerificationRequest;
// Also check for pending retail stock transfer on variants
if ($pendingRequest === null) {
$pendingRequest = OwnerVerificationRequest::query()
->where('subject_type', ProductVariant::class)
->whereIn('subject_id', $product->variants->pluck('id'))
->where('action', OwnerVerificationAction::RETAIL_STOCK_TRANSFER)
->pending()
->latest()
->first();
}
$product->setAttribute('has_pending_request', $pendingRequest !== null);
$product->setAttribute('pending_request_id', $pendingRequest?->id);
$product->setAttribute('pending_request_action', $pendingRequest?->action->value);
$product->setAttribute('pending_request_action_label', $pendingRequest?->action->label());
$product->setAttribute('pending_request_submitted_by_name', $pendingRequest?->submittedBy?->profile?->full_name ?? $pendingRequest?->submittedBy?->username);
$product->setAttribute(
'display_status',
$pendingRequest?->pendingToggleStatus() ?? $product->status->value,
);
return $product;
});
}
public function findForEdit(Product $product): Product

View File

@ -88,18 +88,30 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $st
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
$paginator = $query
->paginate(25)
->withQueryString()
->through(function (RawMaterial $rawMaterial) {
$rawMaterial->prices->each(function (RawMaterialPrice $price) use ($rawMaterial): void {
$price->setAttribute('images', MediaPresenter::collection($price, 'images'));
$price->setAttribute('unit_abbreviation', $rawMaterial->unit->abbreviation());
$price->unsetRelation('rawMaterial');
});
->withQueryString();
return $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 {
$price->setAttribute('images', MediaPresenter::collection($price, 'images'));
$price->setAttribute('unit_abbreviation', $rawMaterial->unit->abbreviation());
$price->unsetRelation('rawMaterial');
});
return $rawMaterial;
});
}
public function findForEdit(RawMaterial $rawMaterial): RawMaterial