feat(inventory): 實作過期與瑕疵庫存總計顯示,並強化庫存明細過期提示
This commit is contained in:
@@ -113,57 +113,77 @@ class ProductController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 顯示建立表單。
|
||||
*/
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('Product/Create', [
|
||||
'categories' => Category::where('is_active', true)->get()->map(fn($c) => (object)['id' => $c->id, 'name' => $c->name]),
|
||||
'units' => Unit::all()->map(fn($u) => (object)['id' => (string) $u->id, 'name' => $u->name, 'code' => $u->code]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 將新建立的資源儲存到儲存體中。
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'code' => 'required|string|min:2|max:8|unique:products,code',
|
||||
'barcode' => 'required|string|unique:products,barcode',
|
||||
'code' => 'nullable|unique:products,code',
|
||||
'barcode' => 'nullable|unique:products,barcode',
|
||||
'name' => 'required|string|max:255',
|
||||
'category_id' => 'required|exists:categories,id',
|
||||
'brand' => 'nullable|string|max:255',
|
||||
'specification' => 'nullable|string',
|
||||
|
||||
'base_unit_id' => 'required|exists:units,id',
|
||||
'large_unit_id' => 'nullable|exists:units,id',
|
||||
'conversion_rate' => 'required_with:large_unit_id|nullable|numeric|min:0.0001',
|
||||
'purchase_unit_id' => 'nullable|exists:units,id',
|
||||
'purchase_unit_id' => 'nullable|exists:units,id',
|
||||
'conversion_rate' => 'nullable|numeric|min:0',
|
||||
'location' => 'nullable|string|max:255',
|
||||
'cost_price' => 'nullable|numeric|min:0',
|
||||
'price' => 'nullable|numeric|min:0',
|
||||
'member_price' => 'nullable|numeric|min:0',
|
||||
'wholesale_price' => 'nullable|numeric|min:0',
|
||||
], [
|
||||
'code.required' => '商品代號為必填',
|
||||
'code.max' => '商品代號最多 8 碼',
|
||||
'code.min' => '商品代號最少 2 碼',
|
||||
'code.unique' => '商品代號已存在',
|
||||
'barcode.required' => '條碼編號為必填',
|
||||
'barcode.unique' => '條碼編號已存在',
|
||||
'name.required' => '商品名稱為必填',
|
||||
'category_id.required' => '請選擇分類',
|
||||
'category_id.exists' => '所選分類不存在',
|
||||
'base_unit_id.required' => '基本庫存單位為必填',
|
||||
'base_unit_id.exists' => '所選基本單位不存在',
|
||||
'conversion_rate.required_with' => '填寫大單位時,換算率為必填',
|
||||
'conversion_rate.numeric' => '換算率必須為數字',
|
||||
'conversion_rate.min' => '換算率最小為 0.0001',
|
||||
'cost_price.numeric' => '成本價必須為數字',
|
||||
'cost_price.min' => '成本價不能小於 0',
|
||||
'price.numeric' => '售價必須為數字',
|
||||
'price.min' => '售價不能小於 0',
|
||||
'member_price.numeric' => '會員價必須為數字',
|
||||
'member_price.min' => '會員價不能小於 0',
|
||||
'wholesale_price.numeric' => '批發價必須為數字',
|
||||
'wholesale_price.min' => '批發價不能小於 0',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
|
||||
if (empty($validated['code'])) {
|
||||
$validated['code'] = $this->generateRandomCode();
|
||||
}
|
||||
|
||||
$product = Product::create($validated);
|
||||
|
||||
return redirect()->back()->with('success', '商品已建立');
|
||||
return redirect()->route('products.index')->with('success', '商品已建立');
|
||||
}
|
||||
|
||||
/**
|
||||
* 顯示編輯表單。
|
||||
*/
|
||||
public function edit(Product $product): Response
|
||||
{
|
||||
return Inertia::render('Product/Edit', [
|
||||
'product' => (object) [
|
||||
'id' => (string) $product->id,
|
||||
'code' => $product->code,
|
||||
'barcode' => $product->barcode,
|
||||
'name' => $product->name,
|
||||
'categoryId' => $product->category_id,
|
||||
'brand' => $product->brand,
|
||||
'specification' => $product->specification,
|
||||
'baseUnitId' => $product->base_unit_id,
|
||||
'largeUnitId' => $product->large_unit_id,
|
||||
'conversionRate' => (float) $product->conversion_rate,
|
||||
'purchaseUnitId' => $product->purchase_unit_id,
|
||||
'location' => $product->location,
|
||||
'cost_price' => (float) $product->cost_price,
|
||||
'price' => (float) $product->price,
|
||||
'member_price' => (float) $product->member_price,
|
||||
'wholesale_price' => (float) $product->wholesale_price,
|
||||
],
|
||||
'categories' => Category::where('is_active', true)->get()->map(fn($c) => (object)['id' => $c->id, 'name' => $c->name]),
|
||||
'units' => Unit::all()->map(fn($u) => (object)['id' => (string) $u->id, 'name' => $u->name, 'code' => $u->code]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,50 +192,31 @@ class ProductController extends Controller
|
||||
public function update(Request $request, Product $product)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'code' => 'required|string|min:2|max:8|unique:products,code,' . $product->id,
|
||||
'barcode' => 'required|string|unique:products,barcode,' . $product->id,
|
||||
'code' => 'nullable|unique:products,code,' . $product->id,
|
||||
'barcode' => 'nullable|unique:products,barcode,' . $product->id,
|
||||
'name' => 'required|string|max:255',
|
||||
'category_id' => 'required|exists:categories,id',
|
||||
'brand' => 'nullable|string|max:255',
|
||||
'specification' => 'nullable|string',
|
||||
'base_unit_id' => 'required|exists:units,id',
|
||||
'large_unit_id' => 'nullable|exists:units,id',
|
||||
'conversion_rate' => 'required_with:large_unit_id|nullable|numeric|min:0.0001',
|
||||
'purchase_unit_id' => 'nullable|exists:units,id',
|
||||
'purchase_unit_id' => 'nullable|exists:units,id',
|
||||
'conversion_rate' => 'nullable|numeric|min:0',
|
||||
'location' => 'nullable|string|max:255',
|
||||
'cost_price' => 'nullable|numeric|min:0',
|
||||
'price' => 'nullable|numeric|min:0',
|
||||
'member_price' => 'nullable|numeric|min:0',
|
||||
'wholesale_price' => 'nullable|numeric|min:0',
|
||||
], [
|
||||
'code.required' => '商品代號為必填',
|
||||
'code.max' => '商品代號最多 8 碼',
|
||||
'code.min' => '商品代號最少 2 碼',
|
||||
'code.unique' => '商品代號已存在',
|
||||
'barcode.required' => '條碼編號為必填',
|
||||
'barcode.unique' => '條碼編號已存在',
|
||||
'name.required' => '商品名稱為必填',
|
||||
'category_id.required' => '請選擇分類',
|
||||
'category_id.exists' => '所選分類不存在',
|
||||
'base_unit_id.required' => '基本庫存單位為必填',
|
||||
'base_unit_id.exists' => '所選基本單位不存在',
|
||||
'conversion_rate.required_with' => '填寫大單位時,換算率為必填',
|
||||
'conversion_rate.numeric' => '換算率必須為數字',
|
||||
'conversion_rate.min' => '換算率最小為 0.0001',
|
||||
'cost_price.numeric' => '成本價必須為數字',
|
||||
'cost_price.min' => '成本價不能小於 0',
|
||||
'price.numeric' => '售價必須為數字',
|
||||
'price.min' => '售價不能小於 0',
|
||||
'member_price.numeric' => '會員價必須為數字',
|
||||
'member_price.min' => '會員價不能小於 0',
|
||||
'wholesale_price.numeric' => '批發價必須為數字',
|
||||
'wholesale_price.min' => '批發價不能小於 0',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
if (empty($validated['code'])) {
|
||||
$validated['code'] = $this->generateRandomCode();
|
||||
}
|
||||
|
||||
$product->update($validated);
|
||||
|
||||
return redirect()->back()->with('success', '商品已更新');
|
||||
return redirect()->route('products.index')->with('success', '商品已更新');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,4 +260,22 @@ class ProductController extends Controller
|
||||
return redirect()->back()->withErrors(['file' => '匯入失敗: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成隨機 8 碼代號 (大寫英文+數字)
|
||||
*/
|
||||
private function generateRandomCode(): string
|
||||
{
|
||||
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
$code = '';
|
||||
|
||||
do {
|
||||
$code = '';
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
$code .= $characters[rand(0, strlen($characters) - 1)];
|
||||
}
|
||||
} while (Product::where('code', $code)->exists());
|
||||
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,19 @@ class WarehouseController extends Controller
|
||||
->orWhere('expiry_date', '>=', now());
|
||||
});
|
||||
}], 'total_value')
|
||||
->withSum(['inventories as abnormal_amount' => function ($query) {
|
||||
$query->where('quantity', '>', 0)
|
||||
->where(function ($q) {
|
||||
$q->where('quality_status', '!=', 'normal')
|
||||
->orWhere(function ($sq) {
|
||||
$sq->whereNotNull('expiry_date')
|
||||
->where('expiry_date', '<', now());
|
||||
})
|
||||
->orWhereHas('warehouse', function ($wq) {
|
||||
$wq->where('type', \App\Enums\WarehouseType::QUARANTINE);
|
||||
});
|
||||
});
|
||||
}], 'total_value')
|
||||
->addSelect(['low_stock_count' => function ($query) {
|
||||
$query->selectRaw('count(*)')
|
||||
->from('warehouse_product_safety_stocks as ss')
|
||||
@@ -85,6 +98,17 @@ class WarehouseController extends Controller
|
||||
$q->whereNull('expiry_date')
|
||||
->orWhere('expiry_date', '>=', now());
|
||||
})->sum('total_value'),
|
||||
'abnormal_amount' => \App\Modules\Inventory\Models\Inventory::where('quantity', '>', 0)
|
||||
->where(function ($q) {
|
||||
$q->where('quality_status', '!=', 'normal')
|
||||
->orWhere(function ($sq) {
|
||||
$sq->whereNotNull('expiry_date')
|
||||
->where('expiry_date', '<', now());
|
||||
})
|
||||
->orWhereHas('warehouse', function ($wq) {
|
||||
$wq->where('type', \App\Enums\WarehouseType::QUARANTINE);
|
||||
});
|
||||
})->sum('total_value'),
|
||||
'book_stock' => \App\Modules\Inventory\Models\Inventory::sum('quantity'),
|
||||
'book_amount' => \App\Modules\Inventory\Models\Inventory::sum('total_value'),
|
||||
];
|
||||
|
||||
@@ -63,8 +63,14 @@ class ProductImport implements ToModel, WithHeadingRow, WithValidation, WithMapp
|
||||
return null;
|
||||
}
|
||||
|
||||
// 處理商品代號:若為空則自動生成
|
||||
$code = $row['商品代號'] ?? null;
|
||||
if (empty($code)) {
|
||||
$code = $this->generateRandomCode();
|
||||
}
|
||||
|
||||
return new Product([
|
||||
'code' => $row['商品代號'],
|
||||
'code' => $code,
|
||||
'barcode' => $row['條碼'],
|
||||
'name' => $row['商品名稱'],
|
||||
'category_id' => $categoryId,
|
||||
@@ -81,10 +87,28 @@ class ProductImport implements ToModel, WithHeadingRow, WithValidation, WithMapp
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成隨機 8 碼代號 (大寫英文+數字)
|
||||
*/
|
||||
private function generateRandomCode(): string
|
||||
{
|
||||
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
$code = '';
|
||||
|
||||
do {
|
||||
$code = '';
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
$code .= $characters[rand(0, strlen($characters) - 1)];
|
||||
}
|
||||
} while (Product::where('code', $code)->exists());
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'商品代號' => ['required', 'string', 'min:2', 'max:8', 'unique:products,code'],
|
||||
'商品代號' => ['nullable', 'string', 'min:2', 'max:8', 'unique:products,code'],
|
||||
'條碼' => ['required', 'string', 'unique:products,barcode'],
|
||||
'商品名稱' => ['required', 'string'],
|
||||
'類別名稱' => ['required', function($attribute, $value, $fail) {
|
||||
|
||||
@@ -33,6 +33,8 @@ Route::middleware('auth')->group(function () {
|
||||
Route::get('/products/template', [ProductController::class, 'template'])->name('products.template');
|
||||
Route::post('/products/import', [ProductController::class, 'import'])->middleware('permission:products.create')->name('products.import');
|
||||
Route::get('/products', [ProductController::class, 'index'])->name('products.index');
|
||||
Route::get('/products/create', [ProductController::class, 'create'])->middleware('permission:products.create')->name('products.create');
|
||||
Route::get('/products/{product}/edit', [ProductController::class, 'edit'])->middleware('permission:products.edit')->name('products.edit');
|
||||
Route::post('/products', [ProductController::class, 'store'])->middleware('permission:products.create')->name('products.store');
|
||||
Route::put('/products/{product}', [ProductController::class, 'update'])->middleware('permission:products.edit')->name('products.update');
|
||||
Route::delete('/products/{product}', [ProductController::class, 'destroy'])->middleware('permission:products.delete')->name('products.destroy');
|
||||
|
||||
Reference in New Issue
Block a user