[FEAT] 優化庫存分析邏輯,增加銷售 Reference Type 追蹤並修正 InventoryService 閉包變數問題
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m20s

This commit is contained in:
2026-03-10 11:15:55 +08:00
parent 197df3bec4
commit d52a215916
7 changed files with 57 additions and 8 deletions

View File

@@ -133,7 +133,10 @@ class SyncOrderAction
$warehouseId, $warehouseId,
$qty, $qty,
"POS Order: " . $order->external_order_id, "POS Order: " . $order->external_order_id,
true true,
null,
\App\Modules\Integration\Models\SalesOrder::class,
$order->id
); );
} }

View File

@@ -130,7 +130,10 @@ class SyncVendingOrderAction
$warehouseId, $warehouseId,
$qty, $qty,
"Vending Order: " . $order->external_order_id, "Vending Order: " . $order->external_order_id,
true true,
null,
\App\Modules\Integration\Models\SalesOrder::class,
$order->id
); );
} }

View File

@@ -23,7 +23,7 @@ interface InventoryServiceInterface
* @param string|null $slot * @param string|null $slot
* @return void * @return void
*/ */
public function decreaseStock(int $productId, int $warehouseId, float $quantity, ?string $reason = null, bool $force = false, ?string $slot = null): void; public function decreaseStock(int $productId, int $warehouseId, float $quantity, ?string $reason = null, bool $force = false, ?string $slot = null, ?string $referenceType = null, $referenceId = null): void;
/** /**
* Get all active warehouses. * Get all active warehouses.

View File

@@ -87,9 +87,9 @@ class InventoryService implements InventoryServiceInterface
return $stock >= $quantity; return $stock >= $quantity;
} }
public function decreaseStock(int $productId, int $warehouseId, float $quantity, ?string $reason = null, bool $force = false, ?string $slot = null): void public function decreaseStock(int $productId, int $warehouseId, float $quantity, ?string $reason = null, bool $force = false, ?string $slot = null, ?string $referenceType = null, $referenceId = null): void
{ {
DB::transaction(function () use ($productId, $warehouseId, $quantity, $reason, $force, $slot) { DB::transaction(function () use ($productId, $warehouseId, $quantity, $reason, $force, $slot, $referenceType, $referenceId) {
$query = Inventory::where('product_id', $productId) $query = Inventory::where('product_id', $productId)
->where('warehouse_id', $warehouseId) ->where('warehouse_id', $warehouseId)
->where('quantity', '>', 0); ->where('quantity', '>', 0);
@@ -108,7 +108,7 @@ class InventoryService implements InventoryServiceInterface
if ($remainingToDecrease <= 0) break; if ($remainingToDecrease <= 0) break;
$decreaseAmount = min($inventory->quantity, $remainingToDecrease); $decreaseAmount = min($inventory->quantity, $remainingToDecrease);
$this->decreaseInventoryQuantity($inventory->id, $decreaseAmount, $reason); $this->decreaseInventoryQuantity($inventory->id, $decreaseAmount, $reason, $referenceType, $referenceId);
$remainingToDecrease -= $decreaseAmount; $remainingToDecrease -= $decreaseAmount;
} }
@@ -139,7 +139,7 @@ class InventoryService implements InventoryServiceInterface
]); ]);
} }
$this->decreaseInventoryQuantity($inventory->id, $remainingToDecrease, $reason); $this->decreaseInventoryQuantity($inventory->id, $remainingToDecrease, $reason, $referenceType, $referenceId);
} else { } else {
throw new \Exception("庫存不足,無法扣除所有請求的數量。"); throw new \Exception("庫存不足,無法扣除所有請求的數量。");
} }

View File

@@ -69,6 +69,12 @@ class TurnoverService
->select('inventories.product_id', DB::raw('ABS(SUM(inventory_transactions.quantity)) as sales_qty_30d')) ->select('inventories.product_id', DB::raw('ABS(SUM(inventory_transactions.quantity)) as sales_qty_30d'))
->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id') ->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id')
->where('inventory_transactions.type', '出庫') // Adjust type as needed based on actual data ->where('inventory_transactions.type', '出庫') // Adjust type as needed based on actual data
->where(function ($q) {
$q->whereIn('inventory_transactions.reference_type', [
\App\Modules\Integration\Models\SalesOrder::class,
\App\Modules\Sales\Models\SalesImportBatch::class,
])->orWhereNull('inventory_transactions.reference_type');
})
->where('inventory_transactions.actual_time', '>=', $thirtyDaysAgo) ->where('inventory_transactions.actual_time', '>=', $thirtyDaysAgo)
->groupBy('inventories.product_id'); ->groupBy('inventories.product_id');
@@ -87,6 +93,12 @@ class TurnoverService
->select('inventories.product_id', DB::raw('MAX(actual_time) as last_sale_date')) ->select('inventories.product_id', DB::raw('MAX(actual_time) as last_sale_date'))
->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id') ->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id')
->where('inventory_transactions.type', '出庫') ->where('inventory_transactions.type', '出庫')
->where(function ($q) {
$q->whereIn('inventory_transactions.reference_type', [
\App\Modules\Integration\Models\SalesOrder::class,
\App\Modules\Sales\Models\SalesImportBatch::class,
])->orWhereNull('inventory_transactions.reference_type');
})
->groupBy('inventories.product_id'); ->groupBy('inventories.product_id');
if ($warehouseId) { if ($warehouseId) {
@@ -199,6 +211,12 @@ class TurnoverService
// Get IDs of products sold in last 90 days // Get IDs of products sold in last 90 days
$soldProductIds = InventoryTransaction::query() $soldProductIds = InventoryTransaction::query()
->where('type', '出庫') ->where('type', '出庫')
->where(function ($q) {
$q->whereIn('reference_type', [
\App\Modules\Integration\Models\SalesOrder::class,
\App\Modules\Sales\Models\SalesImportBatch::class,
])->orWhereNull('reference_type');
})
->where('actual_time', '>=', $ninetyDaysAgo) ->where('actual_time', '>=', $ninetyDaysAgo)
->distinct() ->distinct()
->pluck('inventory_id') // Wait, transaction links to inventory, inventory links to product. ->pluck('inventory_id') // Wait, transaction links to inventory, inventory links to product.
@@ -214,6 +232,12 @@ class TurnoverService
$soldProductIdsQuery = DB::table('inventory_transactions') $soldProductIdsQuery = DB::table('inventory_transactions')
->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id') ->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id')
->where('inventory_transactions.type', '出庫') ->where('inventory_transactions.type', '出庫')
->where(function ($q) {
$q->whereIn('inventory_transactions.reference_type', [
\App\Modules\Integration\Models\SalesOrder::class,
\App\Modules\Sales\Models\SalesImportBatch::class,
])->orWhereNull('inventory_transactions.reference_type');
})
->where('inventory_transactions.actual_time', '>=', $ninetyDaysAgo) ->where('inventory_transactions.actual_time', '>=', $ninetyDaysAgo)
->select('inventories.product_id') ->select('inventories.product_id')
->distinct(); ->distinct();
@@ -236,6 +260,12 @@ class TurnoverService
->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id') ->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id')
->join('products', 'inventories.product_id', '=', 'products.id') ->join('products', 'inventories.product_id', '=', 'products.id')
->where('inventory_transactions.type', '出庫') ->where('inventory_transactions.type', '出庫')
->where(function ($q) {
$q->whereIn('inventory_transactions.reference_type', [
\App\Modules\Integration\Models\SalesOrder::class,
\App\Modules\Sales\Models\SalesImportBatch::class,
])->orWhereNull('inventory_transactions.reference_type');
})
->where('inventory_transactions.actual_time', '>=', Carbon::now()->subDays($analysisDays)) ->where('inventory_transactions.actual_time', '>=', Carbon::now()->subDays($analysisDays))
->when($warehouseId, fn($q) => $q->where('inventories.warehouse_id', $warehouseId)) ->when($warehouseId, fn($q) => $q->where('inventories.warehouse_id', $warehouseId))
->when($categoryId, fn($q) => $q->where('products.category_id', $categoryId)) ->when($categoryId, fn($q) => $q->where('products.category_id', $categoryId))

View File

@@ -157,7 +157,9 @@ class SalesImportController extends Controller
$deduction['quantity'], $deduction['quantity'],
$reason, $reason,
true, // Force deduction true, // Force deduction
$deduction['slot'] // Location/Slot $deduction['slot'], // Location/Slot
\App\Modules\Sales\Models\SalesImportBatch::class,
$import->id
); );
} }

View File

@@ -175,6 +175,9 @@ class PosApiTest extends TestCase
'Accept' => 'application/json', 'Accept' => 'application/json',
])->postJson('/api/v1/integration/orders', $payload); ])->postJson('/api/v1/integration/orders', $payload);
$response->assertStatus(201)
->assertJsonPath('message', 'Order synced and stock deducted successfully');
$response->assertStatus(201) $response->assertStatus(201)
->assertJsonPath('message', 'Order synced and stock deducted successfully'); ->assertJsonPath('message', 'Order synced and stock deducted successfully');
@@ -196,6 +199,14 @@ class PosApiTest extends TestCase
'warehouse_id' => $warehouseId, 'warehouse_id' => $warehouseId,
'quantity' => 95, 'quantity' => 95,
]); ]);
$order = \App\Modules\Integration\Models\SalesOrder::where('external_order_id', 'ORD-001')->first();
$this->assertDatabaseHas('inventory_transactions', [
'reference_type' => \App\Modules\Integration\Models\SalesOrder::class,
'reference_id' => $order->id,
'quantity' => -5,
'type' => '出庫',
]);
tenancy()->end(); tenancy()->end();
} }