[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,
$qty,
"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,
$qty,
"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
* @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.

View File

@@ -87,9 +87,9 @@ class InventoryService implements InventoryServiceInterface
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)
->where('warehouse_id', $warehouseId)
->where('quantity', '>', 0);
@@ -108,7 +108,7 @@ class InventoryService implements InventoryServiceInterface
if ($remainingToDecrease <= 0) break;
$decreaseAmount = min($inventory->quantity, $remainingToDecrease);
$this->decreaseInventoryQuantity($inventory->id, $decreaseAmount, $reason);
$this->decreaseInventoryQuantity($inventory->id, $decreaseAmount, $reason, $referenceType, $referenceId);
$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 {
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'))
->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id')
->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)
->groupBy('inventories.product_id');
@@ -87,6 +93,12 @@ class TurnoverService
->select('inventories.product_id', DB::raw('MAX(actual_time) as last_sale_date'))
->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id')
->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');
if ($warehouseId) {
@@ -199,6 +211,12 @@ class TurnoverService
// Get IDs of products sold in last 90 days
$soldProductIds = InventoryTransaction::query()
->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)
->distinct()
->pluck('inventory_id') // Wait, transaction links to inventory, inventory links to product.
@@ -214,6 +232,12 @@ class TurnoverService
$soldProductIdsQuery = DB::table('inventory_transactions')
->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id')
->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)
->select('inventories.product_id')
->distinct();
@@ -236,6 +260,12 @@ class TurnoverService
->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id')
->join('products', 'inventories.product_id', '=', 'products.id')
->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))
->when($warehouseId, fn($q) => $q->where('inventories.warehouse_id', $warehouseId))
->when($categoryId, fn($q) => $q->where('products.category_id', $categoryId))

View File

@@ -157,7 +157,9 @@ class SalesImportController extends Controller
$deduction['quantity'],
$reason,
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',
])->postJson('/api/v1/integration/orders', $payload);
$response->assertStatus(201)
->assertJsonPath('message', 'Order synced and stock deducted successfully');
$response->assertStatus(201)
->assertJsonPath('message', 'Order synced and stock deducted successfully');
@@ -197,6 +200,14 @@ class PosApiTest extends TestCase
'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();
}
}