From 3696cbe165fe2d25eefe4134b303715a282da869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sun, 22 Mar 2026 10:17:54 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[dev]=20=EC=9E=85=EA=B3=A0=20=EA=B0=95?= =?UTF-8?q?=EC=A0=9C=EC=83=9D=EC=84=B1=20API=20=EC=B6=94=EA=B0=80=20(POST?= =?UTF-8?q?=20/dev/force-receiving)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Api/V1/ReceivingController.php | 181 ++++++++++++++++++ routes/api/v1/inventory.php | 3 + 2 files changed, 184 insertions(+) diff --git a/app/Http/Controllers/Api/V1/ReceivingController.php b/app/Http/Controllers/Api/V1/ReceivingController.php index a5d2e336..2bbfca17 100644 --- a/app/Http/Controllers/Api/V1/ReceivingController.php +++ b/app/Http/Controllers/Api/V1/ReceivingController.php @@ -7,8 +7,14 @@ use App\Http\Requests\V1\Receiving\ProcessReceivingRequest; use App\Http\Requests\V1\Receiving\StoreReceivingRequest; use App\Http\Requests\V1\Receiving\UpdateReceivingRequest; +use App\Models\Items\Item; +use App\Models\Qualitys\Inspection; +use App\Models\Tenants\Receiving; +use App\Models\Tenants\Stock; +use App\Models\Tenants\StockLot; use App\Services\ReceivingService; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; class ReceivingController extends Controller { @@ -96,4 +102,179 @@ public function process(int $id, ProcessReceivingRequest $request) return ApiResponse::success($receiving, __('message.receiving.processed')); } + + /** + * [개발전용] 입고 강제 생성 (입고 + 재고 + 수입검사 한번에) + * + * POST /api/v1/dev/force-receiving + * Body: { item_id: number, qty?: number } + */ + public function forceCreate(Request $request) + { + $request->validate([ + 'item_id' => 'required|integer', + 'qty' => 'nullable|integer|min:1|max:10000', + ]); + + $tenantId = app('tenant_id'); + $itemId = $request->input('item_id'); + $qty = $request->input('qty', 100); + $userId = auth()->id() ?? 33; + $date = now()->toDateString(); + + $item = Item::withoutGlobalScopes() + ->where('tenant_id', $tenantId) + ->where('id', $itemId) + ->first(); + + if (! $item) { + return ApiResponse::error(__('error.not_found'), 404); + } + + $result = DB::transaction(function () use ($item, $tenantId, $qty, $userId, $date) { + $datePrefix = date('Ymd', strtotime($date)); + $dateShort = date('ymd', strtotime($date)); + + // 채번 + $receivingSeq = (Receiving::withoutGlobalScopes()->where('tenant_id', $tenantId) + ->where('receiving_number', 'LIKE', "RV{$datePrefix}%")->count()) + 1; + $lotSeq = (StockLot::withoutGlobalScopes()->where('tenant_id', $tenantId) + ->where('lot_no', 'LIKE', "{$dateShort}-%")->count()) + 1; + $inspSeq = (Inspection::withoutGlobalScopes()->where('tenant_id', $tenantId) + ->where('inspection_no', 'LIKE', "IQC-{$dateShort}-%")->count()) + 1; + + $receivingNumber = 'RV'.$datePrefix.str_pad($receivingSeq, 4, '0', STR_PAD_LEFT); + $lotNo = $dateShort.'-'.str_pad($lotSeq, 2, '0', STR_PAD_LEFT); + $orderNo = 'PO-'.$dateShort.'-DEV'.str_pad(rand(1, 999), 3, '0', STR_PAD_LEFT); + + // 1. Receiving 생성 + $receiving = Receiving::withoutGlobalScopes()->create([ + 'tenant_id' => $tenantId, + 'receiving_number' => $receivingNumber, + 'order_no' => $orderNo, + 'order_date' => $date, + 'item_id' => $item->id, + 'item_code' => $item->code, + 'item_name' => $item->name, + 'supplier' => '(주)테스트공급업체', + 'order_qty' => $qty, + 'order_unit' => $item->unit ?: 'EA', + 'due_date' => $date, + 'receiving_qty' => $qty, + 'receiving_date' => $date, + 'lot_no' => $lotNo, + 'supplier_lot' => 'DEV-'.rand(1000, 9999), + 'receiving_location' => 'A-01-01', + 'receiving_manager' => '개발자', + 'status' => 'completed', + 'remark' => '[개발전용] 강제 생성 입고', + 'options' => [ + 'inspection_status' => '적', + 'inspection_date' => $date, + 'inspection_result' => '합격', + 'force_created' => true, + ], + 'created_by' => $userId, + 'updated_by' => $userId, + ]); + + // 2. Stock 생성/갱신 + $itemTypeMap = ['FG' => 'purchased_part', 'PT' => 'bent_part', 'SM' => 'sub_material', 'RM' => 'raw_material', 'CS' => 'consumable']; + $stockType = $itemTypeMap[$item->item_type] ?? 'raw_material'; + + $stock = Stock::withoutGlobalScopes() + ->where('tenant_id', $tenantId) + ->where('item_id', $item->id) + ->first(); + + if ($stock) { + $stock->stock_qty += $qty; + $stock->available_qty += $qty; + $stock->lot_count += 1; + $stock->last_receipt_date = $date; + $stock->status = 'normal'; + $stock->updated_by = $userId; + $stock->save(); + } else { + $stock = Stock::withoutGlobalScopes()->create([ + 'tenant_id' => $tenantId, + 'item_id' => $item->id, + 'item_code' => $item->code, + 'item_name' => $item->name, + 'item_type' => $stockType, + 'unit' => $item->unit ?: 'EA', + 'stock_qty' => $qty, + 'safety_stock' => 10, + 'reserved_qty' => 0, + 'available_qty' => $qty, + 'lot_count' => 1, + 'oldest_lot_date' => $date, + 'location' => 'A-01-01', + 'status' => 'normal', + 'last_receipt_date' => $date, + 'created_by' => $userId, + ]); + } + + // 3. StockLot 생성 + $nextFifo = (StockLot::withoutGlobalScopes()->where('stock_id', $stock->id)->max('fifo_order') ?? 0) + 1; + StockLot::withoutGlobalScopes()->create([ + 'tenant_id' => $tenantId, + 'stock_id' => $stock->id, + 'lot_no' => $lotNo, + 'fifo_order' => $nextFifo, + 'receipt_date' => $date, + 'qty' => $qty, + 'reserved_qty' => 0, + 'available_qty' => $qty, + 'unit' => $item->unit ?: 'EA', + 'supplier' => '(주)테스트공급업체', + 'supplier_lot' => $receiving->supplier_lot, + 'po_number' => $orderNo, + 'location' => 'A-01-01', + 'status' => 'available', + 'receiving_id' => $receiving->id, + 'created_by' => $userId, + ]); + + // 4. IQC 수입검사 생성 (합격) + $inspectionNo = 'IQC-'.$dateShort.'-'.str_pad($inspSeq, 4, '0', STR_PAD_LEFT); + Inspection::withoutGlobalScopes()->create([ + 'tenant_id' => $tenantId, + 'inspection_no' => $inspectionNo, + 'inspection_type' => 'IQC', + 'status' => 'completed', + 'result' => 'pass', + 'request_date' => $date, + 'inspection_date' => $date, + 'item_id' => $item->id, + 'lot_no' => $lotNo, + 'meta' => [ + 'quantity' => $qty, + 'unit' => $item->unit ?: 'EA', + 'item_code' => $item->code, + 'item_name' => $item->name, + 'force_created' => true, + ], + 'items' => [ + ['item' => '외관검사', 'standard' => '이상 없을 것', 'result' => '양호', 'judgment' => '적'], + ['item' => '치수검사', 'standard' => '규격 일치', 'result' => '양호', 'judgment' => '적'], + ], + 'extra' => ['remarks' => '[개발전용] 자동 합격', 'opinion' => '양호'], + 'created_by' => $userId, + 'updated_by' => $userId, + ]); + + return [ + 'receiving_number' => $receivingNumber, + 'lot_no' => $lotNo, + 'item_code' => $item->code, + 'item_name' => $item->name, + 'qty' => $qty, + 'available_qty' => $stock->available_qty, + ]; + }); + + return ApiResponse::success($result, '입고 데이터가 강제 생성되었습니다.'); + } } diff --git a/routes/api/v1/inventory.php b/routes/api/v1/inventory.php index e689107c..bcb2bb2f 100644 --- a/routes/api/v1/inventory.php +++ b/routes/api/v1/inventory.php @@ -103,6 +103,9 @@ Route::post('/{id}/process', [ReceivingController::class, 'process'])->whereNumber('id')->name('v1.receivings.process'); }); +// Dev: 입고 강제생성 (개발 전용 - 입고+재고+수입검사 한번에 생성) +Route::post('/dev/force-receiving', [ReceivingController::class, 'forceCreate'])->name('v1.dev.force-receiving'); + // Stock API (재고 현황) Route::prefix('stocks')->group(function () { Route::get('', [StockController::class, 'index'])->name('v1.stocks.index');