feat(inventory): 完善庫存盤調更新與日誌邏輯,新增「無需盤調」狀態判定
1. 修正 AdjustDocController 缺失 update 方法導致的錯誤。 2. 修正 ActivityDetailDialog 前端 map 渲染 undefined 的 TypeError。 3. 優化盤調單「過帳」日誌,現在會同步包含當時的商品明細快照。 4. 實作盤點單「無需盤調」(no_adjust) 自動判定邏輯: - 當盤點數量與庫存完全一致時,自動標記為 no_adjust 結案。 - 更新前端標籤樣式與操作按鈕對應邏輯。 - 限制 no_adjust 單據不可重複建立盤調單。 5. 統一盤點單與盤調單的日誌配置,優化 ID 轉名稱顯示。
This commit is contained in:
@@ -29,6 +29,7 @@ class ActivityLogController extends Controller
|
|||||||
'App\Modules\Production\Models\RecipeItem' => '配方品項',
|
'App\Modules\Production\Models\RecipeItem' => '配方品項',
|
||||||
'App\Modules\Production\Models\ProductionOrderItem' => '工單品項',
|
'App\Modules\Production\Models\ProductionOrderItem' => '工單品項',
|
||||||
'App\Modules\Inventory\Models\InventoryCountDoc' => '庫存盤點單',
|
'App\Modules\Inventory\Models\InventoryCountDoc' => '庫存盤點單',
|
||||||
|
'App\Modules\Inventory\Models\InventoryAdjustDoc' => '庫存盤調單',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ class AdjustDocController extends Controller
|
|||||||
// 模式 1: 從盤點單建立
|
// 模式 1: 從盤點單建立
|
||||||
if ($request->filled('count_doc_id')) {
|
if ($request->filled('count_doc_id')) {
|
||||||
$countDoc = InventoryCountDoc::findOrFail($request->count_doc_id);
|
$countDoc = InventoryCountDoc::findOrFail($request->count_doc_id);
|
||||||
|
if ($countDoc->status !== 'completed') {
|
||||||
|
$errorMsg = $countDoc->status === 'no_adjust'
|
||||||
|
? '此盤點單無庫存差異,無需建立盤調單'
|
||||||
|
: '只有已完成盤點的單據可以建立盤調單';
|
||||||
|
return redirect()->back()->with('error', $errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
// 檢查是否已存在對應的盤調單 (避免重複建立)
|
// 檢查是否已存在對應的盤調單 (避免重複建立)
|
||||||
if (InventoryAdjustDoc::where('count_doc_id', $countDoc->id)->exists()) {
|
if (InventoryAdjustDoc::where('count_doc_id', $countDoc->id)->exists()) {
|
||||||
@@ -76,21 +82,6 @@ class AdjustDocController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$doc = $this->adjustService->createFromCountDoc($countDoc, auth()->id());
|
$doc = $this->adjustService->createFromCountDoc($countDoc, auth()->id());
|
||||||
|
|
||||||
// 記錄活動
|
|
||||||
activity()
|
|
||||||
->performedOn($doc)
|
|
||||||
->causedBy(auth()->user())
|
|
||||||
->event('created')
|
|
||||||
->withProperties([
|
|
||||||
'attributes' => $doc->toArray(),
|
|
||||||
'snapshot' => [
|
|
||||||
'doc_no' => $doc->doc_no,
|
|
||||||
'warehouse_name' => $doc->warehouse?->name,
|
|
||||||
'count_doc_no' => $countDoc->doc_no,
|
|
||||||
]
|
|
||||||
])
|
|
||||||
->log('created_from_count');
|
|
||||||
|
|
||||||
return redirect()->route('inventory.adjust.show', [$doc->id])
|
return redirect()->route('inventory.adjust.show', [$doc->id])
|
||||||
->with('success', '已從盤點單生成盤調單');
|
->with('success', '已從盤點單生成盤調單');
|
||||||
@@ -143,6 +134,49 @@ class AdjustDocController extends Controller
|
|||||||
return response()->json($counts);
|
return response()->json($counts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, InventoryAdjustDoc $doc)
|
||||||
|
{
|
||||||
|
$action = $request->input('action', 'update');
|
||||||
|
|
||||||
|
if ($action === 'post') {
|
||||||
|
if ($doc->status !== 'draft') {
|
||||||
|
return redirect()->back()->with('error', '只有草稿狀態的單據可以過帳');
|
||||||
|
}
|
||||||
|
$this->adjustService->post($doc, auth()->id());
|
||||||
|
return redirect()->back()->with('success', '單據已過帳');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'void') {
|
||||||
|
if ($doc->status !== 'draft') {
|
||||||
|
return redirect()->back()->with('error', '只有草稿狀態的單據可以作廢');
|
||||||
|
}
|
||||||
|
$this->adjustService->void($doc, auth()->id());
|
||||||
|
return redirect()->back()->with('success', '單據已作廢');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 一般更新 (更新品項與基本資訊)
|
||||||
|
if ($doc->status !== 'draft') {
|
||||||
|
return redirect()->back()->with('error', '只有草稿狀態的單據可以修改');
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'reason' => 'required|string',
|
||||||
|
'remarks' => 'nullable|string',
|
||||||
|
'items' => 'required|array|min:1',
|
||||||
|
'items.*.product_id' => 'required',
|
||||||
|
'items.*.adjust_qty' => 'required|numeric',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$doc->update([
|
||||||
|
'reason' => $request->reason,
|
||||||
|
'remarks' => $request->remarks,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->adjustService->updateItems($doc, $request->items);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '單據已更新');
|
||||||
|
}
|
||||||
|
|
||||||
public function show(InventoryAdjustDoc $doc)
|
public function show(InventoryAdjustDoc $doc)
|
||||||
{
|
{
|
||||||
$doc->load(['items.product.baseUnit', 'createdBy', 'postedBy', 'warehouse', 'countDoc']);
|
$doc->load(['items.product.baseUnit', 'createdBy', 'postedBy', 'warehouse', 'countDoc']);
|
||||||
@@ -185,18 +219,7 @@ class AdjustDocController extends Controller
|
|||||||
return redirect()->back()->with('error', '只能刪除草稿狀態的單據');
|
return redirect()->back()->with('error', '只能刪除草稿狀態的單據');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 記錄活動
|
|
||||||
activity()
|
|
||||||
->performedOn($doc)
|
|
||||||
->causedBy(auth()->user())
|
|
||||||
->event('deleted')
|
|
||||||
->withProperties([
|
|
||||||
'snapshot' => [
|
|
||||||
'doc_no' => $doc->doc_no,
|
|
||||||
'warehouse_name' => $doc->warehouse?->name,
|
|
||||||
]
|
|
||||||
])
|
|
||||||
->log('deleted');
|
|
||||||
|
|
||||||
$doc->items()->delete();
|
$doc->items()->delete();
|
||||||
$doc->delete();
|
$doc->delete();
|
||||||
|
|||||||
@@ -192,8 +192,8 @@ class CountDocController extends Controller
|
|||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($doc->status !== 'completed') {
|
if (!in_array($doc->status, ['completed', 'no_adjust'])) {
|
||||||
return redirect()->back()->with('error', '僅能針對已完成的盤點單重新開啟盤點');
|
return redirect()->back()->with('error', '僅能針對已完成或無需盤調的盤點單重新開啟盤點');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 執行取消核准邏輯
|
// 執行取消核准邏輯
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use App\Modules\Core\Models\User;
|
use App\Modules\Core\Models\User;
|
||||||
|
|
||||||
|
use Spatie\Activitylog\Traits\LogsActivity;
|
||||||
|
use Spatie\Activitylog\LogOptions;
|
||||||
|
|
||||||
class InventoryAdjustDoc extends Model
|
class InventoryAdjustDoc extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory, LogsActivity;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'doc_no',
|
'doc_no',
|
||||||
@@ -78,4 +81,64 @@ class InventoryAdjustDoc extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(User::class, 'posted_by');
|
return $this->belongsTo(User::class, 'posted_by');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getActivitylogOptions(): LogOptions
|
||||||
|
{
|
||||||
|
return LogOptions::defaults()
|
||||||
|
->logFillable()
|
||||||
|
->dontSubmitEmptyLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName)
|
||||||
|
{
|
||||||
|
// 確保為陣列以進行修改
|
||||||
|
$properties = $activity->properties instanceof \Illuminate\Support\Collection
|
||||||
|
? $activity->properties->toArray()
|
||||||
|
: $activity->properties;
|
||||||
|
|
||||||
|
$snapshot = $properties['snapshot'] ?? [];
|
||||||
|
|
||||||
|
// Snapshot key information
|
||||||
|
$snapshot['doc_no'] = $this->doc_no;
|
||||||
|
$snapshot['warehouse_name'] = $this->warehouse ? $this->warehouse->name : null;
|
||||||
|
$snapshot['posted_at'] = $this->posted_at ? $this->posted_at->format('Y-m-d H:i:s') : null;
|
||||||
|
$snapshot['status'] = $this->status;
|
||||||
|
$snapshot['created_by_name'] = $this->createdBy ? $this->createdBy->name : null;
|
||||||
|
$snapshot['posted_by_name'] = $this->postedBy ? $this->postedBy->name : null;
|
||||||
|
|
||||||
|
$properties['snapshot'] = $snapshot;
|
||||||
|
|
||||||
|
// 全域 ID 轉名稱邏輯 (用於 attributes 與 old)
|
||||||
|
$convertIdsToNames = function (&$data) {
|
||||||
|
if (empty($data) || !is_array($data)) return;
|
||||||
|
|
||||||
|
// 倉庫 ID 轉換
|
||||||
|
if (isset($data['warehouse_id']) && is_numeric($data['warehouse_id'])) {
|
||||||
|
$warehouse = \App\Modules\Inventory\Models\Warehouse::find($data['warehouse_id']);
|
||||||
|
if ($warehouse) {
|
||||||
|
$data['warehouse_id'] = $warehouse->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用者 ID 轉換
|
||||||
|
$userFields = ['created_by', 'updated_by', 'posted_by'];
|
||||||
|
foreach ($userFields as $field) {
|
||||||
|
if (isset($data[$field]) && is_numeric($data[$field])) {
|
||||||
|
$user = \App\Modules\Core\Models\User::find($data[$field]);
|
||||||
|
if ($user) {
|
||||||
|
$data[$field] = $user->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isset($properties['attributes'])) {
|
||||||
|
$convertIdsToNames($properties['attributes']);
|
||||||
|
}
|
||||||
|
if (isset($properties['old'])) {
|
||||||
|
$convertIdsToNames($properties['old']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$activity->properties = $properties;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,8 +82,7 @@ class InventoryCountDoc extends Model
|
|||||||
public function getActivitylogOptions(): \Spatie\Activitylog\LogOptions
|
public function getActivitylogOptions(): \Spatie\Activitylog\LogOptions
|
||||||
{
|
{
|
||||||
return \Spatie\Activitylog\LogOptions::defaults()
|
return \Spatie\Activitylog\LogOptions::defaults()
|
||||||
->logAll()
|
->logFillable()
|
||||||
->logOnlyDirty()
|
|
||||||
->dontSubmitEmptyLogs();
|
->dontSubmitEmptyLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,21 @@ class AdjustService
|
|||||||
public function updateItems(InventoryAdjustDoc $doc, array $itemsData): void
|
public function updateItems(InventoryAdjustDoc $doc, array $itemsData): void
|
||||||
{
|
{
|
||||||
DB::transaction(function () use ($doc, $itemsData) {
|
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();
|
$doc->items()->delete();
|
||||||
|
|
||||||
foreach ($itemsData as $data) {
|
foreach ($itemsData as $data) {
|
||||||
@@ -71,13 +86,60 @@ class AdjustService
|
|||||||
|
|
||||||
$qtyBefore = $inventory ? $inventory->quantity : 0;
|
$qtyBefore = $inventory ? $inventory->quantity : 0;
|
||||||
|
|
||||||
$doc->items()->create([
|
$newItem = $doc->items()->create([
|
||||||
'product_id' => $data['product_id'],
|
'product_id' => $data['product_id'],
|
||||||
'batch_number' => $data['batch_number'] ?? null,
|
'batch_number' => $data['batch_number'] ?? null,
|
||||||
'qty_before' => $qtyBefore,
|
'qty_before' => $qtyBefore,
|
||||||
'adjust_qty' => $data['adjust_qty'],
|
'adjust_qty' => $data['adjust_qty'],
|
||||||
'notes' => $data['notes'] ?? null,
|
'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');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -88,13 +150,11 @@ class AdjustService
|
|||||||
public function post(InventoryAdjustDoc $doc, int $userId): void
|
public function post(InventoryAdjustDoc $doc, int $userId): void
|
||||||
{
|
{
|
||||||
DB::transaction(function () use ($doc, $userId) {
|
DB::transaction(function () use ($doc, $userId) {
|
||||||
|
$oldStatus = $doc->status;
|
||||||
|
|
||||||
foreach ($doc->items as $item) {
|
foreach ($doc->items as $item) {
|
||||||
if ($item->adjust_qty == 0) continue;
|
if ($item->adjust_qty == 0) continue;
|
||||||
|
|
||||||
// 找尋或建立 Inventory
|
|
||||||
// 若是減少庫存,必須確保 Inventory 存在 (且理論上不能變負? 視策略而定,這裡假設允許變負或由 InventoryService 控管)
|
|
||||||
// 若是增加庫存,若不存在需建立
|
|
||||||
|
|
||||||
$inventory = Inventory::firstOrNew([
|
$inventory = Inventory::firstOrNew([
|
||||||
'warehouse_id' => $doc->warehouse_id,
|
'warehouse_id' => $doc->warehouse_id,
|
||||||
'product_id' => $item->product_id,
|
'product_id' => $item->product_id,
|
||||||
@@ -103,7 +163,6 @@ class AdjustService
|
|||||||
|
|
||||||
// 如果是新建立的 object (id 為空),需要初始化 default
|
// 如果是新建立的 object (id 為空),需要初始化 default
|
||||||
if (!$inventory->exists) {
|
if (!$inventory->exists) {
|
||||||
// 繼承 Product 成本或預設 0 (簡化處理)
|
|
||||||
$inventory->unit_cost = $item->product->cost ?? 0;
|
$inventory->unit_cost = $item->product->cost ?? 0;
|
||||||
$inventory->quantity = 0;
|
$inventory->quantity = 0;
|
||||||
}
|
}
|
||||||
@@ -112,7 +171,6 @@ class AdjustService
|
|||||||
$newQty = $oldQty + $item->adjust_qty;
|
$newQty = $oldQty + $item->adjust_qty;
|
||||||
|
|
||||||
$inventory->quantity = $newQty;
|
$inventory->quantity = $newQty;
|
||||||
// 用最新的數量 * 單位成本 (簡化成本計算,不採用移動加權)
|
|
||||||
$inventory->total_value = $newQty * $inventory->unit_cost;
|
$inventory->total_value = $newQty * $inventory->unit_cost;
|
||||||
$inventory->save();
|
$inventory->save();
|
||||||
|
|
||||||
@@ -129,17 +187,53 @@ class AdjustService
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$doc->update([
|
// 使用 saveQuietly 避免重複產生自動日誌
|
||||||
'status' => 'posted',
|
$doc->status = 'posted';
|
||||||
'posted_at' => now(),
|
$doc->posted_at = now();
|
||||||
'posted_by' => $userId,
|
$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. 若關聯盤點單,連動更新盤點單狀態
|
// 4. 若關聯盤點單,連動更新盤點單狀態
|
||||||
if ($doc->count_doc_id) {
|
if ($doc->count_doc_id) {
|
||||||
InventoryCountDoc::where('id', $doc->count_doc_id)->update([
|
$countDoc = InventoryCountDoc::find($doc->count_doc_id);
|
||||||
'status' => 'adjusted'
|
if ($countDoc) {
|
||||||
]);
|
$countDoc->status = 'adjusted';
|
||||||
|
$countDoc->saveQuietly(); // 盤點單也靜默更新
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -152,9 +246,20 @@ class AdjustService
|
|||||||
if ($doc->status !== 'draft') {
|
if ($doc->status !== 'draft') {
|
||||||
throw new \Exception('只能作廢草稿狀態的單據');
|
throw new \Exception('只能作廢草稿狀態的單據');
|
||||||
}
|
}
|
||||||
$doc->update([
|
|
||||||
'status' => 'voided',
|
$oldStatus = $doc->status;
|
||||||
'updated_by' => $userId
|
$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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,16 +145,20 @@ class CountService
|
|||||||
$newDocAttributesLog = [];
|
$newDocAttributesLog = [];
|
||||||
|
|
||||||
if ($isAllCounted) {
|
if ($isAllCounted) {
|
||||||
if ($doc->status !== 'completed') {
|
// 檢查是否有任何差異
|
||||||
$doc->status = 'completed';
|
$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_at = now();
|
||||||
$doc->completed_by = auth()->id();
|
$doc->completed_by = auth()->id();
|
||||||
$doc->saveQuietly();
|
$doc->saveQuietly();
|
||||||
|
|
||||||
$doc->refresh(); // 獲取更新後的屬性 (如時間)
|
$doc->refresh();
|
||||||
|
|
||||||
$newDocAttributesLog = [
|
$newDocAttributesLog = [
|
||||||
'status' => 'completed',
|
'status' => $targetStatus,
|
||||||
'completed_at' => $doc->completed_at->format('Y-m-d H:i:s'),
|
'completed_at' => $doc->completed_at->format('Y-m-d H:i:s'),
|
||||||
'completed_by' => $doc->completed_by,
|
'completed_by' => $doc->completed_by,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -146,7 +146,9 @@ const fieldLabels: Record<string, string> = {
|
|||||||
created_by: '建立者',
|
created_by: '建立者',
|
||||||
updated_by: '更新者',
|
updated_by: '更新者',
|
||||||
completed_by: '完成者',
|
completed_by: '完成者',
|
||||||
|
posted_by: '過帳者',
|
||||||
counted_qty: '盤點數量',
|
counted_qty: '盤點數量',
|
||||||
|
adjust_qty: '調整數量',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 狀態翻譯對照表
|
// 狀態翻譯對照表
|
||||||
@@ -164,6 +166,8 @@ const statusMap: Record<string, string> = {
|
|||||||
// 庫存單據狀態
|
// 庫存單據狀態
|
||||||
counting: '盤點中',
|
counting: '盤點中',
|
||||||
posted: '已過帳',
|
posted: '已過帳',
|
||||||
|
no_adjust: '無需盤調',
|
||||||
|
adjusted: '已盤調',
|
||||||
// 生產工單狀態
|
// 生產工單狀態
|
||||||
planned: '已計畫',
|
planned: '已計畫',
|
||||||
in_progress: '生產中',
|
in_progress: '生產中',
|
||||||
@@ -492,7 +496,7 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{/* 更新項目 */}
|
{/* 更新項目 */}
|
||||||
{activity.properties.items_diff.updated.map((item: any, idx: number) => (
|
{activity.properties.items_diff.updated?.map((item: any, idx: number) => (
|
||||||
<TableRow key={`upd-${idx}`} className="bg-blue-50/10 hover:bg-blue-50/20">
|
<TableRow key={`upd-${idx}`} className="bg-blue-50/10 hover:bg-blue-50/20">
|
||||||
<TableCell className="font-medium">{item.product_name}</TableCell>
|
<TableCell className="font-medium">{item.product_name}</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
@@ -500,41 +504,46 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-sm">
|
<TableCell className="text-sm">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{item.old.quantity !== item.new.quantity && (
|
{item.old?.quantity !== item.new?.quantity && item.old?.quantity !== undefined && (
|
||||||
<div>數量: <span className="text-gray-500 line-through">{item.old.quantity}</span> → <span className="text-blue-700 font-bold">{item.new.quantity}</span></div>
|
<div>數量: <span className="text-gray-500 line-through">{item.old.quantity}</span> → <span className="text-blue-700 font-bold">{item.new.quantity}</span></div>
|
||||||
)}
|
)}
|
||||||
{item.old.counted_qty !== item.new.counted_qty && (
|
{item.old?.counted_qty !== item.new?.counted_qty && item.old?.counted_qty !== undefined && (
|
||||||
<div>盤點量: <span className="text-gray-500 line-through">{item.old.counted_qty ?? '未盤'}</span> → <span className="text-blue-700 font-bold">{item.new.counted_qty ?? '未盤'}</span></div>
|
<div>盤點量: <span className="text-gray-500 line-through">{item.old.counted_qty ?? '未盤'}</span> → <span className="text-blue-700 font-bold">{item.new.counted_qty ?? '未盤'}</span></div>
|
||||||
)}
|
)}
|
||||||
{item.old.unit_name !== item.new.unit_name && (
|
{item.old?.adjust_qty !== item.new?.adjust_qty && (
|
||||||
|
<div>調整量: <span className="text-gray-500 line-through">{item.old?.adjust_qty ?? '0'}</span> → <span className="text-blue-700 font-bold">{item.new?.adjust_qty ?? '0'}</span></div>
|
||||||
|
)}
|
||||||
|
{item.old?.unit_name !== item.new?.unit_name && item.old?.unit_name !== undefined && (
|
||||||
<div>單位: <span className="text-gray-500 line-through">{item.old.unit_name || '-'}</span> → <span className="text-blue-700 font-bold">{item.new.unit_name || '-'}</span></div>
|
<div>單位: <span className="text-gray-500 line-through">{item.old.unit_name || '-'}</span> → <span className="text-blue-700 font-bold">{item.new.unit_name || '-'}</span></div>
|
||||||
)}
|
)}
|
||||||
{item.old.subtotal !== item.new.subtotal && (
|
{item.old?.subtotal !== item.new?.subtotal && item.old?.subtotal !== undefined && (
|
||||||
<div>小計: <span className="text-gray-500 line-through">${item.old.subtotal}</span> → <span className="text-blue-700 font-bold">${item.new.subtotal}</span></div>
|
<div>小計: <span className="text-gray-500 line-through">${item.old.subtotal}</span> → <span className="text-blue-700 font-bold">${item.new.subtotal}</span></div>
|
||||||
)}
|
)}
|
||||||
{item.old.notes !== item.new.notes && (
|
{item.old?.notes !== item.new?.notes && (
|
||||||
<div>備註: <span className="text-gray-500 line-through">{item.old.notes || '-'}</span> → <span className="text-blue-700 font-bold">{item.new.notes || '-'}</span></div>
|
<div>備註: <span className="text-gray-500 line-through">{item.old?.notes || '-'}</span> → <span className="text-blue-700 font-bold">{item.new?.notes || '-'}</span></div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
)) || null}
|
||||||
|
|
||||||
{/* 新增項目 */}
|
{/* 新增項目 */}
|
||||||
{activity.properties.items_diff.added.map((item: any, idx: number) => (
|
{activity.properties.items_diff.added?.map((item: any, idx: number) => (
|
||||||
<TableRow key={`add-${idx}`} className="bg-green-50/10 hover:bg-green-50/20">
|
<TableRow key={`add-${idx}`} className="bg-green-50/10 hover:bg-green-50/20">
|
||||||
<TableCell className="font-medium">{item.product_name}</TableCell>
|
<TableCell className="font-medium">{item.product_name}</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200">新增</Badge>
|
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200">新增</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-sm">
|
<TableCell className="text-sm">
|
||||||
數量: {item.quantity} {item.unit_name} / 小計: ${item.subtotal}
|
{item.quantity !== undefined ? `數量: ${item.quantity} ${item.unit_name || ''} / ` : ''}
|
||||||
|
{item.adjust_qty !== undefined ? `調整量: ${item.adjust_qty} / ` : ''}
|
||||||
|
{item.subtotal !== undefined ? `小計: $${item.subtotal}` : ''}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
)) || null}
|
||||||
|
|
||||||
{/* 移除項目 */}
|
{/* 移除項目 */}
|
||||||
{activity.properties.items_diff.removed.map((item: any, idx: number) => (
|
{activity.properties.items_diff.removed?.map((item: any, idx: number) => (
|
||||||
<TableRow key={`rem-${idx}`} className="bg-red-50/10 hover:bg-red-50/20">
|
<TableRow key={`rem-${idx}`} className="bg-red-50/10 hover:bg-red-50/20">
|
||||||
<TableCell className="font-medium text-gray-400 line-through">{item.product_name}</TableCell>
|
<TableCell className="font-medium text-gray-400 line-through">{item.product_name}</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
@@ -544,7 +553,7 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
|||||||
原紀錄: {item.quantity} {item.unit_name}
|
原紀錄: {item.quantity} {item.unit_name}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
)) || null}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -143,6 +143,8 @@ export default function Index({ docs, warehouses, filters }: any) {
|
|||||||
return <Badge className="bg-blue-500 hover:bg-blue-600">盤點中</Badge>;
|
return <Badge className="bg-blue-500 hover:bg-blue-600">盤點中</Badge>;
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return <Badge className="bg-green-500 hover:bg-green-600">盤點完成</Badge>;
|
return <Badge className="bg-green-500 hover:bg-green-600">盤點完成</Badge>;
|
||||||
|
case 'no_adjust':
|
||||||
|
return <Badge className="bg-green-600 hover:bg-green-700">盤點完成 (無需盤調)</Badge>;
|
||||||
case 'adjusted':
|
case 'adjusted':
|
||||||
return <Badge className="bg-purple-500 hover:bg-purple-600">已盤調庫存</Badge>;
|
return <Badge className="bg-purple-500 hover:bg-purple-600">已盤調庫存</Badge>;
|
||||||
case 'cancelled':
|
case 'cancelled':
|
||||||
@@ -307,7 +309,7 @@ export default function Index({ docs, warehouses, filters }: any) {
|
|||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
{/* Action Button Logic: Prefer Edit if allowed and status is active, otherwise fallback to View if allowed */}
|
{/* Action Button Logic: Prefer Edit if allowed and status is active, otherwise fallback to View if allowed */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const isEditable = !['completed', 'adjusted'].includes(doc.status);
|
const isEditable = !['completed', 'no_adjust', 'adjusted'].includes(doc.status);
|
||||||
const canEdit = can('inventory_count.edit');
|
const canEdit = can('inventory_count.edit');
|
||||||
const canView = can('inventory_count.view');
|
const canView = can('inventory_count.view');
|
||||||
|
|
||||||
@@ -343,7 +345,7 @@ export default function Index({ docs, warehouses, filters }: any) {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
})()}
|
})()}
|
||||||
{!['completed', 'adjusted'].includes(doc.status) && (
|
{!['completed', 'no_adjust', 'adjusted'].includes(doc.status) && (
|
||||||
<Can permission="inventory_count.delete">
|
<Can permission="inventory_count.delete">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default function Show({ doc }: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { can } = usePermission();
|
const { can } = usePermission();
|
||||||
const isCompleted = ['completed', 'adjusted'].includes(doc.status);
|
const isCompleted = ['completed', 'no_adjust', 'adjusted'].includes(doc.status);
|
||||||
const canEdit = can('inventory_count.edit');
|
const canEdit = can('inventory_count.edit');
|
||||||
const isReadOnly = isCompleted || !canEdit;
|
const isReadOnly = isCompleted || !canEdit;
|
||||||
|
|
||||||
@@ -105,6 +105,9 @@ export default function Show({ doc }: any) {
|
|||||||
{doc.status === 'completed' && (
|
{doc.status === 'completed' && (
|
||||||
<Badge className="bg-green-500 hover:bg-green-600">盤點完成</Badge>
|
<Badge className="bg-green-500 hover:bg-green-600">盤點完成</Badge>
|
||||||
)}
|
)}
|
||||||
|
{doc.status === 'no_adjust' && (
|
||||||
|
<Badge className="bg-green-600 hover:bg-green-700">盤點完成 (無需盤調)</Badge>
|
||||||
|
)}
|
||||||
{doc.status === 'adjusted' && (
|
{doc.status === 'adjusted' && (
|
||||||
<Badge className="bg-purple-500 hover:bg-purple-600">已盤調庫存</Badge>
|
<Badge className="bg-purple-500 hover:bg-purple-600">已盤調庫存</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -128,7 +131,7 @@ export default function Show({ doc }: any) {
|
|||||||
列印
|
列印
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{doc.status === 'completed' && (
|
{['completed', 'no_adjust'].includes(doc.status) && (
|
||||||
<Can permission="inventory.adjust">
|
<Can permission="inventory.adjust">
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
|
|||||||
Reference in New Issue
Block a user