refactor(inventory): 重構倉庫管理邏輯,移除 is_sellable 欄位並改由類型判定可用庫存
This commit is contained in:
@@ -26,9 +26,12 @@ class WarehouseController extends Controller
|
|||||||
|
|
||||||
$warehouses = $query->withSum('inventories as book_stock', 'quantity') // 帳面庫存 = 所有庫存總和
|
$warehouses = $query->withSum('inventories as book_stock', 'quantity') // 帳面庫存 = 所有庫存總和
|
||||||
->withSum(['inventories as available_stock' => function ($query) {
|
->withSum(['inventories as available_stock' => function ($query) {
|
||||||
// 可用庫存 = 庫存 > 0 且 品質正常 且 (未過期 或 無效期)
|
// 可用庫存 = 庫存 > 0 且 品質正常 且 (未過期 或 無效期) 且 倉庫類型不為瑕疵倉
|
||||||
$query->where('quantity', '>', 0)
|
$query->where('quantity', '>', 0)
|
||||||
->where('quality_status', 'normal')
|
->where('quality_status', 'normal')
|
||||||
|
->whereHas('warehouse', function ($q) {
|
||||||
|
$q->where('type', '!=', \App\Enums\WarehouseType::QUARANTINE);
|
||||||
|
})
|
||||||
->where(function ($q) {
|
->where(function ($q) {
|
||||||
$q->whereNull('expiry_date')
|
$q->whereNull('expiry_date')
|
||||||
->orWhere('expiry_date', '>=', now());
|
->orWhere('expiry_date', '>=', now());
|
||||||
@@ -38,20 +41,15 @@ class WarehouseController extends Controller
|
|||||||
->paginate(10)
|
->paginate(10)
|
||||||
->withQueryString();
|
->withQueryString();
|
||||||
|
|
||||||
// 修正各倉庫列表中的可用庫存計算:若倉庫不可銷售,則可用庫存為 0
|
// 移除原本對 is_sellable 的手動修正邏輯,現在由 type 自動過濾
|
||||||
$warehouses->getCollection()->transform(function ($w) {
|
|
||||||
if (!$w->is_sellable) {
|
|
||||||
$w->available_stock = 0;
|
|
||||||
}
|
|
||||||
return $w;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 計算全域總計 (不分頁)
|
// 計算全域總計 (不分頁)
|
||||||
$totals = [
|
$totals = [
|
||||||
'available_stock' => \App\Modules\Inventory\Models\Inventory::where('quantity', '>', 0)
|
'available_stock' => \App\Modules\Inventory\Models\Inventory::where('quantity', '>', 0)
|
||||||
->where('quality_status', 'normal')
|
->where('quality_status', 'normal')
|
||||||
->whereHas('warehouse', function ($q) {
|
->whereHas('warehouse', function ($q) {
|
||||||
$q->where('is_sellable', true);
|
$q->where('type', '!=', \App\Enums\WarehouseType::QUARANTINE);
|
||||||
})
|
})
|
||||||
->where(function ($q) {
|
->where(function ($q) {
|
||||||
$q->whereNull('expiry_date')
|
$q->whereNull('expiry_date')
|
||||||
@@ -73,7 +71,6 @@ class WarehouseController extends Controller
|
|||||||
'name' => 'required|string|max:50',
|
'name' => 'required|string|max:50',
|
||||||
'address' => 'nullable|string|max:255',
|
'address' => 'nullable|string|max:255',
|
||||||
'description' => 'nullable|string',
|
'description' => 'nullable|string',
|
||||||
'is_sellable' => 'nullable|boolean',
|
|
||||||
'type' => 'required|string',
|
'type' => 'required|string',
|
||||||
'license_plate' => 'nullable|string|max:20',
|
'license_plate' => 'nullable|string|max:20',
|
||||||
'driver_name' => 'nullable|string|max:50',
|
'driver_name' => 'nullable|string|max:50',
|
||||||
@@ -98,7 +95,6 @@ class WarehouseController extends Controller
|
|||||||
'name' => 'required|string|max:50',
|
'name' => 'required|string|max:50',
|
||||||
'address' => 'nullable|string|max:255',
|
'address' => 'nullable|string|max:255',
|
||||||
'description' => 'nullable|string',
|
'description' => 'nullable|string',
|
||||||
'is_sellable' => 'nullable|boolean',
|
|
||||||
'type' => 'required|string',
|
'type' => 'required|string',
|
||||||
'license_plate' => 'nullable|string|max:20',
|
'license_plate' => 'nullable|string|max:20',
|
||||||
'driver_name' => 'nullable|string|max:50',
|
'driver_name' => 'nullable|string|max:50',
|
||||||
|
|||||||
@@ -18,13 +18,11 @@ class Warehouse extends Model
|
|||||||
'type',
|
'type',
|
||||||
'address',
|
'address',
|
||||||
'description',
|
'description',
|
||||||
'is_sellable',
|
|
||||||
'license_plate',
|
'license_plate',
|
||||||
'driver_name',
|
'driver_name',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'is_sellable' => 'boolean',
|
|
||||||
'type' => \App\Enums\WarehouseType::class,
|
'type' => \App\Enums\WarehouseType::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('warehouses', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('is_sellable');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('warehouses', function (Blueprint $table) {
|
||||||
|
$table->boolean('is_sellable')->default(true)->after('description')->comment('是否可銷售');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -100,12 +100,18 @@ export default function WarehouseCard({
|
|||||||
|
|
||||||
{/* 統計區塊 - 狀態標籤 */}
|
{/* 統計區塊 - 狀態標籤 */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* 銷售狀態 */}
|
{/* 銷售狀態與可用性說明 */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm text-gray-500">銷售狀態</span>
|
<span className="text-sm text-gray-500">庫存可用性</span>
|
||||||
<Badge variant={warehouse.is_sellable ? "default" : "secondary"} className={warehouse.is_sellable ? "bg-green-600" : "bg-gray-400"}>
|
{warehouse.type === 'quarantine' ? (
|
||||||
{warehouse.is_sellable ? "可銷售" : "暫停銷售"}
|
<Badge variant="secondary" className="bg-red-100 text-red-700 border-red-200">
|
||||||
</Badge>
|
不計入可用
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="default" className="bg-green-600">
|
||||||
|
計入可用
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 低庫存警告狀態 */}
|
{/* 低庫存警告狀態 */}
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ export default function WarehouseDialog({
|
|||||||
address: string;
|
address: string;
|
||||||
description: string;
|
description: string;
|
||||||
type: WarehouseType;
|
type: WarehouseType;
|
||||||
is_sellable: boolean;
|
|
||||||
license_plate: string;
|
license_plate: string;
|
||||||
driver_name: string;
|
driver_name: string;
|
||||||
}>({
|
}>({
|
||||||
@@ -71,7 +70,6 @@ export default function WarehouseDialog({
|
|||||||
address: "",
|
address: "",
|
||||||
description: "",
|
description: "",
|
||||||
type: "standard",
|
type: "standard",
|
||||||
is_sellable: true,
|
|
||||||
license_plate: "",
|
license_plate: "",
|
||||||
driver_name: "",
|
driver_name: "",
|
||||||
});
|
});
|
||||||
@@ -86,7 +84,6 @@ export default function WarehouseDialog({
|
|||||||
address: warehouse.address || "",
|
address: warehouse.address || "",
|
||||||
description: warehouse.description || "",
|
description: warehouse.description || "",
|
||||||
type: warehouse.type || "standard",
|
type: warehouse.type || "standard",
|
||||||
is_sellable: warehouse.is_sellable ?? true,
|
|
||||||
license_plate: warehouse.license_plate || "",
|
license_plate: warehouse.license_plate || "",
|
||||||
driver_name: warehouse.driver_name || "",
|
driver_name: warehouse.driver_name || "",
|
||||||
});
|
});
|
||||||
@@ -97,7 +94,6 @@ export default function WarehouseDialog({
|
|||||||
address: "",
|
address: "",
|
||||||
description: "",
|
description: "",
|
||||||
type: "standard",
|
type: "standard",
|
||||||
is_sellable: true,
|
|
||||||
license_plate: "",
|
license_plate: "",
|
||||||
driver_name: "",
|
driver_name: "",
|
||||||
});
|
});
|
||||||
@@ -219,25 +215,7 @@ export default function WarehouseDialog({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 銷售設定 */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="border-b pb-2">
|
|
||||||
<h4 className="text-sm text-gray-700">銷售設定</h4>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="is_sellable"
|
|
||||||
className="h-4 w-4 rounded border-gray-300 text-primary-main focus:ring-primary-main"
|
|
||||||
checked={formData.is_sellable}
|
|
||||||
onChange={(e) => setFormData({ ...formData, is_sellable: e.target.checked })}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="is_sellable">此倉庫可進行銷售扣庫</Label>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-500 ml-6">
|
|
||||||
啟用後,該倉庫庫存可用於 POS 或訂單銷售扣減。總倉通常不啟用,門市與行動販賣車需啟用。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 區塊 B:位置 */}
|
{/* 區塊 B:位置 */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export interface Warehouse {
|
|||||||
total_quantity?: number;
|
total_quantity?: number;
|
||||||
low_stock_count?: number;
|
low_stock_count?: number;
|
||||||
type?: WarehouseType;
|
type?: WarehouseType;
|
||||||
is_sellable?: boolean;
|
|
||||||
license_plate?: string; // 車牌號碼 (移動倉)
|
license_plate?: string; // 車牌號碼 (移動倉)
|
||||||
driver_name?: string; // 司機姓名 (移動倉)
|
driver_name?: string; // 司機姓名 (移動倉)
|
||||||
book_stock?: number;
|
book_stock?: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user