Files
star-erp/app/Modules/Inventory/Services/AdjustService.php

266 lines
9.6 KiB
PHP
Raw Normal View History

<?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;
});
}
/**
* 更新盤調單內容 (Items)
* 此處採用 "全量更新" 方式處理 items (先刪後加),簡單可靠
*/
public function updateItems(InventoryAdjustDoc $doc, array $itemsData): void
{
DB::transaction(function () use ($doc, $itemsData) {
$updatedItems = [];
$oldItems = $doc->items()->with('product')->get();
// 記錄舊品項狀態 (用於標註異動)
foreach ($oldItems as $oldItem) {
$updatedItems[] = [
'product_name' => $oldItem->product->name,
'old' => [
'adjust_qty' => (float)$oldItem->adjust_qty,
'notes' => $oldItem->notes,
],
'new' => null // 標記為刪除或待更新
];
}
$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;
$newItem = $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,
]);
// 更新日誌中的品項列表
$productName = \App\Modules\Inventory\Models\Product::find($data['product_id'])?->name;
$found = false;
foreach ($updatedItems as $idx => $ui) {
if ($ui['product_name'] === $productName && $ui['new'] === null) {
$updatedItems[$idx]['new'] = [
'adjust_qty' => (float)$data['adjust_qty'],
'notes' => $data['notes'] ?? null,
];
$found = true;
break;
}
}
if (!$found) {
$updatedItems[] = [
'product_name' => $productName,
'old' => null,
'new' => [
'adjust_qty' => (float)$data['adjust_qty'],
'notes' => $data['notes'] ?? null,
]
];
}
}
// 清理沒被更新到的舊品項 (即真正被刪除的)
$finalUpdatedItems = [];
foreach ($updatedItems as $ui) {
if ($ui['old'] === null && $ui['new'] === null) continue;
// 比對是否有實質變動
if ($ui['old'] != $ui['new']) {
$finalUpdatedItems[] = $ui;
}
}
if (!empty($finalUpdatedItems)) {
activity()
->performedOn($doc)
->causedBy(auth()->user())
->event('updated')
->withProperties([
'items_diff' => [
'updated' => $finalUpdatedItems,
]
])
->log('updated');
}
});
}
/**
* 過帳 (Post) - 生效庫存異動
*/
public function post(InventoryAdjustDoc $doc, int $userId): void
{
DB::transaction(function () use ($doc, $userId) {
$oldStatus = $doc->status;
foreach ($doc->items as $item) {
if ($item->adjust_qty == 0) continue;
$inventory = Inventory::firstOrNew([
'warehouse_id' => $doc->warehouse_id,
'product_id' => $item->product_id,
'batch_number' => $item->batch_number,
]);
// 如果是新建立的 object (id 為空),需要初始化 default
if (!$inventory->exists) {
$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,
'reason' => "盤調單 {$doc->doc_no}: " . ($doc->reason ?? '手動調整'),
'actual_time' => now(),
'user_id' => $userId,
]);
}
// 使用 saveQuietly 避免重複產生自動日誌
$doc->status = 'posted';
$doc->posted_at = now();
$doc->posted_by = $userId;
$doc->saveQuietly();
// 準備品項快照供日誌使用
$itemsSnapshot = $doc->items->map(function($item) {
return [
'product_name' => $item->product->name,
'old' => null, // 過帳視為整單生效,不顯示個別欄位差異
'new' => [
'adjust_qty' => (float)$item->adjust_qty,
'notes' => $item->notes,
]
];
})->toArray();
// 手動產生過帳日誌
activity()
->performedOn($doc)
->causedBy(auth()->user())
->event('updated')
->withProperties([
'attributes' => [
'status' => 'posted',
'posted_at' => $doc->posted_at->format('Y-m-d H:i:s'),
'posted_by' => $userId,
],
'old' => [
'status' => $oldStatus,
'posted_at' => null,
'posted_by' => null,
],
'items_diff' => [
'updated' => $itemsSnapshot,
]
])
->log('posted');
// 4. 若關聯盤點單,連動更新盤點單狀態
if ($doc->count_doc_id) {
$countDoc = InventoryCountDoc::find($doc->count_doc_id);
if ($countDoc) {
$countDoc->status = 'adjusted';
$countDoc->saveQuietly(); // 盤點單也靜默更新
}
}
});
}
/**
* 作廢 (Void)
*/
public function void(InventoryAdjustDoc $doc, int $userId): void
{
if ($doc->status !== 'draft') {
throw new \Exception('只能作廢草稿狀態的單據');
}
$oldStatus = $doc->status;
$doc->status = 'voided';
$doc->updated_by = $userId;
$doc->saveQuietly();
activity()
->performedOn($doc)
->causedBy(auth()->user())
->event('updated')
->withProperties([
'attributes' => ['status' => 'voided'],
'old' => ['status' => $oldStatus]
])
->log('voided');
}
}