2026-01-28 18:04:45 +08:00
|
|
|
<?php
|
|
|
|
|
namespace App\Modules\Inventory\Services;
|
|
|
|
|
|
|
|
|
|
use App\Modules\Inventory\Models\Inventory;
|
|
|
|
|
use App\Modules\Inventory\Models\InventoryCountDoc;
|
|
|
|
|
use App\Modules\Inventory\Models\InventoryAdjustDoc;
|
|
|
|
|
use App\Modules\Inventory\Models\InventoryAdjustItem;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
class AdjustService
|
|
|
|
|
{
|
|
|
|
|
public function createDoc(string $warehouseId, string $reason, ?string $remarks = null, int $userId, ?int $countDocId = null): InventoryAdjustDoc
|
|
|
|
|
{
|
|
|
|
|
return InventoryAdjustDoc::create([
|
|
|
|
|
'warehouse_id' => $warehouseId,
|
|
|
|
|
'count_doc_id' => $countDocId,
|
|
|
|
|
'status' => 'draft',
|
|
|
|
|
'reason' => $reason,
|
|
|
|
|
'remarks' => $remarks,
|
|
|
|
|
'created_by' => $userId,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 從盤點單建立盤調單
|
|
|
|
|
*/
|
|
|
|
|
public function createFromCountDoc(InventoryCountDoc $countDoc, int $userId): InventoryAdjustDoc
|
|
|
|
|
{
|
|
|
|
|
return DB::transaction(function () use ($countDoc, $userId) {
|
|
|
|
|
// 1. 建立盤調單頭
|
|
|
|
|
$adjDoc = $this->createDoc(
|
|
|
|
|
$countDoc->warehouse_id,
|
|
|
|
|
"盤點調整: " . $countDoc->doc_no,
|
|
|
|
|
"由盤點單 {$countDoc->doc_no} 自動生成",
|
|
|
|
|
$userId,
|
|
|
|
|
$countDoc->id
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 2. 抓取有差異的明細 (diff_qty != 0)
|
|
|
|
|
foreach ($countDoc->items as $item) {
|
|
|
|
|
if (abs($item->diff_qty) < 0.0001) continue;
|
|
|
|
|
|
|
|
|
|
$adjDoc->items()->create([
|
|
|
|
|
'product_id' => $item->product_id,
|
|
|
|
|
'batch_number' => $item->batch_number,
|
|
|
|
|
'qty_before' => $item->system_qty,
|
|
|
|
|
'adjust_qty' => $item->diff_qty,
|
|
|
|
|
'notes' => "盤點差異: " . $item->diff_qty,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $adjDoc;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-29 13:41:31 +08:00
|
|
|
* 更新盤調單內容 (Items)
|
2026-01-28 18:04:45 +08:00
|
|
|
* 此處採用 "全量更新" 方式處理 items (先刪後加),簡單可靠
|
|
|
|
|
*/
|
|
|
|
|
public function updateItems(InventoryAdjustDoc $doc, array $itemsData): void
|
|
|
|
|
{
|
|
|
|
|
DB::transaction(function () use ($doc, $itemsData) {
|
|
|
|
|
$doc->items()->delete();
|
|
|
|
|
|
|
|
|
|
foreach ($itemsData as $data) {
|
|
|
|
|
// 取得當前庫存作為 qty_before 參考 (僅參考,實際扣減以過帳當下為準)
|
|
|
|
|
$inventory = Inventory::where('warehouse_id', $doc->warehouse_id)
|
|
|
|
|
->where('product_id', $data['product_id'])
|
|
|
|
|
->where('batch_number', $data['batch_number'] ?? null)
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
$qtyBefore = $inventory ? $inventory->quantity : 0;
|
|
|
|
|
|
|
|
|
|
$doc->items()->create([
|
|
|
|
|
'product_id' => $data['product_id'],
|
|
|
|
|
'batch_number' => $data['batch_number'] ?? null,
|
|
|
|
|
'qty_before' => $qtyBefore,
|
|
|
|
|
'adjust_qty' => $data['adjust_qty'],
|
|
|
|
|
'notes' => $data['notes'] ?? null,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 過帳 (Post) - 生效庫存異動
|
|
|
|
|
*/
|
|
|
|
|
public function post(InventoryAdjustDoc $doc, int $userId): void
|
|
|
|
|
{
|
|
|
|
|
DB::transaction(function () use ($doc, $userId) {
|
|
|
|
|
foreach ($doc->items as $item) {
|
|
|
|
|
if ($item->adjust_qty == 0) continue;
|
|
|
|
|
|
|
|
|
|
// 找尋或建立 Inventory
|
|
|
|
|
// 若是減少庫存,必須確保 Inventory 存在 (且理論上不能變負? 視策略而定,這裡假設允許變負或由 InventoryService 控管)
|
|
|
|
|
// 若是增加庫存,若不存在需建立
|
|
|
|
|
|
|
|
|
|
$inventory = Inventory::firstOrNew([
|
|
|
|
|
'warehouse_id' => $doc->warehouse_id,
|
|
|
|
|
'product_id' => $item->product_id,
|
|
|
|
|
'batch_number' => $item->batch_number,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 如果是新建立的 object (id 為空),需要初始化 default
|
|
|
|
|
if (!$inventory->exists) {
|
|
|
|
|
// 繼承 Product 成本或預設 0 (簡化處理)
|
|
|
|
|
$inventory->unit_cost = $item->product->cost ?? 0;
|
|
|
|
|
$inventory->quantity = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$oldQty = $inventory->quantity;
|
|
|
|
|
$newQty = $oldQty + $item->adjust_qty;
|
|
|
|
|
|
|
|
|
|
$inventory->quantity = $newQty;
|
|
|
|
|
// 用最新的數量 * 單位成本 (簡化成本計算,不採用移動加權)
|
|
|
|
|
$inventory->total_value = $newQty * $inventory->unit_cost;
|
|
|
|
|
$inventory->save();
|
|
|
|
|
|
|
|
|
|
// 建立 Transaction
|
|
|
|
|
$inventory->transactions()->create([
|
|
|
|
|
'type' => '庫存調整',
|
|
|
|
|
'quantity' => $item->adjust_qty,
|
|
|
|
|
'unit_cost' => $inventory->unit_cost,
|
|
|
|
|
'balance_before' => $oldQty,
|
|
|
|
|
'balance_after' => $newQty,
|
2026-01-29 13:41:31 +08:00
|
|
|
'reason' => "盤調單 {$doc->doc_no}: " . ($doc->reason ?? '手動調整'),
|
2026-01-28 18:04:45 +08:00
|
|
|
'actual_time' => now(),
|
|
|
|
|
'user_id' => $userId,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$doc->update([
|
|
|
|
|
'status' => 'posted',
|
|
|
|
|
'posted_at' => now(),
|
|
|
|
|
'posted_by' => $userId,
|
|
|
|
|
]);
|
2026-01-29 13:41:31 +08:00
|
|
|
|
|
|
|
|
// 4. 若關聯盤點單,連動更新盤點單狀態
|
|
|
|
|
if ($doc->count_doc_id) {
|
|
|
|
|
InventoryCountDoc::where('id', $doc->count_doc_id)->update([
|
|
|
|
|
'status' => 'adjusted'
|
|
|
|
|
]);
|
|
|
|
|
}
|
2026-01-28 18:04:45 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 作廢 (Void)
|
|
|
|
|
*/
|
|
|
|
|
public function void(InventoryAdjustDoc $doc, int $userId): void
|
|
|
|
|
{
|
|
|
|
|
if ($doc->status !== 'draft') {
|
|
|
|
|
throw new \Exception('只能作廢草稿狀態的單據');
|
|
|
|
|
}
|
|
|
|
|
$doc->update([
|
|
|
|
|
'status' => 'voided',
|
|
|
|
|
'updated_by' => $userId
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|