實作產品與庫存匯入邏輯 (ProductImport, InventoryImport) 並更新相關 Service 與 Controller
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 56s
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 56s
This commit is contained in:
@@ -38,4 +38,44 @@ interface ProductServiceInterface
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function findByCodes(array $codes);
|
||||
|
||||
/**
|
||||
* 建立新商品。
|
||||
*
|
||||
* @param array $data
|
||||
* @return \App\Modules\Inventory\Models\Product
|
||||
*/
|
||||
public function createProduct(array $data);
|
||||
|
||||
/**
|
||||
* 更新現有商品。
|
||||
*
|
||||
* @param \App\Modules\Inventory\Models\Product $product
|
||||
* @param array $data
|
||||
* @return \App\Modules\Inventory\Models\Product
|
||||
*/
|
||||
public function updateProduct(\App\Modules\Inventory\Models\Product $product, array $data);
|
||||
|
||||
/**
|
||||
* 生成隨機 8 碼代號 (大寫英文+數字)。
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generateRandomCode();
|
||||
|
||||
/**
|
||||
* 生成隨機 13 碼條碼 (純數字)。
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generateRandomBarcode();
|
||||
|
||||
/**
|
||||
* 根據條碼或代號查找商品。
|
||||
*
|
||||
* @param string|null $barcode
|
||||
* @param string|null $code
|
||||
* @return \App\Modules\Inventory\Models\Product|null
|
||||
*/
|
||||
public function findByBarcodeOrCode(?string $barcode, ?string $code);
|
||||
}
|
||||
|
||||
@@ -186,8 +186,11 @@ class InventoryController extends Controller
|
||||
]);
|
||||
|
||||
return DB::transaction(function () use ($validated, $warehouse) {
|
||||
// 修正時間精度:手動入庫亦補上當下時分秒
|
||||
$inboundDateTime = $validated['inboundDate'] . ' ' . date('H:i:s');
|
||||
|
||||
$this->inventoryService->processIncomingInventory($warehouse, $validated['items'], [
|
||||
'inboundDate' => $validated['inboundDate'],
|
||||
'inboundDate' => $inboundDateTime,
|
||||
'reason' => $validated['reason'],
|
||||
'notes' => $validated['notes'] ?? '',
|
||||
]);
|
||||
|
||||
@@ -16,6 +16,12 @@ use App\Modules\Inventory\Imports\ProductImport;
|
||||
|
||||
class ProductController extends Controller
|
||||
{
|
||||
protected $productService;
|
||||
|
||||
public function __construct(\App\Modules\Inventory\Contracts\ProductServiceInterface $productService)
|
||||
{
|
||||
$this->productService = $productService;
|
||||
}
|
||||
/**
|
||||
* 顯示資源列表。
|
||||
*/
|
||||
@@ -195,15 +201,7 @@ class ProductController extends Controller
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
if (empty($validated['code'])) {
|
||||
$validated['code'] = $this->generateRandomCode();
|
||||
}
|
||||
|
||||
if (empty($validated['barcode'])) {
|
||||
$validated['barcode'] = $this->generateRandomBarcode();
|
||||
}
|
||||
|
||||
$product = Product::create($validated);
|
||||
$product = $this->productService->createProduct($validated);
|
||||
|
||||
return redirect()->route('products.index')->with('success', '商品已建立');
|
||||
}
|
||||
@@ -262,15 +260,7 @@ class ProductController extends Controller
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
if (empty($validated['code'])) {
|
||||
$validated['code'] = $this->generateRandomCode();
|
||||
}
|
||||
|
||||
if (empty($validated['barcode'])) {
|
||||
$validated['barcode'] = $this->generateRandomBarcode();
|
||||
}
|
||||
|
||||
$product->update($validated);
|
||||
$this->productService->updateProduct($product, $validated);
|
||||
|
||||
if ($request->input('from') === 'show') {
|
||||
return redirect()->route('products.show', $product->id)->with('success', '商品已更新');
|
||||
@@ -320,39 +310,4 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成隨機 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,12 @@ class InventoryImport implements ToModel, WithHeadingRow, WithValidation, WithMa
|
||||
{
|
||||
HeadingRowFormatter::default('none');
|
||||
$this->warehouse = $warehouse;
|
||||
$this->inboundDate = $inboundDate;
|
||||
|
||||
// 修正時間精度:將選定的日期與「現在的時分秒」結合
|
||||
// 這樣既能保留使用者選的日期,又能提供精確的紀錄時點排順序
|
||||
$currentTime = date('H:i:s');
|
||||
$this->inboundDate = $inboundDate . ' ' . $currentTime;
|
||||
|
||||
$this->notes = $notes;
|
||||
}
|
||||
|
||||
@@ -95,7 +100,7 @@ class InventoryImport implements ToModel, WithHeadingRow, WithValidation, WithMa
|
||||
// 更新單價與總價值
|
||||
$inventory->unit_cost = $unitCost;
|
||||
$inventory->total_value = $inventory->quantity * $unitCost;
|
||||
$inventory->save();
|
||||
$inventory->saveQuietly();
|
||||
|
||||
// 記錄交易歷史
|
||||
$inventory->transactions()->create([
|
||||
|
||||
@@ -16,6 +16,7 @@ class ProductImport implements ToModel, WithHeadingRow, WithValidation, WithMapp
|
||||
{
|
||||
private $categories;
|
||||
private $units;
|
||||
private $productService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -25,6 +26,7 @@ class ProductImport implements ToModel, WithHeadingRow, WithValidation, WithMapp
|
||||
// 快取所有類別與單位,避免 N+1 查詢
|
||||
$this->categories = Category::pluck('id', 'name');
|
||||
$this->units = Unit::pluck('id', 'name');
|
||||
$this->productService = app(\App\Modules\Inventory\Contracts\ProductServiceInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,15 +67,8 @@ class ProductImport implements ToModel, WithHeadingRow, WithValidation, WithMapp
|
||||
$code = $row['商品代號'] ?? null;
|
||||
$barcode = $row['條碼'] ?? null;
|
||||
|
||||
// Upsert 邏輯:優先以條碼查找,次之以商品代號查找
|
||||
$product = null;
|
||||
if (!empty($barcode)) {
|
||||
$product = Product::where('barcode', $barcode)->first();
|
||||
}
|
||||
|
||||
if (!$product && !empty($code)) {
|
||||
$product = Product::where('code', $code)->first();
|
||||
}
|
||||
// Upsert 邏輯:透過 Service 統一查找與處理
|
||||
$product = $this->productService->findByBarcodeOrCode($barcode, $code);
|
||||
|
||||
$data = [
|
||||
'name' => $row['商品名稱'],
|
||||
@@ -91,58 +86,16 @@ class ProductImport implements ToModel, WithHeadingRow, WithValidation, WithMapp
|
||||
];
|
||||
|
||||
if ($product) {
|
||||
// 更新現有商品
|
||||
$product->update($data);
|
||||
return null; // 返回 null 以避免 Maatwebsite/Excel 嘗試再次 insert
|
||||
$this->productService->updateProduct($product, $data);
|
||||
} else {
|
||||
if (!empty($code)) $data['code'] = $code;
|
||||
if (!empty($barcode)) $data['barcode'] = $barcode;
|
||||
$this->productService->createProduct($data);
|
||||
}
|
||||
|
||||
// 建立新商品:處理代碼與條碼自動生成
|
||||
if (empty($code)) {
|
||||
$code = $this->generateRandomCode();
|
||||
}
|
||||
if (empty($barcode)) {
|
||||
$barcode = $this->generateRandomBarcode();
|
||||
}
|
||||
|
||||
$data['code'] = $code;
|
||||
$data['barcode'] = $barcode;
|
||||
|
||||
return new Product($data);
|
||||
return null; // 返回 null,因為 Service 已經處理完儲存
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成隨機 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成隨機 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
|
||||
|
||||
@@ -110,4 +110,72 @@ class ProductService implements ProductServiceInterface
|
||||
{
|
||||
return Product::whereIn('code', $codes)->get();
|
||||
}
|
||||
|
||||
public function createProduct(array $data)
|
||||
{
|
||||
if (empty($data['code'])) {
|
||||
$data['code'] = $this->generateRandomCode();
|
||||
}
|
||||
|
||||
if (empty($data['barcode'])) {
|
||||
$data['barcode'] = $this->generateRandomBarcode();
|
||||
}
|
||||
|
||||
return Product::create($data);
|
||||
}
|
||||
|
||||
public function updateProduct(Product $product, array $data)
|
||||
{
|
||||
if (empty($data['code'])) {
|
||||
$data['code'] = $this->generateRandomCode();
|
||||
}
|
||||
|
||||
if (empty($data['barcode'])) {
|
||||
$data['barcode'] = $this->generateRandomBarcode();
|
||||
}
|
||||
|
||||
$product->update($data);
|
||||
return $product;
|
||||
}
|
||||
|
||||
public function generateRandomCode()
|
||||
{
|
||||
$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 generateRandomBarcode()
|
||||
{
|
||||
$barcode = '';
|
||||
|
||||
do {
|
||||
$barcode = '';
|
||||
for ($i = 0; $i < 13; $i++) {
|
||||
$barcode .= rand(0, 9);
|
||||
}
|
||||
} while (Product::where('barcode', $barcode)->exists());
|
||||
|
||||
return $barcode;
|
||||
}
|
||||
|
||||
public function findByBarcodeOrCode(?string $barcode, ?string $code)
|
||||
{
|
||||
$product = null;
|
||||
if (!empty($barcode)) {
|
||||
$product = Product::where('barcode', $barcode)->first();
|
||||
}
|
||||
if (!$product && !empty($code)) {
|
||||
$product = Product::where('code', $code)->first();
|
||||
}
|
||||
return $product;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
if ((key === 'order_date' || key === 'expected_delivery_date' || key === 'invoice_date' || key === 'arrival_date' || key === 'expiry_date' || key === 'received_date' || key === 'production_date') && typeof value === 'string') {
|
||||
return value.split('T')[0].split(' ')[0];
|
||||
}
|
||||
if ((key === 'snapshot_date' || key === 'completed_at' || key === 'posted_at' || key === 'created_at' || key === 'updated_at') && typeof value === 'string') {
|
||||
if ((key === 'snapshot_date' || key === 'completed_at' || key === 'posted_at' || key === 'created_at' || key === 'updated_at' || key === 'actual_time') && typeof value === 'string') {
|
||||
try {
|
||||
const date = new Date(value);
|
||||
return date.toLocaleString('zh-TW', {
|
||||
|
||||
Reference in New Issue
Block a user