Files
star-erp/app/Modules/Sales/Controllers/SalesImportController.php
sky121113 deef3baacc
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 57s
refactor: 重構模組通訊與調整儀表板功能
- 依循跨模組通訊規範,將 Sales 與 Production 模組中對 Inventory 的直接模型關聯改為透過 InventoryServiceInterface 取得
- 於 InventoryService 實作獲取最高庫存價值、即將過期商品等方法,供儀表板使用
- 確保所有跨模組調用皆採用手動水和(Manual Hydration)方式組合資料
- 移除本地已歸檔的 .agent 規範檔案
2026-02-25 11:48:52 +08:00

174 lines
6.6 KiB
PHP

<?php
namespace App\Modules\Sales\Controllers;
use App\Http\Controllers\Controller;
use App\Modules\Sales\Models\SalesImportBatch;
use App\Modules\Sales\Imports\SalesImport;
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Maatwebsite\Excel\Facades\Excel;
use Illuminate\Support\Facades\DB;
class SalesImportController extends Controller
{
public function index(Request $request)
{
$perPage = $request->input('per_page', 10);
$search = $request->input('search');
$batches = SalesImportBatch::with('importer')
->when($search, function ($query, $search) {
$query->where(function ($q) use ($search) {
$q->where('id', 'like', "%{$search}%")
->orWhereHas('importer', function ($u) use ($search) {
$u->where('name', 'like', "%{$search}%");
});
});
})
->orderByDesc('created_at')
->paginate($perPage)
->withQueryString();
return Inertia::render('Sales/Import/Index', [
'batches' => $batches,
'filters' => [
'per_page' => (string) $perPage,
'search' => $search,
],
]);
}
public function store(Request $request)
{
$request->validate([
'file' => 'required|file|mimes:xlsx,xls,csv,zip',
]);
DB::transaction(function () use ($request) {
$batch = SalesImportBatch::create([
'import_date' => now(),
'imported_by' => auth()->id(),
'status' => 'pending',
'tenant_id' => tenant('id'), // If tenant context requires it, but usually automatic
]);
Excel::import(new SalesImport($batch), $request->file('file'));
});
return redirect()->route('sales-imports.index')->with('success', '匯入成功,請確認內容。');
}
public function show(Request $request, SalesImportBatch $import)
{
$import->load(['items', 'importer']);
$perPage = $request->input('per_page', 10);
$paginatedItems = $import->items()->paginate($perPage)->withQueryString();
// Manual Hydration for Products and Warehouses
$inventoryService = app(InventoryServiceInterface::class);
$productIds = collect($paginatedItems->items())->pluck('product_id')->filter()->unique()->toArray();
$machineCodes = collect($paginatedItems->items())->pluck('machine_id')->filter()->unique()->toArray();
$products = $inventoryService->getProductsByIds($productIds)->keyBy('id');
$warehouses = $inventoryService->getWarehousesByCodes($machineCodes)->keyBy('code');
$paginatedItems->getCollection()->transform(function ($item) use ($products, $warehouses) {
$item->product = $products->get($item->product_id);
$item->warehouse = $warehouses->get($item->machine_id);
return $item;
});
return Inertia::render('Sales/Import/Show', [
'import' => $import,
'items' => $paginatedItems,
'filters' => [
'per_page' => $perPage,
],
]);
}
public function confirm(SalesImportBatch $import, InventoryServiceInterface $inventoryService)
{
if ($import->status !== 'pending') {
return back()->with('error', '此批次無法確認。');
}
DB::transaction(function () use ($import, $inventoryService) {
// 1. Prepare Aggregation
$aggregatedDeductions = []; // Key: "warehouse_id:product_id:slot"
// Pre-load necessary warehouses for matching
$machineIds = $import->items->pluck('machine_id')->filter()->unique()->toArray();
$warehouses = $inventoryService->getWarehousesByCodes($machineIds)->keyBy('code');
foreach ($import->items as $item) {
// Only process shipped items with a valid product
if ($item->product_id && $item->original_status === '已出貨') {
// Resolve Warehouse from Machine ID
$warehouse = $warehouses->get($item->machine_id);
// Skip if machine_id is empty or warehouse not found
if (!$warehouse) {
continue;
}
// Aggregation Key includes Slot (貨道)
$slot = $item->slot ?: '';
$key = "{$warehouse->id}:{$item->product_id}:{$slot}";
if (!isset($aggregatedDeductions[$key])) {
$aggregatedDeductions[$key] = [
'warehouse_id' => $warehouse->id,
'product_id' => $item->product_id,
'slot' => $slot,
'quantity' => 0,
'details' => []
];
}
$aggregatedDeductions[$key]['quantity'] += $item->quantity;
$aggregatedDeductions[$key]['details'][] = $item->transaction_serial;
}
}
// 2. Execute Aggregated Deductions
foreach ($aggregatedDeductions as $deduction) {
// Construct a descriptive reason
$serialCount = count($deduction['details']);
$reason = "銷售出貨彙總 (批號: {$import->id}, 貨道: {$deduction['slot']}, 共 {$serialCount} 筆交易)";
$inventoryService->decreaseStock(
$deduction['product_id'],
$deduction['warehouse_id'],
$deduction['quantity'],
$reason,
true, // Force deduction
$deduction['slot'] // Location/Slot
);
}
// 3. Update Batch Status
$import->update([
'status' => 'confirmed',
'confirmed_at' => now(),
]);
});
return redirect()->route('sales-imports.index')->with('success', '已彙總(含貨道)並扣除庫存。');
}
public function destroy(SalesImportBatch $import)
{
if ($import->status !== 'pending') {
return back()->with('error', '只能刪除待確認的批次。');
}
$import->delete();
return redirect()->route('sales-imports.index')->with('success', '已刪除匯入批次。');
}
}