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\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,
|
2026-02-04 15:12:10 +08:00
|
|
|
'status' => 'counting',
|
|
|
|
|
'snapshot_date' => now(),
|
2026-01-28 18:04:45 +08:00
|
|
|
'remarks' => $remarks,
|
|
|
|
|
'created_by' => $userId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return $doc;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 執行快照:鎖定當前庫存量
|
|
|
|
|
*/
|
2026-02-04 15:12:10 +08:00
|
|
|
public function snapshot(InventoryCountDoc $doc, bool $updateDoc = true): void
|
2026-01-28 18:04:45 +08:00
|
|
|
{
|
2026-02-04 15:12:10 +08:00
|
|
|
DB::transaction(function () use ($doc, $updateDoc) {
|
2026-01-28 18:04:45 +08:00
|
|
|
// 清除舊的 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 15:12:10 +08:00
|
|
|
if ($updateDoc) {
|
|
|
|
|
$doc->update([
|
|
|
|
|
'status' => 'counting',
|
|
|
|
|
'snapshot_date' => now(),
|
|
|
|
|
]);
|
|
|
|
|
}
|
2026-01-28 18:04:45 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 完成盤點:過帳差異
|
|
|
|
|
*/
|
|
|
|
|
public function complete(InventoryCountDoc $doc, int $userId): void
|
|
|
|
|
{
|
|
|
|
|
DB::transaction(function () use ($doc, $userId) {
|
2026-01-29 09:36:07 +08:00
|
|
|
// 僅更新單據狀態為「已完成」,不執行庫存入庫/調整
|
|
|
|
|
// 盤點單僅作為記錄,後續調整由盤調單 (AdjustDoc) 執行
|
2026-01-28 18:04:45 +08:00
|
|
|
$doc->update([
|
|
|
|
|
'status' => 'completed',
|
|
|
|
|
'completed_at' => now(),
|
|
|
|
|
'completed_by' => $userId,
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新盤點數量
|
|
|
|
|
*/
|
|
|
|
|
public function updateCount(InventoryCountDoc $doc, array $itemsData): void
|
|
|
|
|
{
|
|
|
|
|
DB::transaction(function () use ($doc, $itemsData) {
|
2026-02-04 15:12:10 +08:00
|
|
|
$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,
|
|
|
|
|
];
|
|
|
|
|
|
2026-01-28 18:04:45 +08:00
|
|
|
foreach ($itemsData as $data) {
|
2026-02-04 15:12:10 +08:00
|
|
|
$item = $doc->items()->with('product')->find($data['id']);
|
2026-01-28 18:04:45 +08:00
|
|
|
if ($item) {
|
2026-02-04 15:12:10 +08:00
|
|
|
$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) {
|
2026-02-04 16:56:08 +08:00
|
|
|
// 檢查是否有任何差異
|
|
|
|
|
$hasDiff = $doc->items()->where('diff_qty', '!=', 0)->exists();
|
|
|
|
|
$targetStatus = $hasDiff ? 'completed' : 'no_adjust';
|
|
|
|
|
|
|
|
|
|
if ($doc->status !== $targetStatus) {
|
|
|
|
|
$doc->status = $targetStatus;
|
2026-02-04 15:12:10 +08:00
|
|
|
$doc->completed_at = now();
|
|
|
|
|
$doc->completed_by = auth()->id();
|
|
|
|
|
$doc->saveQuietly();
|
2026-01-28 18:04:45 +08:00
|
|
|
|
2026-02-04 16:56:08 +08:00
|
|
|
$doc->refresh();
|
2026-02-04 15:12:10 +08:00
|
|
|
|
|
|
|
|
$newDocAttributesLog = [
|
2026-02-04 16:56:08 +08:00
|
|
|
'status' => $targetStatus,
|
2026-02-04 15:12:10 +08:00
|
|
|
'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;
|
2026-01-28 18:04:45 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-04 15:12:10 +08:00
|
|
|
|
|
|
|
|
// 記錄操作日誌
|
|
|
|
|
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');
|
|
|
|
|
}
|
2026-01-28 18:04:45 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|