feat(procurement): 實作採購退回單模組並修復商品選單報錯
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 58s
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 58s
This commit is contained in:
210
app/Modules/Procurement/Services/PurchaseReturnService.php
Normal file
210
app/Modules/Procurement/Services/PurchaseReturnService.php
Normal file
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Procurement\Services;
|
||||
|
||||
use App\Modules\Procurement\Models\PurchaseReturn;
|
||||
use App\Modules\Procurement\Models\PurchaseReturnItem;
|
||||
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Exception;
|
||||
|
||||
class PurchaseReturnService
|
||||
{
|
||||
protected $inventoryService;
|
||||
|
||||
public function __construct(InventoryServiceInterface $inventoryService)
|
||||
{
|
||||
// 依賴反轉,透過介面呼叫 Inventory
|
||||
$this->inventoryService = $inventoryService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立退貨單 (草稿)
|
||||
*/
|
||||
public function store(array $data)
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$data['code'] = $this->generateCode($data['return_date']);
|
||||
$data['user_id'] = auth()->id();
|
||||
$data['status'] = PurchaseReturn::STATUS_DRAFT;
|
||||
|
||||
$totalAmount = 0;
|
||||
|
||||
$purchaseReturn = PurchaseReturn::create($data);
|
||||
|
||||
foreach ($data['items'] as $itemData) {
|
||||
$amount = $itemData['quantity_returned'] * $itemData['unit_price'];
|
||||
$totalAmount += $amount;
|
||||
|
||||
$prItem = new PurchaseReturnItem([
|
||||
'product_id' => $itemData['product_id'],
|
||||
'quantity_returned' => $itemData['quantity_returned'],
|
||||
'unit_price' => $itemData['unit_price'],
|
||||
'total_amount' => $amount,
|
||||
'batch_number' => $itemData['batch_number'] ?? null,
|
||||
]);
|
||||
|
||||
$purchaseReturn->items()->save($prItem);
|
||||
}
|
||||
|
||||
// 更新總計 (這裡假定不含額外稅金邏輯,或是由前端帶入 tax_amount)
|
||||
$taxAmount = $data['tax_amount'] ?? 0;
|
||||
$purchaseReturn->update([
|
||||
'total_amount' => $totalAmount,
|
||||
'tax_amount' => $taxAmount,
|
||||
'grand_total' => $totalAmount + $taxAmount,
|
||||
]);
|
||||
|
||||
return $purchaseReturn;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新退貨單 (限草稿)
|
||||
*/
|
||||
public function update(PurchaseReturn $purchaseReturn, array $data)
|
||||
{
|
||||
if ($purchaseReturn->status !== PurchaseReturn::STATUS_DRAFT) {
|
||||
throw new Exception('只有草稿狀態的退回單可以修改。');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($purchaseReturn, $data) {
|
||||
$updateData = [
|
||||
'vendor_id' => $data['vendor_id'] ?? $purchaseReturn->vendor_id,
|
||||
'warehouse_id' => $data['warehouse_id'] ?? $purchaseReturn->warehouse_id,
|
||||
'return_date' => $data['return_date'] ?? $purchaseReturn->return_date,
|
||||
'remarks' => $data['remarks'] ?? $purchaseReturn->remarks,
|
||||
];
|
||||
|
||||
if (isset($data['tax_amount'])) {
|
||||
$updateData['tax_amount'] = $data['tax_amount'];
|
||||
}
|
||||
|
||||
$purchaseReturn->update($updateData);
|
||||
|
||||
if (isset($data['items'])) {
|
||||
$purchaseReturn->items()->delete();
|
||||
$totalAmount = 0;
|
||||
|
||||
foreach ($data['items'] as $itemData) {
|
||||
$amount = $itemData['quantity_returned'] * $itemData['unit_price'];
|
||||
$totalAmount += $amount;
|
||||
|
||||
$prItem = new PurchaseReturnItem([
|
||||
'product_id' => $itemData['product_id'],
|
||||
'quantity_returned' => $itemData['quantity_returned'],
|
||||
'unit_price' => $itemData['unit_price'],
|
||||
'total_amount' => $amount,
|
||||
'batch_number' => $itemData['batch_number'] ?? null,
|
||||
]);
|
||||
$purchaseReturn->items()->save($prItem);
|
||||
}
|
||||
|
||||
$taxAmount = $purchaseReturn->tax_amount;
|
||||
$purchaseReturn->update([
|
||||
'total_amount' => $totalAmount,
|
||||
'grand_total' => $totalAmount + $taxAmount,
|
||||
]);
|
||||
}
|
||||
|
||||
return $purchaseReturn->fresh('items');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 送出審核 / 確認退貨 (扣減倉庫庫存)
|
||||
*/
|
||||
public function submit(PurchaseReturn $purchaseReturn)
|
||||
{
|
||||
if ($purchaseReturn->status !== PurchaseReturn::STATUS_DRAFT) {
|
||||
throw new Exception('只有草稿狀態的退回單可以提交。');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($purchaseReturn) {
|
||||
// 1. 儲存狀態,避免觸發自動修改紀錄 (合併行為)
|
||||
$purchaseReturn->status = PurchaseReturn::STATUS_COMPLETED;
|
||||
$purchaseReturn->saveQuietly();
|
||||
|
||||
// 2. 扣減庫存
|
||||
$updatedItems = [];
|
||||
foreach ($purchaseReturn->items as $prItem) {
|
||||
// 調用 Inventory Service 的 FIFO 扣庫存邏輯
|
||||
$this->inventoryService->decreaseStock(
|
||||
$prItem->product_id,
|
||||
$purchaseReturn->warehouse_id,
|
||||
$prItem->quantity_returned,
|
||||
'採購退回 (' . $purchaseReturn->code . ')'
|
||||
);
|
||||
|
||||
$updatedItems[] = [
|
||||
'product_id' => $prItem->product_id,
|
||||
'quantity_returned' => $prItem->quantity_returned,
|
||||
];
|
||||
}
|
||||
|
||||
// 3. 手動觸發合併的操作紀錄
|
||||
activity()
|
||||
->performedOn($purchaseReturn)
|
||||
->withProperties([
|
||||
'items_diff' => ['returned' => $updatedItems],
|
||||
'attributes' => ['status' => PurchaseReturn::STATUS_COMPLETED],
|
||||
'old' => ['status' => PurchaseReturn::STATUS_DRAFT],
|
||||
'snapshot' => [
|
||||
'return_no' => $purchaseReturn->code,
|
||||
'vendor_id' => $purchaseReturn->vendor_id,
|
||||
] // Service 層若無法拿到 warehouse 名稱,依賴 Model tapActivity 解析
|
||||
])
|
||||
->log('submitted');
|
||||
|
||||
return $purchaseReturn;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消退貨單
|
||||
*/
|
||||
public function cancel(PurchaseReturn $purchaseReturn)
|
||||
{
|
||||
if ($purchaseReturn->status === PurchaseReturn::STATUS_COMPLETED) {
|
||||
throw new Exception('已完成扣庫的退貨單無法直接取消,需進行逆向調整。');
|
||||
}
|
||||
|
||||
$purchaseReturn->update(['status' => PurchaseReturn::STATUS_CANCELLED]);
|
||||
return $purchaseReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刪除退貨單
|
||||
*/
|
||||
public function delete(PurchaseReturn $purchaseReturn)
|
||||
{
|
||||
if ($purchaseReturn->status === PurchaseReturn::STATUS_COMPLETED) {
|
||||
throw new Exception('已完成的退貨單無法刪除。');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($purchaseReturn) {
|
||||
$purchaseReturn->items()->delete();
|
||||
$purchaseReturn->delete();
|
||||
});
|
||||
}
|
||||
|
||||
private function generateCode(string $date)
|
||||
{
|
||||
// 格式: PR-YYYYMMDD-NN
|
||||
$prefix = 'PR-' . date('Ymd', strtotime($date)) . '-';
|
||||
|
||||
$last = PurchaseReturn::where('code', 'like', $prefix . '%')
|
||||
->orderBy('id', 'desc')
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($last) {
|
||||
$seq = intval(substr($last->code, -2)) + 1;
|
||||
} else {
|
||||
$seq = 1;
|
||||
}
|
||||
|
||||
return $prefix . str_pad($seq, 2, '0', STR_PAD_LEFT);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user