2026-02-06 16:36:14 +08:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Modules\Inventory\Imports;
|
|
|
|
|
|
|
|
|
|
use App\Modules\Inventory\Models\Product;
|
|
|
|
|
use App\Modules\Inventory\Models\Inventory;
|
|
|
|
|
use App\Modules\Inventory\Models\Warehouse;
|
|
|
|
|
use Maatwebsite\Excel\Concerns\ToModel;
|
|
|
|
|
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
|
|
|
|
use Maatwebsite\Excel\Concerns\WithValidation;
|
|
|
|
|
use Maatwebsite\Excel\Concerns\WithMapping;
|
2026-02-09 10:19:46 +08:00
|
|
|
use Maatwebsite\Excel\Concerns\SkipsEmptyRows;
|
2026-02-06 16:36:14 +08:00
|
|
|
use Maatwebsite\Excel\Imports\HeadingRowFormatter;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
2026-02-09 10:19:46 +08:00
|
|
|
class InventoryImport implements ToModel, WithHeadingRow, WithValidation, WithMapping, SkipsEmptyRows
|
2026-02-06 16:36:14 +08:00
|
|
|
{
|
|
|
|
|
private $warehouse;
|
|
|
|
|
private $inboundDate;
|
|
|
|
|
private $notes;
|
|
|
|
|
|
|
|
|
|
public function __construct(Warehouse $warehouse, string $inboundDate, ?string $notes = null)
|
|
|
|
|
{
|
|
|
|
|
HeadingRowFormatter::default('none');
|
|
|
|
|
$this->warehouse = $warehouse;
|
|
|
|
|
$this->inboundDate = $inboundDate;
|
|
|
|
|
$this->notes = $notes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function map($row): array
|
|
|
|
|
{
|
|
|
|
|
// 處理條碼或代號為字串
|
|
|
|
|
if (isset($row['商品條碼'])) {
|
|
|
|
|
$row['商品條碼'] = (string) $row['商品條碼'];
|
|
|
|
|
}
|
|
|
|
|
if (isset($row['商品代號'])) {
|
|
|
|
|
$row['商品代號'] = (string) $row['商品代號'];
|
|
|
|
|
}
|
2026-02-09 10:19:46 +08:00
|
|
|
if (isset($row['儲位/貨道'])) {
|
|
|
|
|
$row['儲位/貨道'] = (string) $row['儲位/貨道'];
|
|
|
|
|
}
|
2026-02-06 16:36:14 +08:00
|
|
|
return $row;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function model(array $row)
|
|
|
|
|
{
|
|
|
|
|
// 查找商品
|
|
|
|
|
$product = null;
|
|
|
|
|
if (!empty($row['商品條碼'])) {
|
|
|
|
|
$product = Product::where('barcode', $row['商品條碼'])->first();
|
|
|
|
|
}
|
|
|
|
|
if (!$product && !empty($row['商品代號'])) {
|
|
|
|
|
$product = Product::where('code', $row['商品代號'])->first();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$product) {
|
|
|
|
|
return null; // 透過 Validation 攔截
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$quantity = (float) $row['數量'];
|
|
|
|
|
$unitCost = isset($row['入庫單價']) ? (float) $row['入庫單價'] : ($product->cost_price ?? 0);
|
2026-02-06 17:35:50 +08:00
|
|
|
$location = $row['儲位/貨道'] ?? null;
|
2026-02-06 16:36:14 +08:00
|
|
|
|
|
|
|
|
// 批號邏輯:若 Excel 留空則使用 NO-BATCH
|
|
|
|
|
$batchNumber = !empty($row['批號']) ? $row['批號'] : 'NO-BATCH';
|
|
|
|
|
$originCountry = $row['產地'] ?? 'TW';
|
|
|
|
|
$expiryDate = !empty($row['效期']) ? $row['效期'] : null;
|
|
|
|
|
|
2026-02-06 17:35:50 +08:00
|
|
|
return DB::transaction(function () use ($product, $quantity, $unitCost, $location, $batchNumber, $originCountry, $expiryDate) {
|
2026-02-06 16:36:14 +08:00
|
|
|
// 使用與 InventoryController 相同的 firstOrNew 邏輯
|
|
|
|
|
$inventory = $this->warehouse->inventories()->withTrashed()->firstOrNew(
|
|
|
|
|
[
|
|
|
|
|
'product_id' => $product->id,
|
2026-02-06 17:35:50 +08:00
|
|
|
'batch_number' => $batchNumber,
|
|
|
|
|
'location' => $location, // 加入儲位/貨道作為區分關鍵字
|
2026-02-06 16:36:14 +08:00
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'quantity' => 0,
|
|
|
|
|
'unit_cost' => $unitCost,
|
|
|
|
|
'total_value' => 0,
|
|
|
|
|
'arrival_date' => $this->inboundDate,
|
|
|
|
|
'expiry_date' => $expiryDate,
|
|
|
|
|
'origin_country' => $originCountry,
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if ($inventory->trashed()) {
|
|
|
|
|
$inventory->restore();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新數量
|
|
|
|
|
$oldQty = $inventory->quantity;
|
|
|
|
|
$inventory->quantity += $quantity;
|
|
|
|
|
|
|
|
|
|
// 更新單價與總價值
|
|
|
|
|
$inventory->unit_cost = $unitCost;
|
|
|
|
|
$inventory->total_value = $inventory->quantity * $unitCost;
|
|
|
|
|
$inventory->save();
|
|
|
|
|
|
|
|
|
|
// 記錄交易歷史
|
|
|
|
|
$inventory->transactions()->create([
|
|
|
|
|
'warehouse_id' => $this->warehouse->id,
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'batch_number' => $inventory->batch_number,
|
|
|
|
|
'quantity' => $quantity,
|
|
|
|
|
'unit_cost' => $unitCost,
|
2026-02-09 10:19:46 +08:00
|
|
|
'type' => '手動入庫',
|
2026-02-06 16:36:14 +08:00
|
|
|
'reason' => 'Excel 匯入入庫',
|
2026-02-09 10:19:46 +08:00
|
|
|
'balance_before' => $oldQty,
|
|
|
|
|
'balance_after' => $inventory->quantity,
|
|
|
|
|
'actual_time' => $this->inboundDate,
|
2026-02-06 16:36:14 +08:00
|
|
|
'notes' => $this->notes,
|
|
|
|
|
'expiry_date' => $inventory->expiry_date,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return $inventory;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function rules(): array
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
'商品條碼' => ['nullable', 'string'],
|
|
|
|
|
'商品代號' => ['nullable', 'string'],
|
2026-02-09 10:19:46 +08:00
|
|
|
'數量' => [
|
|
|
|
|
'required_with:商品條碼,商品代號', // 只有在有商品資訊時,數量才是必填
|
|
|
|
|
'numeric',
|
|
|
|
|
'min:0' // 允許數量為 0
|
|
|
|
|
],
|
2026-02-06 16:36:14 +08:00
|
|
|
'入庫單價' => ['nullable', 'numeric', 'min:0'],
|
2026-02-06 17:35:50 +08:00
|
|
|
'儲位/貨道' => ['nullable', 'string', 'max:50'],
|
|
|
|
|
'批號' => ['nullable', 'string'],
|
2026-02-06 16:36:14 +08:00
|
|
|
'效期' => ['nullable', 'date'],
|
|
|
|
|
'產地' => ['nullable', 'string', 'max:2'],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|