From 407afe38e484875131b8562f9b8c74109578ff50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 16 Mar 2026 21:27:13 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[order]=20=EC=9E=AC=EA=B3=A0=EC=83=9D?= =?UTF-8?q?=EC=82=B0=EA=B4=80=EB=A6=AC(STOCK)=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Order 모델에 TYPE_STOCK = 'STOCK' 상수 추가 - StoreOrderRequest/UpdateOrderRequest에 STOCK 타입 validation 추가 - options에 production_reason, target_stock_qty 필드 추가 - 재고생산 채번: STK{YYYYMMDD}{NNNN} 형식 - stats()에 order_type 필터 파라미터 추가 - STOCK 타입 확정 시 매출 자동 생성 스킵 --- .../Controllers/Api/V1/OrderController.php | 6 +-- app/Http/Requests/Order/StoreOrderRequest.php | 4 +- .../Requests/Order/UpdateOrderRequest.php | 4 +- app/Models/Orders/Order.php | 2 + app/Services/OrderService.php | 45 ++++++++++++++++--- 5 files changed, 49 insertions(+), 12 deletions(-) diff --git a/app/Http/Controllers/Api/V1/OrderController.php b/app/Http/Controllers/Api/V1/OrderController.php index d7b4b2ca..acf7001f 100644 --- a/app/Http/Controllers/Api/V1/OrderController.php +++ b/app/Http/Controllers/Api/V1/OrderController.php @@ -30,10 +30,10 @@ public function index(Request $request) /** * 통계 조회 */ - public function stats() + public function stats(Request $request) { - return ApiResponse::handle(function () { - return $this->service->stats(); + return ApiResponse::handle(function () use ($request) { + return $this->service->stats($request->input('order_type')); }, __('message.order.fetched')); } diff --git a/app/Http/Requests/Order/StoreOrderRequest.php b/app/Http/Requests/Order/StoreOrderRequest.php index aed252d1..272f924c 100644 --- a/app/Http/Requests/Order/StoreOrderRequest.php +++ b/app/Http/Requests/Order/StoreOrderRequest.php @@ -18,7 +18,7 @@ public function rules(): array return [ // 기본 정보 'quote_id' => 'nullable|integer|exists:quotes,id', - 'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE])], + 'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE, Order::TYPE_STOCK])], 'status_code' => ['nullable', Rule::in([ Order::STATUS_DRAFT, Order::STATUS_CONFIRMED, @@ -55,6 +55,8 @@ public function rules(): array 'options.shipping_address' => 'nullable|string|max:500', 'options.shipping_address_detail' => 'nullable|string|max:500', 'options.manager_name' => 'nullable|string|max:100', + 'options.production_reason' => 'nullable|string|max:500', + 'options.target_stock_qty' => 'nullable|numeric|min:0', // 품목 배열 'items' => 'nullable|array', diff --git a/app/Http/Requests/Order/UpdateOrderRequest.php b/app/Http/Requests/Order/UpdateOrderRequest.php index 59a25181..53cd3168 100644 --- a/app/Http/Requests/Order/UpdateOrderRequest.php +++ b/app/Http/Requests/Order/UpdateOrderRequest.php @@ -17,7 +17,7 @@ public function rules(): array { return [ // 기본 정보 (order_no는 수정 불가) - 'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE])], + 'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE, Order::TYPE_STOCK])], 'category_code' => 'nullable|string|max:50', // 거래처 정보 @@ -49,6 +49,8 @@ public function rules(): array 'options.shipping_address' => 'nullable|string|max:500', 'options.shipping_address_detail' => 'nullable|string|max:500', 'options.manager_name' => 'nullable|string|max:100', + 'options.production_reason' => 'nullable|string|max:500', + 'options.target_stock_qty' => 'nullable|numeric|min:0', // 품목 배열 (전체 교체) 'items' => 'nullable|array', diff --git a/app/Models/Orders/Order.php b/app/Models/Orders/Order.php index 426aece9..792ab592 100644 --- a/app/Models/Orders/Order.php +++ b/app/Models/Orders/Order.php @@ -78,6 +78,8 @@ class Order extends Model public const TYPE_PURCHASE = 'PURCHASE'; // 발주 + public const TYPE_STOCK = 'STOCK'; // 재고생산 + // 매출 인식 시점 public const SALES_ON_ORDER_CONFIRM = 'on_order_confirm'; // 수주확정 시 diff --git a/app/Services/OrderService.php b/app/Services/OrderService.php index f0636ca0..dd1ab494 100644 --- a/app/Services/OrderService.php +++ b/app/Services/OrderService.php @@ -109,17 +109,22 @@ public function index(array $params) /** * 통계 조회 */ - public function stats(): array + public function stats(?string $orderType = null): array { $tenantId = $this->tenantId(); - $counts = Order::where('tenant_id', $tenantId) + $baseQuery = Order::where('tenant_id', $tenantId); + if ($orderType !== null) { + $baseQuery->where('order_type_code', $orderType); + } + + $counts = (clone $baseQuery) ->select('status_code', DB::raw('count(*) as count')) ->groupBy('status_code') ->pluck('count', 'status_code') ->toArray(); - $amounts = Order::where('tenant_id', $tenantId) + $amounts = (clone $baseQuery) ->select('status_code', DB::raw('sum(total_amount) as total')) ->groupBy('status_code') ->pluck('total', 'status_code') @@ -162,10 +167,13 @@ public function store(array $data) $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId, $userId) { - // 수주번호 자동 생성 + // 수주번호 자동 생성 (재고생산은 STK 접두사) $pairCode = $data['pair_code'] ?? null; unset($data['pair_code']); - $data['order_no'] = $this->generateOrderNo($tenantId, $pairCode); + $isStock = ($data['order_type_code'] ?? null) === Order::TYPE_STOCK; + $data['order_no'] = $isStock + ? $this->generateStockOrderNo($tenantId) + : $this->generateOrderNo($tenantId, $pairCode); $data['tenant_id'] = $tenantId; $data['created_by'] = $userId; $data['updated_by'] = $userId; @@ -629,8 +637,8 @@ public function updateStatus(int $id, string $status) $createdSale = null; $previousStatus = $order->status_code; - // 수주확정 시 매출 자동 생성 (sales_recognition = on_order_confirm인 경우) - if ($status === Order::STATUS_CONFIRMED && $order->shouldCreateSaleOnConfirm()) { + // 수주확정 시 매출 자동 생성 (재고생산은 매출 생성 불필요) + if ($status === Order::STATUS_CONFIRMED && $order->order_type_code !== Order::TYPE_STOCK && $order->shouldCreateSaleOnConfirm()) { $createdSale = $this->createSaleFromOrder($order, $userId); $order->sale_id = $createdSale->id; } @@ -776,6 +784,29 @@ private function generateOrderNoLegacy(int $tenantId): string return sprintf('%s%s%04d', $prefix, $date, $seq); } + /** + * 재고생산 번호 생성 (STK{YYYYMMDD}{NNNN}) + */ + private function generateStockOrderNo(int $tenantId): string + { + $prefix = 'STK'; + $date = now()->format('Ymd'); + + $lastNo = Order::withoutGlobalScopes() + ->where('tenant_id', $tenantId) + ->where('order_no', 'like', "{$prefix}{$date}%") + ->orderByDesc('order_no') + ->value('order_no'); + + if ($lastNo) { + $seq = (int) substr($lastNo, -4) + 1; + } else { + $seq = 1; + } + + return sprintf('%s%s%04d', $prefix, $date, $seq); + } + /** * 견적에서 수주 생성 */