diff --git a/app/Enums/Role.php b/app/Enums/Role.php
index a1819f6..39872f7 100644
--- a/app/Enums/Role.php
+++ b/app/Enums/Role.php
@@ -95,6 +95,7 @@ public function permissions(): array
Permission::CUSTOMERS_VIEW,
Permission::PRODUCTS_VIEW,
+ Permission::STOCKS_VIEW,
Permission::ORDERS_VIEW,
@@ -166,6 +167,7 @@ public function permissions(): array
Permission::PRODUCTS_UPDATE,
Permission::PRODUCTS_DELETE,
Permission::PRODUCTS_TOGGLE_STATUS,
+ Permission::STOCKS_VIEW,
Permission::OWNER_VERIFICATIONS_VIEW,
@@ -293,6 +295,7 @@ public function permissions(): array
Permission::RAW_MATERIALS_CREATE,
Permission::RAW_MATERIALS_UPDATE,
Permission::RAW_MATERIALS_TOGGLE_STATUS,
+ Permission::STOCKS_VIEW,
Permission::OWNER_VERIFICATIONS_VIEW,
@@ -394,6 +397,7 @@ public function permissions(): array
Permission::LEAVE_REQUESTS_DELETE,
Permission::PRODUCTS_VIEW,
+ Permission::STOCKS_VIEW,
Permission::STOK_OPNAMES_VIEW,
Permission::STOK_OPNAMES_CREATE,
diff --git a/app/Http/Controllers/Admin/Manage/Stock/StockHistoryController.php b/app/Http/Controllers/Admin/Manage/Stock/StockHistoryController.php
new file mode 100644
index 0000000..319a868
--- /dev/null
+++ b/app/Http/Controllers/Admin/Manage/Stock/StockHistoryController.php
@@ -0,0 +1,48 @@
+string('stockable_type')->toString();
+ $stockableId = (int) $request->string('stockable_id')->toString();
+
+ $modelClass = match ($stockableType) {
+ 'product-variant' => ProductVariant::class,
+ 'raw-material-price' => RawMaterialPrice::class,
+ default => abort(404, 'Tipe stok tidak valid.'),
+ };
+
+ $stockable = $modelClass::with(
+ $stockableType === 'product-variant'
+ ? ['product', 'stockMutations.user.profile', 'stockMutations.source']
+ : ['rawMaterial', 'stockMutations.user.profile', 'stockMutations.source']
+ )->findOrFail($stockableId);
+
+ $mutations = $stockable->stockMutations()
+ ->with(['user.profile', 'source'])
+ ->latest()
+ ->paginate(50);
+
+ $title = $stockableType === 'product-variant'
+ ? "{$stockable->product?->name} - {$stockable->name}"
+ : "{$stockable->rawMaterial?->name} - {$stockable->variant}";
+
+ return Inertia::render('admin/manage/stock/History', [
+ 'title' => $title,
+ 'stockableType' => $stockableType,
+ 'stockableId' => $stockable->id,
+ 'mutations' => $mutations,
+ ]);
+ }
+}
diff --git a/app/Models/ProductVariant.php b/app/Models/ProductVariant.php
index 1dfe96f..b526633 100644
--- a/app/Models/ProductVariant.php
+++ b/app/Models/ProductVariant.php
@@ -91,4 +91,9 @@ public function product(): BelongsTo
{
return $this->belongsTo(Product::class)->withTrashed();
}
+
+ public function stockMutations(): \Illuminate\Database\Eloquent\Relations\MorphMany
+ {
+ return $this->morphMany(StockMutation::class, 'stockable');
+ }
}
diff --git a/app/Models/RawMaterialPrice.php b/app/Models/RawMaterialPrice.php
index a63cb89..ac3a9a2 100644
--- a/app/Models/RawMaterialPrice.php
+++ b/app/Models/RawMaterialPrice.php
@@ -91,4 +91,9 @@ public function rawMaterial(): BelongsTo
{
return $this->belongsTo(RawMaterial::class)->withTrashed();
}
+
+ public function stockMutations(): \Illuminate\Database\Eloquent\Relations\MorphMany
+ {
+ return $this->morphMany(StockMutation::class, 'stockable');
+ }
}
diff --git a/app/Models/StockMutation.php b/app/Models/StockMutation.php
new file mode 100644
index 0000000..68c6558
--- /dev/null
+++ b/app/Models/StockMutation.php
@@ -0,0 +1,59 @@
+ 'decimal:4',
+ 'stock_before' => 'decimal:4',
+ 'stock_after' => 'decimal:4',
+ ];
+ }
+
+ public function createdAtFormatted(): Attribute
+ {
+ return Attribute::make(
+ get: fn () => $this->created_at?->translatedFormat('l, d F Y H:i'),
+ );
+ }
+
+ public function typeLabel(): Attribute
+ {
+ return Attribute::make(
+ get: fn () => match ($this->type) {
+ 'in' => 'Masuk',
+ 'out' => 'Keluar',
+ 'transfer' => 'Transfer',
+ 'adjustment' => 'Penyesuaian',
+ default => $this->type,
+ },
+ );
+ }
+
+ public function stockable(): MorphTo
+ {
+ return $this->morphTo();
+ }
+
+ public function source(): MorphTo
+ {
+ return $this->morphTo();
+ }
+
+ public function user(): BelongsTo
+ {
+ return $this->belongsTo(User::class);
+ }
+}
diff --git a/app/Services/Manage/CuttingService.php b/app/Services/Manage/CuttingService.php
index 3494a7b..84682d0 100644
--- a/app/Services/Manage/CuttingService.php
+++ b/app/Services/Manage/CuttingService.php
@@ -30,6 +30,7 @@ class CuttingService
public function __construct(
private readonly PushNotificationService $pushNotificationService,
private readonly MediaService $mediaService,
+ private readonly StockMutationService $stockMutationService,
) {}
public function paginateForIndex(array $tableQuery, User $user): LengthAwarePaginator
@@ -772,22 +773,51 @@ private function deductMaterialStock(Cutting $cutting): void
]);
}
- if ((float) $price->stock < $totalTaken) {
+ $stockBefore = (float) $price->stock;
+
+ if ($stockBefore < $totalTaken) {
throw ValidationException::withMessages([
'materials' => "Stok {$price->rawMaterial?->name} ({$price->variant}) tidak mencukupi.",
]);
}
$price->decrement('stock', $totalTaken);
+
+ $this->stockMutationService->record(
+ stockable: $price,
+ type: 'out',
+ quantity: -$totalTaken,
+ stockBefore: $stockBefore,
+ stockAfter: $stockBefore - $totalTaken,
+ source: $cutting,
+ description: "Cutting #{$cutting->id}",
+ );
}
}
private function reverseTotalMaterialStock(Cutting $cutting): void
{
foreach ($cutting->materials as $material) {
- RawMaterialPrice::query()
- ->whereKey($material->raw_material_price_id)
- ->increment('stock', (float) $material->material_usage);
+ $price = RawMaterialPrice::query()->lockForUpdate()->find($material->raw_material_price_id);
+
+ if ($price === null) {
+ continue;
+ }
+
+ $stockBefore = (float) $price->stock;
+ $totalReturned = (float) $material->material_usage;
+
+ $price->increment('stock', $totalReturned);
+
+ $this->stockMutationService->record(
+ stockable: $price,
+ type: 'in',
+ quantity: $totalReturned,
+ stockBefore: $stockBefore,
+ stockAfter: $stockBefore + $totalReturned,
+ source: $cutting,
+ description: "Cutting #{$cutting->id} (batal)",
+ );
}
}
diff --git a/app/Services/Manage/OrderService.php b/app/Services/Manage/OrderService.php
index 19c68b5..4a0d544 100644
--- a/app/Services/Manage/OrderService.php
+++ b/app/Services/Manage/OrderService.php
@@ -38,6 +38,7 @@ public function __construct(
private readonly CashService $cashService,
private readonly CuttingResultPriceResolver $cuttingResultPriceResolver,
private readonly MediaService $mediaService,
+ private readonly StockMutationService $stockMutationService,
) {}
public function defaultPriceType(OrderChannel $channel): ?PriceType
@@ -770,19 +771,45 @@ private function applyDraftPrices(EloquentCollection $items, PriceType $priceTyp
private function decrementStock(OrderItem $item): void
{
$stockQuality = $item->stock_quality ?? ProductStockQuality::GOOD;
+ $column = $this->stockColumn($stockQuality);
- ProductVariant::query()
- ->whereKey($item->product_variant_id)
- ->decrement($this->stockColumn($stockQuality), $item->quantity);
+ $variant = ProductVariant::query()->findOrFail($item->product_variant_id);
+ $stockBefore = (int) $variant->{$column};
+
+ $variant->decrement($column, $item->quantity);
+
+ $this->stockMutationService->record(
+ stockable: $variant,
+ type: 'out',
+ quantity: -$item->quantity,
+ stockBefore: $stockBefore,
+ stockAfter: $stockBefore - $item->quantity,
+ stockQuality: $stockQuality->value,
+ source: $item->order,
+ description: $item->order ? "Pesanan #{$item->order->order_number}" : null,
+ );
}
private function incrementStock(OrderItem $item): void
{
$stockQuality = $item->stock_quality ?? ProductStockQuality::GOOD;
+ $column = $this->stockColumn($stockQuality);
- ProductVariant::query()
- ->whereKey($item->product_variant_id)
- ->increment($this->stockColumn($stockQuality), $item->quantity);
+ $variant = ProductVariant::query()->findOrFail($item->product_variant_id);
+ $stockBefore = (int) $variant->{$column};
+
+ $variant->increment($column, $item->quantity);
+
+ $this->stockMutationService->record(
+ stockable: $variant,
+ type: 'in',
+ quantity: $item->quantity,
+ stockBefore: $stockBefore,
+ stockAfter: $stockBefore + $item->quantity,
+ stockQuality: $stockQuality->value,
+ source: $item->order,
+ description: $item->order ? "Pesanan #{$item->order->order_number} (batal)" : null,
+ );
}
private function availableStock(ProductVariant $variant, ProductStockQuality $stockQuality): int
diff --git a/app/Services/Manage/PurchaseService.php b/app/Services/Manage/PurchaseService.php
index b145585..494a4f9 100644
--- a/app/Services/Manage/PurchaseService.php
+++ b/app/Services/Manage/PurchaseService.php
@@ -29,6 +29,7 @@ class PurchaseService
public function __construct(
private readonly MediaService $mediaService,
private readonly PushNotificationService $pushNotificationService,
+ private readonly StockMutationService $stockMutationService,
) {}
public function paginateForIndex(array $tableQuery): LengthAwarePaginator
@@ -597,16 +598,38 @@ private function presentDraftItem(PurchaseItem $item): array
private function incrementStock(PurchaseItem $item): void
{
- RawMaterialPrice::query()
- ->whereKey($item->raw_material_price_id)
- ->increment('stock', $item->quantity);
+ $price = RawMaterialPrice::query()->findOrFail($item->raw_material_price_id);
+ $stockBefore = (float) $price->stock;
+
+ $price->increment('stock', $item->quantity);
+
+ $this->stockMutationService->record(
+ stockable: $price,
+ type: 'in',
+ quantity: (float) $item->quantity,
+ stockBefore: $stockBefore,
+ stockAfter: $stockBefore + (float) $item->quantity,
+ source: $item->purchase,
+ description: $item->purchase ? "Belanja #{$item->purchase->id}" : null,
+ );
}
private function decrementStock(PurchaseItem $item): void
{
- RawMaterialPrice::query()
- ->whereKey($item->raw_material_price_id)
- ->decrement('stock', $item->quantity);
+ $price = RawMaterialPrice::query()->findOrFail($item->raw_material_price_id);
+ $stockBefore = (float) $price->stock;
+
+ $price->decrement('stock', $item->quantity);
+
+ $this->stockMutationService->record(
+ stockable: $price,
+ type: 'out',
+ quantity: -((float) $item->quantity),
+ stockBefore: $stockBefore,
+ stockAfter: $stockBefore - (float) $item->quantity,
+ source: $item->purchase,
+ description: $item->purchase ? "Belanja #{$item->purchase->id}" : null,
+ );
}
private function notifyPurchase(string $typeLabel, string $body, string $url, ?string $userId = null): void
diff --git a/app/Services/Manage/RestockService.php b/app/Services/Manage/RestockService.php
index c494d09..adb1725 100644
--- a/app/Services/Manage/RestockService.php
+++ b/app/Services/Manage/RestockService.php
@@ -32,6 +32,7 @@ class RestockService
public function __construct(
private readonly MediaService $mediaService,
private readonly PushNotificationService $pushNotificationService,
+ private readonly StockMutationService $stockMutationService,
) {}
public function paginateForIndex(array $tableQuery): LengthAwarePaginator
@@ -529,16 +530,44 @@ private function stockColumn(ProductStockQuality $stockType): string
private function incrementStock(RestockItem $item, ProductStockQuality $stockType): void
{
- ProductVariant::query()
- ->whereKey($item->product_variant_id)
- ->increment($this->stockColumn($stockType), $item->quantity);
+ $column = $this->stockColumn($stockType);
+
+ $variant = ProductVariant::query()->findOrFail($item->product_variant_id);
+ $stockBefore = (int) $variant->{$column};
+
+ $variant->increment($column, $item->quantity);
+
+ $this->stockMutationService->record(
+ stockable: $variant,
+ type: 'in',
+ quantity: $item->quantity,
+ stockBefore: $stockBefore,
+ stockAfter: $stockBefore + $item->quantity,
+ stockQuality: $stockType->value,
+ source: $item->restock,
+ description: $item->restock ? "Restock #{$item->restock->id}" : null,
+ );
}
private function decrementStock(RestockItem $item, ProductStockQuality $stockType): void
{
- ProductVariant::query()
- ->whereKey($item->product_variant_id)
- ->decrement($this->stockColumn($stockType), $item->quantity);
+ $column = $this->stockColumn($stockType);
+
+ $variant = ProductVariant::query()->findOrFail($item->product_variant_id);
+ $stockBefore = (int) $variant->{$column};
+
+ $variant->decrement($column, $item->quantity);
+
+ $this->stockMutationService->record(
+ stockable: $variant,
+ type: 'out',
+ quantity: -$item->quantity,
+ stockBefore: $stockBefore,
+ stockAfter: $stockBefore - $item->quantity,
+ stockQuality: $stockType->value,
+ source: $item->restock,
+ description: $item->restock ? "Restock #{$item->restock->id} (batal)" : null,
+ );
}
private function notifyForPendingRequest(User $user, string $typeLabel, string $body, string $submitterUrl, ?string $search = null): void
diff --git a/app/Services/Manage/RetailStockService.php b/app/Services/Manage/RetailStockService.php
index 4867760..f6715ce 100644
--- a/app/Services/Manage/RetailStockService.php
+++ b/app/Services/Manage/RetailStockService.php
@@ -2,8 +2,8 @@
namespace App\Services\Manage;
+use App\Models\OwnerVerificationRequest;
use App\Models\ProductVariant;
-use App\Models\RetailStockHistory;
use App\Models\User;
use App\Services\System\PushNotificationService;
use Illuminate\Support\Facades\DB;
@@ -13,6 +13,7 @@ class RetailStockService
{
public function __construct(
private readonly PushNotificationService $pushNotificationService,
+ private readonly StockMutationService $stockMutationService,
) {}
public function transfer(int $variantId, int $quantity, User $user, ?string $notes = null): void
@@ -68,23 +69,33 @@ private function executeTransfer(ProductVariant $variant, int $quantity, User $u
->lockForUpdate()
->firstOrFail();
- $stockBefore = $variant->stock;
+ $goodStockBefore = $variant->stock;
$retailStockBefore = $variant->retail_stock;
$variant->decrement('stock', $quantity);
$variant->increment('retail_stock', $quantity);
- RetailStockHistory::create([
- 'product_variant_id' => $variant->id,
- 'user_id' => $user->id,
- 'quantity' => $quantity,
- 'stock_before' => $stockBefore,
- 'retail_stock_before' => $retailStockBefore,
- 'stock_after' => $stockBefore - $quantity,
- 'retail_stock_after' => $retailStockBefore + $quantity,
- 'notes' => $notes,
- 'created_at' => now(),
- ]);
+ $this->stockMutationService->record(
+ stockable: $variant,
+ type: 'out',
+ quantity: -$quantity,
+ stockBefore: $goodStockBefore,
+ stockAfter: $goodStockBefore - $quantity,
+ stockQuality: 'good',
+ description: $notes ? "Transfer ke stok ecer: {$notes}" : 'Transfer ke stok ecer',
+ user: $user,
+ );
+
+ $this->stockMutationService->record(
+ stockable: $variant,
+ type: 'in',
+ quantity: $quantity,
+ stockBefore: $retailStockBefore,
+ stockAfter: $retailStockBefore + $quantity,
+ stockQuality: 'retail',
+ description: $notes ? "Transfer dari stok bagus: {$notes}" : 'Transfer dari stok bagus',
+ user: $user,
+ );
});
}
}
diff --git a/app/Services/Manage/StockMutationService.php b/app/Services/Manage/StockMutationService.php
new file mode 100644
index 0000000..65cbfdb
--- /dev/null
+++ b/app/Services/Manage/StockMutationService.php
@@ -0,0 +1,36 @@
+ $stockable->getMorphClass(),
+ 'stockable_id' => $stockable->id,
+ 'type' => $type,
+ 'source_type' => $source?->getMorphClass(),
+ 'source_id' => $source?->id,
+ 'quantity' => $quantity,
+ 'stock_before' => $stockBefore,
+ 'stock_after' => $stockAfter,
+ 'stock_quality' => $stockQuality,
+ 'description' => $description,
+ 'user_id' => $user?->id ?? auth()->id(),
+ ]);
+ }
+}
diff --git a/app/Services/Manage/StokOpnameService.php b/app/Services/Manage/StokOpnameService.php
index ab1a23d..fd54dae 100644
--- a/app/Services/Manage/StokOpnameService.php
+++ b/app/Services/Manage/StokOpnameService.php
@@ -23,6 +23,7 @@ class StokOpnameService
public function __construct(
private readonly PushNotificationService $pushNotificationService,
+ private readonly StockMutationService $stockMutationService,
) {}
public function paginateForIndex(array $tableQuery, User $user): LengthAwarePaginator
@@ -259,9 +260,25 @@ public function verify(StokOpname $stokOpname, User $user, ?string $verification
ProductStockQuality::RETAIL => 'retail_stock',
ProductStockQuality::REJECT => 'reject_stock',
};
- $item->productVariant()->update([
+
+ $variant = $item->productVariant;
+ $stockBefore = (int) $variant->{$column};
+
+ $variant->update([
$column => $item->physical_stock,
]);
+
+ $this->stockMutationService->record(
+ stockable: $variant,
+ type: 'adjustment',
+ quantity: $item->difference,
+ stockBefore: $stockBefore,
+ stockAfter: $item->physical_stock,
+ stockQuality: $item->stock_quality->value,
+ source: $stokOpname,
+ description: "Stok Opname #{$stokOpname->id}",
+ user: $user,
+ );
}
}
diff --git a/app/Services/Master/ProductService.php b/app/Services/Master/ProductService.php
index 7d9e3eb..45d0c85 100644
--- a/app/Services/Master/ProductService.php
+++ b/app/Services/Master/ProductService.php
@@ -13,6 +13,7 @@
use App\Models\User;
use App\Services\Concerns\CachesQuery;
use App\Services\Concerns\RunsInTransaction;
+use App\Services\Manage\StockMutationService;
use App\Services\Media\MediaService;
use App\Services\System\PushNotificationService;
use App\Support\Media\MediaPresenter;
@@ -29,6 +30,7 @@ class ProductService
public function __construct(
private readonly MediaService $mediaService,
private readonly PushNotificationService $pushNotificationService,
+ private readonly StockMutationService $stockMutationService,
) {}
public function paginateForIndex(array $tableQuery, string $status, string $categoryId = '', string $stockStatus = '', string $productId = ''): LengthAwarePaginator
@@ -145,6 +147,8 @@ function () use ($validated, $user, $isOwner, $isDraft): Product {
'retail_stock' => $variantData['retail_stock'],
]);
+ $this->recordVariantStockMutations($variant, $variantData);
+
$this->syncVariantImages($variant, $variantData, $index);
if (! empty($variantData['prices'])) {
@@ -540,6 +544,9 @@ private function applyPayloadToProduct(
foreach ($payload['variants'] ?? [] as $index => $variantData) {
if (! empty($variantData['id'])) {
$variant = $product->variants()->findOrFail($variantData['id']);
+
+ $this->recordVariantStockChanges($variant, $variantData);
+
$variant->update([
'name' => $variantData['name'],
'stock' => $variantData['stock'],
@@ -572,6 +579,8 @@ private function applyPayloadToProduct(
'retail_stock' => $variantData['retail_stock'],
]);
+ $this->recordVariantStockMutations($variant, $variantData);
+
if ($verificationRequest !== null) {
$this->copyRequestVariantImages($verificationRequest, (int) $index, $variant);
} else {
@@ -853,4 +862,59 @@ private function applySorting(Builder $query, string $sort, string $direction):
$query->latest();
}
+
+ private function recordVariantStockMutations(ProductVariant $variant, array $variantData): void
+ {
+ $stockQualities = [
+ ['column' => 'stock', 'quality' => 'good'],
+ ['column' => 'reject_stock', 'quality' => 'reject'],
+ ['column' => 'retail_stock', 'quality' => 'retail'],
+ ];
+
+ foreach ($stockQualities as $sq) {
+ $value = (int) ($variantData[$sq['column']] ?? 0);
+
+ if ($value <= 0) {
+ continue;
+ }
+
+ $this->stockMutationService->record(
+ stockable: $variant,
+ type: 'adjustment',
+ quantity: $value,
+ stockBefore: 0,
+ stockAfter: $value,
+ stockQuality: $sq['quality'],
+ description: 'Stok awal dari master data',
+ );
+ }
+ }
+
+ private function recordVariantStockChanges(ProductVariant $variant, array $variantData): void
+ {
+ $stockQualities = [
+ ['column' => 'stock', 'quality' => 'good'],
+ ['column' => 'reject_stock', 'quality' => 'reject'],
+ ['column' => 'retail_stock', 'quality' => 'retail'],
+ ];
+
+ foreach ($stockQualities as $sq) {
+ $oldValue = (int) $variant->{$sq['column']};
+ $newValue = (int) ($variantData[$sq['column']] ?? 0);
+
+ if ($newValue === $oldValue) {
+ continue;
+ }
+
+ $this->stockMutationService->record(
+ stockable: $variant,
+ type: 'adjustment',
+ quantity: $newValue - $oldValue,
+ stockBefore: $oldValue,
+ stockAfter: $newValue,
+ stockQuality: $sq['quality'],
+ description: 'Edit langsung dari master data',
+ );
+ }
+ }
}
diff --git a/app/Services/Master/RawMaterialService.php b/app/Services/Master/RawMaterialService.php
index d2dd82e..049c316 100644
--- a/app/Services/Master/RawMaterialService.php
+++ b/app/Services/Master/RawMaterialService.php
@@ -12,6 +12,7 @@
use App\Models\User;
use App\Services\Concerns\CachesQuery;
use App\Services\Concerns\RunsInTransaction;
+use App\Services\Manage\StockMutationService;
use App\Services\Media\MediaService;
use App\Services\System\PushNotificationService;
use App\Support\Media\MediaPresenter;
@@ -28,6 +29,7 @@ class RawMaterialService
public function __construct(
private readonly MediaService $mediaService,
private readonly PushNotificationService $pushNotificationService,
+ private readonly StockMutationService $stockMutationService,
) {}
public function paginateForIndex(array $tableQuery, string $isActive, string $stockStatus = '', string $rawMaterialId = ''): LengthAwarePaginator
@@ -393,6 +395,19 @@ private function applyPayloadToRawMaterial(
foreach ($payload['prices'] ?? [] as $index => $priceData) {
if (! empty($priceData['id'])) {
$price = $rawMaterial->prices()->findOrFail($priceData['id']);
+ $oldStock = (float) $price->stock;
+
+ if ((float) $priceData['stock'] !== $oldStock) {
+ $this->stockMutationService->record(
+ stockable: $price,
+ type: 'adjustment',
+ quantity: (float) $priceData['stock'] - $oldStock,
+ stockBefore: $oldStock,
+ stockAfter: (float) $priceData['stock'],
+ description: 'Edit langsung dari master data',
+ );
+ }
+
$price->update([
'variant' => $priceData['variant'],
'price' => $priceData['price'],
@@ -414,6 +429,15 @@ private function applyPayloadToRawMaterial(
'stock' => $priceData['stock'],
]);
+ $this->stockMutationService->record(
+ stockable: $price,
+ type: 'adjustment',
+ quantity: (float) $priceData['stock'],
+ stockBefore: 0,
+ stockAfter: (float) $priceData['stock'],
+ description: 'Stok awal dari master data',
+ );
+
if ($verificationRequest !== null) {
$this->copyRequestPriceImages($verificationRequest, (int) $index, $price);
} elseif (isset($originalPrices[$index])) {
@@ -549,6 +573,15 @@ private function createPrice(RawMaterial $rawMaterial, array $priceData, int $in
'stock' => $priceData['stock'],
]);
+ $this->stockMutationService->record(
+ stockable: $price,
+ type: 'adjustment',
+ quantity: (float) $priceData['stock'],
+ stockBefore: 0,
+ stockAfter: (float) $priceData['stock'],
+ description: 'Stok awal dari master data',
+ );
+
$this->syncPriceImages($price, $priceData, $index);
return $price;
diff --git a/database/migrations/2026_07_27_000001_create_stock_mutations_table.php b/database/migrations/2026_07_27_000001_create_stock_mutations_table.php
new file mode 100644
index 0000000..62a89b3
--- /dev/null
+++ b/database/migrations/2026_07_27_000001_create_stock_mutations_table.php
@@ -0,0 +1,32 @@
+id();
+ $table->morphs('stockable');
+ $table->string('type');
+ $table->nullableMorphs('source');
+ $table->decimal('quantity', 18, 4);
+ $table->decimal('stock_before', 18, 4);
+ $table->decimal('stock_after', 18, 4);
+ $table->string('stock_quality')->nullable();
+ $table->string('description')->nullable();
+ $table->foreignId('user_id')->constrained();
+ $table->timestamps();
+
+ $table->index(['stockable_type', 'stockable_id', 'created_at']);
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('stock_mutations');
+ }
+};
diff --git a/resources/js/components/button/RowHistoryAction.vue b/resources/js/components/button/RowHistoryAction.vue
new file mode 100644
index 0000000..53ae1a7
--- /dev/null
+++ b/resources/js/components/button/RowHistoryAction.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+ {{ tooltip || 'Riwayat Stok' }}
+
+
diff --git a/resources/js/components/button/index.ts b/resources/js/components/button/index.ts
index 7548802..02032e5 100644
--- a/resources/js/components/button/index.ts
+++ b/resources/js/components/button/index.ts
@@ -10,5 +10,6 @@ export { default as RowPrintAction } from './RowPrintAction.vue';
export { default as RowDetailAction } from './RowDetailAction.vue';
export { default as RowTransferAction } from './RowTransferAction.vue';
export { default as RowShareAction } from './RowShareAction.vue';
+export { default as RowHistoryAction } from './RowHistoryAction.vue';
export { default as CreateButton } from './CreateButton.vue';
export { default as BackButton } from './BackButton.vue';
diff --git a/resources/js/pages/admin/manage/stock/History.vue b/resources/js/pages/admin/manage/stock/History.vue
new file mode 100644
index 0000000..b0bd149
--- /dev/null
+++ b/resources/js/pages/admin/manage/stock/History.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
Riwayat Stok
+
{{ title }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/pages/admin/master/products/table/ProductGroupedTable.vue b/resources/js/pages/admin/master/products/table/ProductGroupedTable.vue
index 6afb4cf..bbbac9e 100644
--- a/resources/js/pages/admin/master/products/table/ProductGroupedTable.vue
+++ b/resources/js/pages/admin/master/products/table/ProductGroupedTable.vue
@@ -1,6 +1,6 @@