Files
star-erp/app/Modules/Inventory/Services/CountService.php
sky121113 2eb136d280 feat(inventory): 完善庫存盤調更新與日誌邏輯,新增「無需盤調」狀態判定
1. 修正 AdjustDocController 缺失 update 方法導致的錯誤。
2. 修正 ActivityDetailDialog 前端 map 渲染 undefined 的 TypeError。
3. 優化盤調單「過帳」日誌,現在會同步包含當時的商品明細快照。
4. 實作盤點單「無需盤調」(no_adjust) 自動判定邏輯:
   - 當盤點數量與庫存完全一致時,自動標記為 no_adjust 結案。
   - 更新前端標籤樣式與操作按鈕對應邏輯。
   - 限制 no_adjust 單據不可重複建立盤調單。
5. 統一盤點單與盤調單的日誌配置,優化 ID 轉名稱顯示。
2026-02-04 16:56:08 +08:00

209 lines
7.4 KiB
PHP

<?php
namespace App\Modules\Inventory\Services;
use App\Modules\Inventory\Models\Inventory;
use App\Modules\Inventory\Models\InventoryCountDoc;
use App\Modules\Inventory\Models\InventoryCountItem;
use App\Modules\Inventory\Models\Warehouse;
use App\Modules\Inventory\Models\Product;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class CountService
{
/**
* 建立新的盤點單並執行快照
*/
public function createDoc(string $warehouseId, string $remarks = null, int $userId): InventoryCountDoc
{
return DB::transaction(function () use ($warehouseId, $remarks, $userId) {
$doc = InventoryCountDoc::create([
'warehouse_id' => $warehouseId,
'status' => 'counting',
'snapshot_date' => now(),
'remarks' => $remarks,
'created_by' => $userId,
]);
return $doc;
});
}
/**
* 執行快照:鎖定當前庫存量
*/
public function snapshot(InventoryCountDoc $doc, bool $updateDoc = true): void
{
DB::transaction(function () use ($doc, $updateDoc) {
// 清除舊的 items (如果有)
$doc->items()->delete();
// 取得該倉庫所有庫存 (包含 quantity = 0 但未軟刪除的)
// 這裡可以根據需求決定是否要過濾掉 0 庫存,通常盤點單會希望能看到所有 "帳上有紀錄" 的東西
$inventories = Inventory::where('warehouse_id', $doc->warehouse_id)
->whereNull('deleted_at')
->get();
$items = [];
foreach ($inventories as $inv) {
$items[] = [
'count_doc_id' => $doc->id,
'product_id' => $inv->product_id,
'batch_number' => $inv->batch_number,
'system_qty' => $inv->quantity,
'counted_qty' => null, // 預設未盤點
'diff_qty' => 0,
'created_at' => now(),
'updated_at' => now(),
];
}
if (!empty($items)) {
InventoryCountItem::insert($items);
}
if ($updateDoc) {
$doc->update([
'status' => 'counting',
'snapshot_date' => now(),
]);
}
});
}
/**
* 完成盤點:過帳差異
*/
public function complete(InventoryCountDoc $doc, int $userId): void
{
DB::transaction(function () use ($doc, $userId) {
// 僅更新單據狀態為「已完成」,不執行庫存入庫/調整
// 盤點單僅作為記錄,後續調整由盤調單 (AdjustDoc) 執行
$doc->update([
'status' => 'completed',
'completed_at' => now(),
'completed_by' => $userId,
]);
});
}
/**
* 更新盤點數量
*/
public function updateCount(InventoryCountDoc $doc, array $itemsData): void
{
DB::transaction(function () use ($doc, $itemsData) {
$updatedItems = [];
$hasChanges = false;
$oldDocAttributes = [
'status' => $doc->status,
'completed_at' => $doc->completed_at ? $doc->completed_at->format('Y-m-d H:i:s') : null,
'completed_by' => $doc->completed_by,
];
foreach ($itemsData as $data) {
$item = $doc->items()->with('product')->find($data['id']);
if ($item) {
$oldQty = $item->counted_qty;
$newQty = $data['counted_qty'];
$oldNotes = $item->notes;
$newNotes = $data['notes'] ?? $item->notes;
$isQtyChanged = $oldQty != $newQty;
$isNotesChanged = $oldNotes !== $newNotes;
if ($isQtyChanged || $isNotesChanged) {
$updatedItems[] = [
'product_name' => $item->product->name,
'old' => [
'counted_qty' => $oldQty,
'notes' => $oldNotes,
],
'new' => [
'counted_qty' => $newQty,
'notes' => $newNotes,
]
];
$countedQty = $data['counted_qty'];
$diff = is_numeric($countedQty) ? ($countedQty - $item->system_qty) : 0;
$item->update([
'counted_qty' => $countedQty,
'diff_qty' => $diff,
'notes' => $newNotes,
]);
$hasChanges = true;
}
}
}
// 檢查是否完成
$doc->refresh();
$isAllCounted = $doc->items()->whereNull('counted_qty')->count() === 0;
$newDocAttributesLog = [];
if ($isAllCounted) {
// 檢查是否有任何差異
$hasDiff = $doc->items()->where('diff_qty', '!=', 0)->exists();
$targetStatus = $hasDiff ? 'completed' : 'no_adjust';
if ($doc->status !== $targetStatus) {
$doc->status = $targetStatus;
$doc->completed_at = now();
$doc->completed_by = auth()->id();
$doc->saveQuietly();
$doc->refresh();
$newDocAttributesLog = [
'status' => $targetStatus,
'completed_at' => $doc->completed_at->format('Y-m-d H:i:s'),
'completed_by' => $doc->completed_by,
];
$hasChanges = true;
}
} else {
if ($doc->status === 'completed') {
$doc->status = 'counting';
$doc->completed_at = null;
$doc->completed_by = null;
$doc->saveQuietly();
$newDocAttributesLog = [
'status' => 'counting',
'completed_at' => null,
'completed_by' => null,
];
$hasChanges = true;
}
}
// 記錄操作日誌
if ($hasChanges) {
$properties = [
'items_diff' => [
'added' => [],
'removed' => [],
'updated' => $updatedItems,
],
];
// 如果有文件層級的屬性變更 (狀態),併入 log
if (!empty($newDocAttributesLog)) {
$properties['attributes'] = $newDocAttributesLog;
$properties['old'] = array_intersect_key($oldDocAttributes, $newDocAttributesLog);
}
activity()
->performedOn($doc)
->causedBy(auth()->user())
->event('updated')
->withProperties($properties)
->log('updated');
}
});
}
}