From 906b094c18a2e2404eec72ea85812e700ca04f79 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Fri, 6 Feb 2026 09:26:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20[=E5=95=86=E5=93=81=E7=AE=A1=E7=90=86]?= =?UTF-8?q?=20=E5=84=AA=E5=8C=96=E5=95=86=E5=93=81=E5=8C=AF=E5=85=A5?= =?UTF-8?q?=E9=82=8F=E8=BC=AF=EF=BC=8C=E6=94=AF=E6=8F=B4=2013=20=E7=A2=BC?= =?UTF-8?q?=E6=A2=9D=E7=A2=BC=E8=87=AA=E5=8B=95=E7=94=9F=E6=88=90=E3=80=81?= =?UTF-8?q?Upsert=20=E6=9B=B4=E6=96=B0=E6=A9=9F=E5=88=B6=E8=88=87=20Excel?= =?UTF-8?q?=20=E8=AA=AA=E6=98=8E=E5=B7=A5=E4=BD=9C=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/ProductController.php | 25 ++++++++ .../Inventory/Exports/InstructionSheet.php | 54 +++++++++++++++++ .../Inventory/Exports/ProductImportSheet.php | 43 +++++++++++++ .../Exports/ProductTemplateExport.php | 34 ++--------- .../Inventory/Imports/ProductImport.php | 60 +++++++++++++++---- .../js/Components/Product/ProductForm.tsx | 9 ++- .../Product/ProductImportDialog.tsx | 6 +- 7 files changed, 187 insertions(+), 44 deletions(-) create mode 100644 app/Modules/Inventory/Exports/InstructionSheet.php create mode 100644 app/Modules/Inventory/Exports/ProductImportSheet.php diff --git a/app/Modules/Inventory/Controllers/ProductController.php b/app/Modules/Inventory/Controllers/ProductController.php index 22a7319..a1d9a30 100644 --- a/app/Modules/Inventory/Controllers/ProductController.php +++ b/app/Modules/Inventory/Controllers/ProductController.php @@ -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; + } } diff --git a/app/Modules/Inventory/Exports/InstructionSheet.php b/app/Modules/Inventory/Exports/InstructionSheet.php new file mode 100644 index 0000000..d37ae65 --- /dev/null +++ b/app/Modules/Inventory/Exports/InstructionSheet.php @@ -0,0 +1,54 @@ + ['font' => ['bold' => true]], + // 欄位寬度自動 + ]; + } +} diff --git a/app/Modules/Inventory/Exports/ProductImportSheet.php b/app/Modules/Inventory/Exports/ProductImportSheet.php new file mode 100644 index 0000000..4b0570e --- /dev/null +++ b/app/Modules/Inventory/Exports/ProductImportSheet.php @@ -0,0 +1,43 @@ + NumberFormat::FORMAT_TEXT, // 商品代號 + 'B' => NumberFormat::FORMAT_TEXT, // 條碼 + ]; + } +} diff --git a/app/Modules/Inventory/Exports/ProductTemplateExport.php b/app/Modules/Inventory/Exports/ProductTemplateExport.php index 6d04e40..5321e74 100644 --- a/app/Modules/Inventory/Exports/ProductTemplateExport.php +++ b/app/Modules/Inventory/Exports/ProductTemplateExport.php @@ -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(), ]; } } diff --git a/app/Modules/Inventory/Imports/ProductImport.php b/app/Modules/Inventory/Imports/ProductImport.php index 9836d32..4dafbaf 100644 --- a/app/Modules/Inventory/Imports/ProductImport.php +++ b/app/Modules/Inventory/Imports/ProductImport.php @@ -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])) { diff --git a/resources/js/Components/Product/ProductForm.tsx b/resources/js/Components/Product/ProductForm.tsx index 816198a..856dc2a 100644 --- a/resources/js/Components/Product/ProductForm.tsx +++ b/resources/js/Components/Product/ProductForm.tsx @@ -62,8 +62,11 @@ export default function ProductForm({ }; const generateRandomBarcode = () => { - const randomDigits = Math.floor(Math.random() * 9000000000) + 1000000000; - setData("barcode", randomDigits.toString()); + let result = ""; + for (let i = 0; i < 13; i++) { + result += Math.floor(Math.random() * 10).toString(); + } + setData("barcode", result); }; const generateRandomCode = () => { @@ -150,7 +153,7 @@ export default function ProductForm({
    -
  • 必填欄位:商品名稱、類別名稱、基本單位、條碼。
  • +
  • 必填欄位:商品名稱、類別名稱、基本單位。
  • 商品代號:2-8 碼,非必填(未填將自動生成,大寫英文+數字 8 碼)。
  • -
  • 唯一性:商品代號(若有填寫)與條碼不可與現有資料重複。
  • +
  • 條碼:13 碼數字,非必填(未填將自動生成)。
  • +
  • 補充說明:詳細規則亦可於 Excel 範本的「填寫說明」工作表中查看。
  • +
  • 更新機制:若系統中已有相同「條碼」「商品代號」,系統將自動更新該筆資料,不會重複建立。
  • 自動關聯:類別與單位請填寫系統當前存在的「名稱」(如:飲品、瓶)。
  • 大單位:若填寫大單位,則「換算率」為必填(需大於 0)。