feat: 整合門市領料日誌、API 文件存取、修改庫存與併發編號問題、供應商商品內聯編輯及日誌 UI 優化
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m0s
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m0s
This commit is contained in:
@@ -104,4 +104,12 @@ interface ProcurementServiceInterface
|
||||
* 移除供貨商品關聯
|
||||
*/
|
||||
public function detachProductFromVendor(int $vendorId, int $productId): void;
|
||||
|
||||
/**
|
||||
* 整批同步供貨商品
|
||||
*
|
||||
* @param int $vendorId
|
||||
* @param array $productsData Format: [['product_id' => 1, 'last_price' => 100], ...]
|
||||
*/
|
||||
public function syncVendorProducts(int $vendorId, array $productsData): void;
|
||||
}
|
||||
|
||||
@@ -189,71 +189,81 @@ class PurchaseOrderController extends Controller
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
// 使用 Cache Lock 防止併發時產生重複單號
|
||||
$lock = \Illuminate\Support\Facades\Cache::lock('po_code_generation', 10);
|
||||
|
||||
// 生成單號:PO-YYYYMMDD-01
|
||||
$today = now()->format('Ymd');
|
||||
$prefix = 'PO-' . $today . '-';
|
||||
$lastOrder = PurchaseOrder::where('code', 'like', $prefix . '%')
|
||||
->lockForUpdate() // 鎖定以避免並發衝突
|
||||
->orderBy('code', 'desc')
|
||||
->first();
|
||||
|
||||
if ($lastOrder) {
|
||||
// 取得最後 2 碼序號並加 1
|
||||
$lastSequence = intval(substr($lastOrder->code, -2));
|
||||
$sequence = str_pad($lastSequence + 1, 2, '0', STR_PAD_LEFT);
|
||||
} else {
|
||||
$sequence = '01';
|
||||
}
|
||||
$code = $prefix . $sequence;
|
||||
|
||||
$totalAmount = 0;
|
||||
foreach ($validated['items'] as $item) {
|
||||
$totalAmount += $item['subtotal'];
|
||||
if (!$lock->get()) {
|
||||
return back()->withErrors(['error' => '系統忙碌中,請稍後再試']);
|
||||
}
|
||||
|
||||
// 稅額計算
|
||||
$taxAmount = isset($validated['tax_amount']) ? $validated['tax_amount'] : round($totalAmount * 0.05, 2);
|
||||
$grandTotal = $totalAmount + $taxAmount;
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// 確保有一個有效的使用者 ID
|
||||
$userId = auth()->id();
|
||||
if (!$userId) {
|
||||
$user = $this->coreService->ensureSystemUserExists(); $userId = $user->id;
|
||||
}
|
||||
// 生成單號:PO-YYYYMMDD-01
|
||||
$today = now()->format('Ymd');
|
||||
$prefix = 'PO-' . $today . '-';
|
||||
$lastOrder = PurchaseOrder::where('code', 'like', $prefix . '%')
|
||||
->orderBy('code', 'desc')
|
||||
->first();
|
||||
|
||||
$order = PurchaseOrder::create([
|
||||
'code' => $code,
|
||||
'vendor_id' => $validated['vendor_id'],
|
||||
'warehouse_id' => $validated['warehouse_id'],
|
||||
'user_id' => $userId,
|
||||
'status' => 'draft',
|
||||
'order_date' => $validated['order_date'], // 新增
|
||||
'expected_delivery_date' => $validated['expected_delivery_date'],
|
||||
'total_amount' => $totalAmount,
|
||||
'tax_amount' => $taxAmount,
|
||||
'grand_total' => $grandTotal,
|
||||
'remark' => $validated['remark'],
|
||||
'invoice_number' => $validated['invoice_number'] ?? null,
|
||||
'invoice_date' => $validated['invoice_date'] ?? null,
|
||||
'invoice_amount' => $validated['invoice_amount'] ?? null,
|
||||
]);
|
||||
if ($lastOrder) {
|
||||
$lastSequence = intval(substr($lastOrder->code, -2));
|
||||
$sequence = str_pad($lastSequence + 1, 2, '0', STR_PAD_LEFT);
|
||||
} else {
|
||||
$sequence = '01';
|
||||
}
|
||||
$code = $prefix . $sequence;
|
||||
|
||||
foreach ($validated['items'] as $item) {
|
||||
// 反算單價
|
||||
$unitPrice = $item['quantity'] > 0 ? $item['subtotal'] / $item['quantity'] : 0;
|
||||
$totalAmount = 0;
|
||||
foreach ($validated['items'] as $item) {
|
||||
$totalAmount += $item['subtotal'];
|
||||
}
|
||||
|
||||
$order->items()->create([
|
||||
'product_id' => $item['productId'],
|
||||
'quantity' => $item['quantity'],
|
||||
'unit_id' => $item['unitId'] ?? null,
|
||||
'unit_price' => $unitPrice,
|
||||
'subtotal' => $item['subtotal'],
|
||||
// 稅額計算
|
||||
$taxAmount = isset($validated['tax_amount']) ? $validated['tax_amount'] : round($totalAmount * 0.05, 2);
|
||||
$grandTotal = $totalAmount + $taxAmount;
|
||||
|
||||
// 確保有一個有效的使用者 ID
|
||||
$userId = auth()->id();
|
||||
if (!$userId) {
|
||||
$user = $this->coreService->ensureSystemUserExists();
|
||||
$userId = $user->id;
|
||||
}
|
||||
|
||||
$order = PurchaseOrder::create([
|
||||
'code' => $code,
|
||||
'vendor_id' => $validated['vendor_id'],
|
||||
'warehouse_id' => $validated['warehouse_id'],
|
||||
'user_id' => $userId,
|
||||
'status' => 'draft',
|
||||
'order_date' => $validated['order_date'],
|
||||
'expected_delivery_date' => $validated['expected_delivery_date'],
|
||||
'total_amount' => $totalAmount,
|
||||
'tax_amount' => $taxAmount,
|
||||
'grand_total' => $grandTotal,
|
||||
'remark' => $validated['remark'],
|
||||
'invoice_number' => $validated['invoice_number'] ?? null,
|
||||
'invoice_date' => $validated['invoice_date'] ?? null,
|
||||
'invoice_amount' => $validated['invoice_amount'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
foreach ($validated['items'] as $item) {
|
||||
// 反算單價
|
||||
$unitPrice = $item['quantity'] > 0 ? $item['subtotal'] / $item['quantity'] : 0;
|
||||
|
||||
$order->items()->create([
|
||||
'product_id' => $item['productId'],
|
||||
'quantity' => $item['quantity'],
|
||||
'unit_id' => $item['unitId'] ?? null,
|
||||
'unit_price' => $unitPrice,
|
||||
'subtotal' => $item['subtotal'],
|
||||
]);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
} finally {
|
||||
$lock->release();
|
||||
}
|
||||
|
||||
return redirect()->route('purchase-orders.index')->with('success', '採購單已成功建立');
|
||||
|
||||
|
||||
@@ -133,4 +133,35 @@ class VendorProductController extends Controller
|
||||
|
||||
return redirect()->back()->with('success', '供貨商品已移除');
|
||||
}
|
||||
|
||||
/**
|
||||
* 整批同步供貨商品
|
||||
*/
|
||||
public function sync(Request $request, Vendor $vendor)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'products' => 'present|array',
|
||||
'products.*.product_id' => 'required|exists:products,id',
|
||||
'products.*.last_price' => 'nullable|numeric|min:0',
|
||||
]);
|
||||
|
||||
$this->procurementService->syncVendorProducts($vendor->id, $validated['products']);
|
||||
|
||||
activity()
|
||||
->performedOn($vendor)
|
||||
->withProperties([
|
||||
'attributes' => [
|
||||
'products_count' => count($validated['products']),
|
||||
],
|
||||
'sub_subject' => '供貨商品',
|
||||
'snapshot' => [
|
||||
'name' => "{$vendor->name} 的供貨清單",
|
||||
'vendor_name' => $vendor->name,
|
||||
]
|
||||
])
|
||||
->event('updated')
|
||||
->log('整批更新供貨商品');
|
||||
|
||||
return redirect()->back()->with('success', '供貨商品已更新');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ class PurchaseOrder extends Model
|
||||
'tax_amount',
|
||||
'grand_total',
|
||||
'remark',
|
||||
'invoice_number',
|
||||
'invoice_date',
|
||||
'invoice_amount',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
||||
@@ -16,6 +16,7 @@ Route::middleware('auth')->group(function () {
|
||||
|
||||
// 供貨商品相關路由
|
||||
Route::post('/vendors/{vendor}/products', [VendorProductController::class, 'store'])->middleware('permission:vendors.edit')->name('vendors.products.store');
|
||||
Route::put('/vendors/{vendor}/products/sync', [VendorProductController::class, 'sync'])->middleware('permission:vendors.edit')->name('vendors.products.sync');
|
||||
Route::put('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'update'])->middleware('permission:vendors.edit')->name('vendors.products.update');
|
||||
Route::delete('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'destroy'])->middleware('permission:vendors.edit')->name('vendors.products.destroy');
|
||||
});
|
||||
|
||||
@@ -147,4 +147,48 @@ class ProcurementService implements ProcurementServiceInterface
|
||||
->where('product_id', $productId)
|
||||
->delete();
|
||||
}
|
||||
|
||||
public function syncVendorProducts(int $vendorId, array $productsData): void
|
||||
{
|
||||
\Illuminate\Support\Facades\DB::transaction(function () use ($vendorId, $productsData) {
|
||||
$existingPivots = \Illuminate\Support\Facades\DB::table('product_vendor')
|
||||
->where('vendor_id', $vendorId)
|
||||
->get();
|
||||
|
||||
$existingProductIds = $existingPivots->pluck('product_id')->toArray();
|
||||
$newProductIds = array_column($productsData, 'product_id');
|
||||
|
||||
$toDelete = array_diff($existingProductIds, $newProductIds);
|
||||
|
||||
if (!empty($toDelete)) {
|
||||
\Illuminate\Support\Facades\DB::table('product_vendor')
|
||||
->where('vendor_id', $vendorId)
|
||||
->whereIn('product_id', $toDelete)
|
||||
->delete();
|
||||
}
|
||||
|
||||
foreach ($productsData as $data) {
|
||||
$exists = in_array($data['product_id'], $existingProductIds);
|
||||
|
||||
if ($exists) {
|
||||
\Illuminate\Support\Facades\DB::table('product_vendor')
|
||||
->where('vendor_id', $vendorId)
|
||||
->where('product_id', $data['product_id'])
|
||||
->update([
|
||||
'last_price' => $data['last_price'] ?? null,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
} else {
|
||||
\Illuminate\Support\Facades\DB::table('product_vendor')
|
||||
->insert([
|
||||
'vendor_id' => $vendorId,
|
||||
'product_id' => $data['product_id'],
|
||||
'last_price' => $data['last_price'] ?? null,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user