feat: 整合門市領料日誌、API 文件存取、修改庫存與併發編號問題、供應商商品內聯編輯及日誌 UI 優化
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m0s

This commit is contained in:
2026-03-02 16:42:12 +08:00
parent 7dac2d1f77
commit 0a955fb993
33 changed files with 1424 additions and 853 deletions

View File

@@ -60,14 +60,6 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei
});
}
/**
* Update an existing Goods Receipt.
*
* @param GoodsReceipt $goodsReceipt
* @param array $data
* @return GoodsReceipt
* @throws \Exception
*/
public function update(GoodsReceipt $goodsReceipt, array $data)
{
if (!in_array($goodsReceipt->status, [GoodsReceipt::STATUS_DRAFT, GoodsReceipt::STATUS_REJECTED])) {
@@ -75,14 +67,42 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei
}
return DB::transaction(function () use ($goodsReceipt, $data) {
$goodsReceipt->update([
$goodsReceipt->fill([
'vendor_id' => $data['vendor_id'] ?? $goodsReceipt->vendor_id,
'received_date' => $data['received_date'] ?? $goodsReceipt->received_date,
'remarks' => $data['remarks'] ?? $goodsReceipt->remarks,
]);
$dirty = $goodsReceipt->getDirty();
$oldAttributes = [];
$newAttributes = [];
foreach ($dirty as $key => $value) {
$oldAttributes[$key] = $goodsReceipt->getOriginal($key);
$newAttributes[$key] = $value;
}
// 儲存但不觸發事件,以避免重複記錄
$goodsReceipt->saveQuietly();
// 捕捉包含商品名稱的舊項目以進行比對
$oldItemsCollection = $goodsReceipt->items()->get();
$oldProductIds = $oldItemsCollection->pluck('product_id')->unique()->toArray();
$oldProducts = $this->inventoryService->getProductsByIds($oldProductIds)->keyBy('id');
$oldItems = $oldItemsCollection->map(function($item) use ($oldProducts) {
$product = $oldProducts->get($item->product_id);
return [
'id' => $item->id,
'product_id' => $item->product_id,
'product_name' => $product?->name ?? 'Unknown',
'quantity_received' => (float) $item->quantity_received,
'unit_price' => (float) $item->unit_price,
'total_amount' => (float) $item->total_amount,
];
})->keyBy('product_id');
if (isset($data['items'])) {
// Simple strategy: delete existing items and recreate
$goodsReceipt->items()->delete();
foreach ($data['items'] as $itemData) {
@@ -99,6 +119,75 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei
}
}
// 計算項目差異
$itemDiffs = [
'added' => [],
'removed' => [],
'updated' => [],
];
$newItemsCollection = $goodsReceipt->items()->get();
$newProductIds = $newItemsCollection->pluck('product_id')->unique()->toArray();
$newProducts = $this->inventoryService->getProductsByIds($newProductIds)->keyBy('id');
$newItemsFormatted = $newItemsCollection->map(function($item) use ($newProducts) {
$product = $newProducts->get($item->product_id);
return [
'product_id' => $item->product_id,
'product_name' => $product?->name ?? 'Unknown',
'quantity_received' => (float) $item->quantity_received,
'unit_price' => (float) $item->unit_price,
'total_amount' => (float) $item->total_amount,
];
})->keyBy('product_id');
foreach ($oldItems as $productId => $oldItem) {
if (!$newItemsFormatted->has($productId)) {
$itemDiffs['removed'][] = $oldItem;
}
}
foreach ($newItemsFormatted as $productId => $newItem) {
if (!$oldItems->has($productId)) {
$itemDiffs['added'][] = $newItem;
} else {
$oldItem = $oldItems[$productId];
if (
$oldItem['quantity_received'] != $newItem['quantity_received'] ||
$oldItem['unit_price'] != $newItem['unit_price'] ||
$oldItem['total_amount'] != $newItem['total_amount']
) {
$itemDiffs['updated'][] = [
'product_name' => $newItem['product_name'],
'old' => [
'quantity_received' => $oldItem['quantity_received'],
'unit_price' => $oldItem['unit_price'],
'total_amount' => $oldItem['total_amount'],
],
'new' => [
'quantity_received' => $newItem['quantity_received'],
'unit_price' => $newItem['unit_price'],
'total_amount' => $newItem['total_amount'],
]
];
}
}
}
// 如果有變更,手動觸發單一合併日誌
if (!empty($newAttributes) || !empty($itemDiffs['added']) || !empty($itemDiffs['removed']) || !empty($itemDiffs['updated'])) {
activity()
->performedOn($goodsReceipt)
->causedBy(auth()->user())
->event('updated')
->withProperties([
'attributes' => $newAttributes,
'old' => $oldAttributes,
'items_diff' => $itemDiffs,
])
->log('updated');
}
return $goodsReceipt->fresh('items');
});
}
@@ -162,23 +251,35 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei
}
private function generateCode(string $date)
private function generateCode(string $date): string
{
// Format: GR-YYYYMMDD-NN
$prefix = 'GR-' . date('Ymd', strtotime($date)) . '-';
$last = GoodsReceipt::where('code', 'like', $prefix . '%')
->orderBy('id', 'desc')
->lockForUpdate()
->first();
// 使用 Cache Lock 防止併發時產生重複單號
$lock = \Illuminate\Support\Facades\Cache::lock('gr_code_generation', 10);
if ($last) {
$seq = intval(substr($last->code, -2)) + 1;
} else {
$seq = 1;
if (!$lock->get()) {
throw new \Exception('系統忙碌中,進貨單號生成失敗,請稍後再試');
}
return $prefix . str_pad($seq, 2, '0', STR_PAD_LEFT);
try {
// Format: GR-YYYYMMDD-NN
$prefix = 'GR-' . date('Ymd', strtotime($date)) . '-';
$last = GoodsReceipt::where('code', 'like', $prefix . '%')
->orderBy('id', 'desc')
->first();
if ($last) {
$seq = intval(substr($last->code, -2)) + 1;
} else {
$seq = 1;
}
$code = $prefix . str_pad($seq, 2, '0', STR_PAD_LEFT);
return $code;
} finally {
$lock->release();
}
}
/**