feat: [商品管理] 優化商品匯入邏輯,支援 13 碼條碼自動生成、Upsert 更新機制與 Excel 說明工作表
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m7s

This commit is contained in:
2026-02-06 09:26:50 +08:00
parent e1aa452b3c
commit 906b094c18
7 changed files with 187 additions and 44 deletions

View File

@@ -197,6 +197,10 @@ class ProductController extends Controller
$validated['code'] = $this->generateRandomCode();
}
if (empty($validated['barcode'])) {
$validated['barcode'] = $this->generateRandomBarcode();
}
$product = Product::create($validated);
return redirect()->route('products.index')->with('success', '商品已建立');
@@ -260,6 +264,10 @@ class ProductController extends Controller
$validated['code'] = $this->generateRandomCode();
}
if (empty($validated['barcode'])) {
$validated['barcode'] = $this->generateRandomBarcode();
}
$product->update($validated);
if ($request->input('from') === 'show') {
@@ -328,4 +336,21 @@ class ProductController extends Controller
return $code;
}
/**
* 生成隨機 13 碼條碼 (純數字)
*/
private function generateRandomBarcode(): string
{
$barcode = '';
do {
$barcode = '';
for ($i = 0; $i < 13; $i++) {
$barcode .= rand(0, 9);
}
} while (Product::where('barcode', $barcode)->exists());
return $barcode;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Modules\Inventory\Exports;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithTitle;
use Maatwebsite\Excel\Concerns\WithStyles;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class InstructionSheet implements FromCollection, WithHeadings, WithTitle, WithStyles
{
public function title(): string
{
return '填寫說明';
}
public function headings(): array
{
return [
'欄位名稱',
'是否必填',
'填寫說明',
];
}
public function collection()
{
return collect([
['商品代號', '選填', '2-8 碼,若未填寫系統將自動生成。若代號已存在,將更新該商品資料。'],
['條碼', '選填', '13 碼數字,若未填寫系統將自動生成。若條碼已存在(優先比對),將更新該商品資料。'],
['商品名稱', '必填', '請填寫完整商品名稱。'],
['類別名稱', '必填', '必須為系統中已存在的類別名稱(如:飲品)。'],
['品牌', '選填', '商品品牌名稱。'],
['規格', '選填', '商品規格描述25kg/袋)。'],
['基本單位', '必填', '必須為系統中已存在的單位名稱(如:瓶、個)。'],
['大單位', '選填', '若有大單位換算請填寫(如:箱)。'],
['換算率', '若有大單位則必填', '1 個大單位等於多少個基本單位。'],
['成本價', '選填', '數字,預設為 0。'],
['售價', '選填', '數字,預設為 0。'],
['會員價', '選填', '數字,預設為 0。'],
['批發價', '選填', '數字,預設為 0。'],
]);
}
public function styles(Worksheet $sheet)
{
return [
// 第一行標題粗體
1 => ['font' => ['bold' => true]],
// 欄位寬度自動
];
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Modules\Inventory\Exports;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
use Maatwebsite\Excel\Concerns\WithTitle;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
class ProductImportSheet implements WithHeadings, WithColumnFormatting, WithTitle
{
public function title(): string
{
return '商品匯入';
}
public function headings(): array
{
return [
'商品代號(選填)',
'條碼(選填)',
'商品名稱',
'類別名稱',
'品牌',
'規格',
'基本單位',
'大單位',
'換算率',
'成本價',
'售價',
'會員價',
'批發價',
];
}
public function columnFormats(): array
{
return [
'A' => NumberFormat::FORMAT_TEXT, // 商品代號
'B' => NumberFormat::FORMAT_TEXT, // 條碼
];
}
}

View File

@@ -2,39 +2,15 @@
namespace App\Modules\Inventory\Exports;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
// use Maatwebsite\Excel\Concerns\WithHeadings;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
class ProductTemplateExport implements WithHeadings, WithColumnFormatting
class ProductTemplateExport implements WithMultipleSheets
{
public function headings(): array
public function sheets(): array
{
return [
'商品代號',
'條碼',
'商品名稱',
'類別名稱',
'品牌',
'規格',
'基本單位',
'大單位',
'換算率',
'成本價',
'售價',
'會員價',
'批發價',
];
}
public function columnFormats(): array
{
return [
'A' => NumberFormat::FORMAT_TEXT, // 商品代號
'B' => NumberFormat::FORMAT_TEXT, // 條碼
new ProductImportSheet(),
new InstructionSheet(),
];
}
}

View File

@@ -57,21 +57,25 @@ class ProductImport implements ToModel, WithHeadingRow, WithValidation, WithMapp
$baseUnitId = $this->units[$row['基本單位']] ?? null;
$largeUnitId = isset($row['大單位']) ? ($this->units[$row['大單位']] ?? null) : null;
// 若必要關聯找不到,理論上 Validation 會攔截,但此處做防禦性編程
if (!$categoryId || !$baseUnitId) {
return null;
}
// 處理商品代號:若為空則自動生成
$code = $row['商品代號'] ?? null;
if (empty($code)) {
$code = $this->generateRandomCode();
$barcode = $row['條碼'] ?? null;
// Upsert 邏輯:優先以條碼查找,次之以商品代號查找
$product = null;
if (!empty($barcode)) {
$product = Product::where('barcode', $barcode)->first();
}
return new Product([
'code' => $code,
'barcode' => $row['條碼'],
if (!$product && !empty($code)) {
$product = Product::where('code', $code)->first();
}
$data = [
'name' => $row['商品名稱'],
'category_id' => $categoryId,
'brand' => $row['品牌'] ?? null,
@@ -84,7 +88,26 @@ class ProductImport implements ToModel, WithHeadingRow, WithValidation, WithMapp
'price' => $row['售價'] ?? null,
'member_price' => $row['會員價'] ?? null,
'wholesale_price' => $row['批發價'] ?? null,
]);
];
if ($product) {
// 更新現有商品
$product->update($data);
return null; // 返回 null 以避免 Maatwebsite/Excel 嘗試再次 insert
}
// 建立新商品:處理代碼與條碼自動生成
if (empty($code)) {
$code = $this->generateRandomCode();
}
if (empty($barcode)) {
$barcode = $this->generateRandomBarcode();
}
$data['code'] = $code;
$data['barcode'] = $barcode;
return new Product($data);
}
/**
@@ -105,11 +128,28 @@ class ProductImport implements ToModel, WithHeadingRow, WithValidation, WithMapp
return $code;
}
/**
* 生成隨機 13 碼條碼 (純數字)
*/
private function generateRandomBarcode(): string
{
$barcode = '';
do {
$barcode = '';
for ($i = 0; $i < 13; $i++) {
$barcode .= rand(0, 9);
}
} while (Product::where('barcode', $barcode)->exists());
return $barcode;
}
public function rules(): array
{
return [
'商品代號' => ['nullable', 'string', 'min:2', 'max:8', 'unique:products,code'],
'條碼' => ['required', 'string', 'unique:products,barcode'],
'商品代號' => ['nullable', 'string', 'min:2', 'max:8'],
'條碼' => ['nullable', 'string'],
'商品名稱' => ['required', 'string'],
'類別名稱' => ['required', function($attribute, $value, $fail) {
if (!isset($this->categories[$value])) {