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:
250
app/Modules/Procurement/Controllers/PurchaseReturnController.php
Normal file
250
app/Modules/Procurement/Controllers/PurchaseReturnController.php
Normal file
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Procurement\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Procurement\Models\PurchaseReturn;
|
||||
use App\Modules\Procurement\Models\Vendor;
|
||||
use App\Modules\Procurement\Services\PurchaseReturnService;
|
||||
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PurchaseReturnController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected PurchaseReturnService $purchaseReturnService,
|
||||
protected InventoryServiceInterface $inventoryService
|
||||
) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = PurchaseReturn::with(['vendor', 'user'])
|
||||
->orderBy('id', 'desc');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where('code', 'like', "%{$search}%")
|
||||
->orWhereHas('vendor', function($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('status') && $request->status !== 'all') {
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
$purchaseReturns = $query->paginate(15)->withQueryString();
|
||||
|
||||
return Inertia::render('PurchaseReturn/Index', [
|
||||
'purchaseReturns' => $purchaseReturns,
|
||||
'filters' => $request->only(['search', 'status']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
// 取得可用的倉庫與廠商資料供前端選單使用
|
||||
$warehouses = $this->inventoryService->getAllWarehouses();
|
||||
$vendors = Vendor::all();
|
||||
|
||||
// 手動注入:獲取廠商商品 (與 PurchaseOrderController 邏輯一致)
|
||||
$vendorIds = $vendors->pluck('id')->toArray();
|
||||
$pivots = DB::table('product_vendor')->whereIn('vendor_id', $vendorIds)->get();
|
||||
$productIds = $pivots->pluck('product_id')->unique()->toArray();
|
||||
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
|
||||
|
||||
$vendors = $vendors->map(function ($vendor) use ($pivots, $products) {
|
||||
$vendorProductPivots = $pivots->where('vendor_id', $vendor->id);
|
||||
|
||||
$commonProducts = $vendorProductPivots->map(function($pivot) use ($products) {
|
||||
$product = $products->get($pivot->product_id);
|
||||
if (!$product) return null;
|
||||
|
||||
return [
|
||||
'productId' => (string) $product->id,
|
||||
'productName' => $product->name,
|
||||
'lastPrice' => (float) $pivot->last_price,
|
||||
];
|
||||
})->filter()->values();
|
||||
|
||||
return [
|
||||
'id' => (string) $vendor->id,
|
||||
'name' => $vendor->name,
|
||||
'commonProducts' => $commonProducts
|
||||
];
|
||||
});
|
||||
|
||||
return Inertia::render('PurchaseReturn/Create', [
|
||||
'warehouses' => $warehouses,
|
||||
'vendors' => $vendors,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'vendor_id' => 'required|exists:vendors,id',
|
||||
'warehouse_id' => 'required|integer', // 透過 interface 無法直接 exists
|
||||
'return_date' => 'required|date',
|
||||
'remarks' => 'nullable|string',
|
||||
'tax_amount' => 'nullable|numeric|min:0',
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.product_id' => 'required|integer',
|
||||
'items.*.quantity_returned' => 'required|numeric|min:0.01',
|
||||
'items.*.unit_price' => 'required|numeric|min:0',
|
||||
'items.*.batch_number' => 'nullable|string',
|
||||
]);
|
||||
|
||||
try {
|
||||
$pr = $this->purchaseReturnService->store($validated);
|
||||
|
||||
return redirect()->route('procurement.purchase-returns.show', $pr->id)
|
||||
->with('flash', ['success' => '退貨單草稿建立成功']);
|
||||
} catch (\Exception $e) {
|
||||
return back()->with('flash', ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function show(PurchaseReturn $purchaseReturn)
|
||||
{
|
||||
$purchaseReturn->load(['vendor', 'user', 'items']);
|
||||
|
||||
// 取出 product name (依賴反轉)
|
||||
$productIds = $purchaseReturn->items->pluck('product_id')->unique()->toArray();
|
||||
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
|
||||
|
||||
// 取出 warehouse name
|
||||
$warehouse = $this->inventoryService->getWarehouse($purchaseReturn->warehouse_id);
|
||||
$purchaseReturn->warehouse_name = $warehouse ? $warehouse->name : '未知倉庫';
|
||||
|
||||
$purchaseReturn->items->transform(function($item) use ($products) {
|
||||
$item->product_name = $products->get($item->product_id)->name ?? '未知商品';
|
||||
$item->product_code = $products->get($item->product_id)->code ?? '';
|
||||
return $item;
|
||||
});
|
||||
|
||||
// 整理歷史紀錄
|
||||
$activities = \Spatie\Activitylog\Models\Activity::where('subject_type', PurchaseReturn::class)
|
||||
->where('subject_id', $purchaseReturn->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return Inertia::render('PurchaseReturn/Show', [
|
||||
'purchaseReturn' => $purchaseReturn,
|
||||
'activities' => $activities,
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(PurchaseReturn $purchaseReturn)
|
||||
{
|
||||
if ($purchaseReturn->status !== PurchaseReturn::STATUS_DRAFT) {
|
||||
return redirect()->route('procurement.purchase-returns.show', $purchaseReturn->id)
|
||||
->with('flash', ['error' => '只有草稿狀態的退貨單能編輯']);
|
||||
}
|
||||
|
||||
$purchaseReturn->load(['items']);
|
||||
|
||||
$productIds = $purchaseReturn->items->pluck('product_id')->unique()->toArray();
|
||||
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
|
||||
|
||||
$purchaseReturn->items->transform(function($item) use ($products) {
|
||||
$product = $products->get($item->product_id);
|
||||
$item->product = $product;
|
||||
return $item;
|
||||
});
|
||||
|
||||
$warehouses = $this->inventoryService->getAllWarehouses();
|
||||
$vendors = Vendor::all();
|
||||
|
||||
// 手動注入:獲取廠商商品 (與 create 邏輯一致)
|
||||
$vendorIds = $vendors->pluck('id')->toArray();
|
||||
$pivots = DB::table('product_vendor')->whereIn('vendor_id', $vendorIds)->get();
|
||||
$allProductIds = $pivots->pluck('product_id')->unique()->toArray();
|
||||
$allProducts = $this->inventoryService->getProductsByIds($allProductIds)->keyBy('id');
|
||||
|
||||
$vendors = $vendors->map(function ($vendor) use ($pivots, $allProducts) {
|
||||
$vendorProductPivots = $pivots->where('vendor_id', $vendor->id);
|
||||
|
||||
$commonProducts = $vendorProductPivots->map(function($pivot) use ($allProducts) {
|
||||
$product = $allProducts->get($pivot->product_id);
|
||||
if (!$product) return null;
|
||||
|
||||
return [
|
||||
'productId' => (string) $product->id,
|
||||
'productName' => $product->name,
|
||||
'lastPrice' => (float) $pivot->last_price,
|
||||
];
|
||||
})->filter()->values();
|
||||
|
||||
return [
|
||||
'id' => (string) $vendor->id,
|
||||
'name' => $vendor->name,
|
||||
'commonProducts' => $commonProducts
|
||||
];
|
||||
});
|
||||
|
||||
return Inertia::render('PurchaseReturn/Edit', [
|
||||
'purchaseReturn' => $purchaseReturn,
|
||||
'warehouses' => $warehouses,
|
||||
'vendors' => $vendors,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, PurchaseReturn $purchaseReturn)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'vendor_id' => 'required|exists:vendors,id',
|
||||
'warehouse_id' => 'required|integer',
|
||||
'return_date' => 'required|date',
|
||||
'remarks' => 'nullable|string',
|
||||
'tax_amount' => 'nullable|numeric|min:0',
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.product_id' => 'required|integer',
|
||||
'items.*.quantity_returned' => 'required|numeric|min:0.01',
|
||||
'items.*.unit_price' => 'required|numeric|min:0',
|
||||
'items.*.batch_number' => 'nullable|string',
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->purchaseReturnService->update($purchaseReturn, $validated);
|
||||
|
||||
return redirect()->route('procurement.purchase-returns.show', $purchaseReturn->id)
|
||||
->with('flash', ['success' => '退貨單已更新']);
|
||||
} catch (\Exception $e) {
|
||||
return back()->with('flash', ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function submit(PurchaseReturn $purchaseReturn)
|
||||
{
|
||||
try {
|
||||
$this->purchaseReturnService->submit($purchaseReturn);
|
||||
return back()->with('flash', ['success' => '退貨單已確認完成,庫存已成功扣減。']);
|
||||
} catch (\Exception $e) {
|
||||
return back()->with('flash', ['error' => '退貨失敗: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function cancel(PurchaseReturn $purchaseReturn)
|
||||
{
|
||||
try {
|
||||
$this->purchaseReturnService->cancel($purchaseReturn);
|
||||
return back()->with('flash', ['success' => '退貨單已取消']);
|
||||
} catch (\Exception $e) {
|
||||
return back()->with('flash', ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy(PurchaseReturn $purchaseReturn)
|
||||
{
|
||||
try {
|
||||
$this->purchaseReturnService->delete($purchaseReturn);
|
||||
return redirect()->route('procurement.purchase-returns.index')
|
||||
->with('flash', ['success' => '退貨單草稿已刪除']);
|
||||
} catch (\Exception $e) {
|
||||
return back()->with('flash', ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
93
app/Modules/Procurement/Models/PurchaseReturn.php
Normal file
93
app/Modules/Procurement/Models/PurchaseReturn.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Procurement\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PurchaseReturn extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use \Spatie\Activitylog\Traits\LogsActivity;
|
||||
|
||||
public const STATUS_DRAFT = 'draft';
|
||||
public const STATUS_COMPLETED = 'completed';
|
||||
public const STATUS_CANCELLED = 'cancelled';
|
||||
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'vendor_id',
|
||||
'warehouse_id',
|
||||
'user_id',
|
||||
'return_date',
|
||||
'status',
|
||||
'total_amount',
|
||||
'tax_amount',
|
||||
'grand_total',
|
||||
'remarks',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'return_date' => 'date',
|
||||
'total_amount' => 'decimal:2',
|
||||
'tax_amount' => 'decimal:2',
|
||||
'grand_total' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function getActivitylogOptions(): \Spatie\Activitylog\LogOptions
|
||||
{
|
||||
return \Spatie\Activitylog\LogOptions::defaults()
|
||||
->logAll()
|
||||
->logOnlyDirty()
|
||||
->dontSubmitEmptyLogs();
|
||||
}
|
||||
|
||||
public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName)
|
||||
{
|
||||
// 活動紀錄名稱解析 (依 activity-logging.md)
|
||||
$properties = $activity->properties instanceof \Illuminate\Support\Collection
|
||||
? $activity->properties->toArray()
|
||||
: $activity->properties;
|
||||
|
||||
$snapshot = $properties['snapshot'] ?? [];
|
||||
$snapshot['return_no'] = $this->code;
|
||||
$snapshot['warehouse_name'] = $this->warehouse?->name;
|
||||
$properties['snapshot'] = $snapshot;
|
||||
|
||||
$resolver = function (&$data) {
|
||||
if (empty($data) || !is_array($data)) return;
|
||||
|
||||
foreach (['user_id', 'created_by', 'updated_by'] as $f) {
|
||||
if (isset($data[$f]) && is_numeric($data[$f])) {
|
||||
$data[$f] = \App\Modules\Core\Models\User::find($data[$f])?->name;
|
||||
}
|
||||
}
|
||||
if (isset($data['vendor_id']) && is_numeric($data['vendor_id'])) {
|
||||
$data['vendor_id'] = Vendor::find($data['vendor_id'])?->name;
|
||||
}
|
||||
// Strict Mode: Warehouse relation may not be available directly here if it's across modules,
|
||||
// but we might need it for display.
|
||||
};
|
||||
|
||||
if (isset($properties['attributes'])) $resolver($properties['attributes']);
|
||||
if (isset($properties['old'])) $resolver($properties['old']);
|
||||
|
||||
$activity->properties = $properties;
|
||||
}
|
||||
|
||||
public function items()
|
||||
{
|
||||
return $this->hasMany(PurchaseReturnItem::class);
|
||||
}
|
||||
|
||||
public function vendor()
|
||||
{
|
||||
return $this->belongsTo(Vendor::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(\App\Modules\Core\Models\User::class);
|
||||
}
|
||||
}
|
||||
33
app/Modules/Procurement/Models/PurchaseReturnItem.php
Normal file
33
app/Modules/Procurement/Models/PurchaseReturnItem.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Procurement\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PurchaseReturnItem extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'purchase_return_id',
|
||||
'product_id',
|
||||
'quantity_returned',
|
||||
'unit_price',
|
||||
'total_amount',
|
||||
'batch_number',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity_returned' => 'decimal:2',
|
||||
'unit_price' => 'decimal:2',
|
||||
'total_amount' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function purchaseReturn()
|
||||
{
|
||||
return $this->belongsTo(PurchaseReturn::class);
|
||||
}
|
||||
|
||||
// Strict Mode: product relation via ID only for external module.
|
||||
}
|
||||
@@ -36,6 +36,27 @@ Route::middleware('auth')->group(function () {
|
||||
Route::delete('/purchase-orders/{id}', [PurchaseOrderController::class, 'destroy'])->middleware('permission:purchase_orders.delete')->name('purchase-orders.destroy');
|
||||
});
|
||||
|
||||
// 採購退貨單管理
|
||||
Route::middleware('permission:purchase_returns.view')->group(function () {
|
||||
Route::get('/purchase-returns', [\App\Modules\Procurement\Controllers\PurchaseReturnController::class, 'index'])->name('purchase-returns.index');
|
||||
|
||||
Route::middleware('permission:purchase_returns.create')->group(function () {
|
||||
Route::get('/purchase-returns/create', [\App\Modules\Procurement\Controllers\PurchaseReturnController::class, 'create'])->name('purchase-returns.create');
|
||||
Route::post('/purchase-returns', [\App\Modules\Procurement\Controllers\PurchaseReturnController::class, 'store'])->name('purchase-returns.store');
|
||||
});
|
||||
|
||||
Route::get('/purchase-returns/{purchaseReturn}', [\App\Modules\Procurement\Controllers\PurchaseReturnController::class, 'show'])->name('purchase-returns.show');
|
||||
|
||||
Route::middleware('permission:purchase_returns.edit')->group(function () {
|
||||
Route::get('/purchase-returns/{purchaseReturn}/edit', [\App\Modules\Procurement\Controllers\PurchaseReturnController::class, 'edit'])->name('purchase-returns.edit');
|
||||
Route::put('/purchase-returns/{purchaseReturn}', [\App\Modules\Procurement\Controllers\PurchaseReturnController::class, 'update'])->name('purchase-returns.update');
|
||||
Route::post('/purchase-returns/{purchaseReturn}/submit', [\App\Modules\Procurement\Controllers\PurchaseReturnController::class, 'submit'])->name('purchase-returns.submit');
|
||||
Route::post('/purchase-returns/{purchaseReturn}/cancel', [\App\Modules\Procurement\Controllers\PurchaseReturnController::class, 'cancel'])->name('purchase-returns.cancel');
|
||||
});
|
||||
|
||||
Route::delete('/purchase-returns/{purchaseReturn}', [\App\Modules\Procurement\Controllers\PurchaseReturnController::class, 'destroy'])->middleware('permission:purchase_returns.delete')->name('purchase-returns.destroy');
|
||||
});
|
||||
|
||||
// 出貨單管理 (Delivery Notes)
|
||||
Route::middleware('permission:delivery_notes.view')->group(function () {
|
||||
Route::get('/delivery-notes', [\App\Modules\Procurement\Controllers\ShippingOrderController::class, 'index'])->name('delivery-notes.index');
|
||||
|
||||
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