Merge branch 'develop' of http://114.203.209.83:3000/SamProject/sam-api into develop
This commit is contained in:
@@ -95,7 +95,7 @@ public function render($request, Throwable $exception)
|
||||
if ($exception instanceof BadRequestHttpException) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '잘못된 요청',
|
||||
'message' => $exception->getMessage() ?: '잘못된 요청',
|
||||
'data' => null,
|
||||
], 400);
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'; // 수주확정 시
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -174,6 +182,11 @@ public function store(array $data)
|
||||
$data['status_code'] = $data['status_code'] ?? Order::STATUS_DRAFT;
|
||||
$data['order_type_code'] = $data['order_type_code'] ?? Order::TYPE_ORDER;
|
||||
|
||||
// 재고생산: 현장명 자동 설정
|
||||
if ($isStock) {
|
||||
$data['site_name'] = '재고생산';
|
||||
}
|
||||
|
||||
$items = $data['items'] ?? [];
|
||||
unset($data['items']);
|
||||
|
||||
@@ -629,8 +642,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 +789,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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적에서 수주 생성
|
||||
*/
|
||||
@@ -1202,124 +1238,145 @@ public function createProductionOrder(int $orderId, array $data)
|
||||
throw new BadRequestHttpException(__('error.order.production_order_already_exists'));
|
||||
}
|
||||
|
||||
// order_nodes의 BOM 결과를 기반으로 공정별 자동 분류
|
||||
$bomItemIds = [];
|
||||
$nodesBomMap = []; // node_id => [item_name => bom_item]
|
||||
// 재고생산(STOCK): 절곡 공정에 모든 품목 직접 배정 (BOM 매칭 스킵)
|
||||
$isStock = $order->order_type_code === Order::TYPE_STOCK;
|
||||
$nodesBomMap = [];
|
||||
|
||||
foreach ($order->rootNodes as $node) {
|
||||
$bomResult = $node->options['bom_result'] ?? [];
|
||||
$bomItems = $bomResult['items'] ?? [];
|
||||
|
||||
foreach ($bomItems as $bomItem) {
|
||||
if (! empty($bomItem['item_id'])) {
|
||||
$bomItemIds[] = $bomItem['item_id'];
|
||||
$nodesBomMap[$node->id][$bomItem['item_name']] = $bomItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$bomItemIds = array_unique($bomItemIds);
|
||||
|
||||
// process_items 테이블에서 item_id → process_id 매핑 조회
|
||||
$itemProcessMap = [];
|
||||
if (! empty($bomItemIds)) {
|
||||
$processItems = DB::table('process_items as pi')
|
||||
->join('processes as p', 'pi.process_id', '=', 'p.id')
|
||||
->where('p.tenant_id', $tenantId)
|
||||
->whereIn('pi.item_id', $bomItemIds)
|
||||
->where('pi.is_active', true)
|
||||
->select('pi.item_id', 'pi.process_id')
|
||||
->get();
|
||||
|
||||
foreach ($processItems as $pi) {
|
||||
$itemProcessMap[$pi->item_id] = $pi->process_id;
|
||||
}
|
||||
}
|
||||
|
||||
// item_code → item_id 매핑 구축 (fallback용 — N+1 방지를 위해 사전 일괄 조회)
|
||||
$codeToIdMap = [];
|
||||
if (! empty($bomItemIds)) {
|
||||
$codeToIdRows = DB::table('items')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('id', $bomItemIds)
|
||||
->whereNull('deleted_at')
|
||||
->select('id', 'code')
|
||||
->get();
|
||||
foreach ($codeToIdRows as $row) {
|
||||
$codeToIdMap[$row->code] = $row->id;
|
||||
}
|
||||
}
|
||||
|
||||
// order_items의 item_code로 추가 매핑 사전 구축 (루프 내 DB 조회 방지)
|
||||
$orderItemCodes = $order->items->pluck('item_code')->filter()->unique()->values()->all();
|
||||
$unmappedCodes = array_diff($orderItemCodes, array_keys($codeToIdMap));
|
||||
if (! empty($unmappedCodes)) {
|
||||
$extraRows = DB::table('items')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('code', $unmappedCodes)
|
||||
->whereNull('deleted_at')
|
||||
->select('id', 'code')
|
||||
->get();
|
||||
foreach ($extraRows as $row) {
|
||||
$codeToIdMap[$row->code] = $row->id;
|
||||
}
|
||||
}
|
||||
|
||||
// 사전 매핑된 item_id에 대한 process_items도 일괄 조회
|
||||
$allResolvedIds = array_values(array_unique(array_merge(
|
||||
array_keys($itemProcessMap),
|
||||
array_values($codeToIdMap)
|
||||
)));
|
||||
$unmappedProcessIds = array_diff($allResolvedIds, array_keys($itemProcessMap));
|
||||
if (! empty($unmappedProcessIds)) {
|
||||
$extraProcessItems = DB::table('process_items')
|
||||
->whereIn('item_id', $unmappedProcessIds)
|
||||
if ($isStock) {
|
||||
$bendingProcess = \App\Models\Process::where('tenant_id', $tenantId)
|
||||
->where('process_name', '절곡')
|
||||
->where('is_active', true)
|
||||
->select('item_id', 'process_id')
|
||||
->get();
|
||||
foreach ($extraProcessItems as $pi) {
|
||||
$itemProcessMap[$pi->item_id] = $pi->process_id;
|
||||
}
|
||||
}
|
||||
->first();
|
||||
|
||||
// order_items를 공정별로 그룹화 (BOM item_id → process 매핑 활용)
|
||||
$itemsByProcess = [];
|
||||
foreach ($order->items as $orderItem) {
|
||||
$processId = null;
|
||||
|
||||
// 1. order_item의 item_id가 있으면 직접 매핑
|
||||
if ($orderItem->item_id && isset($itemProcessMap[$orderItem->item_id])) {
|
||||
$processId = $itemProcessMap[$orderItem->item_id];
|
||||
if (! $bendingProcess) {
|
||||
throw new BadRequestHttpException(__('error.order.bending_process_not_found'));
|
||||
}
|
||||
// 2. item_id가 없으면 노드의 BOM에서 item_name으로 찾기
|
||||
elseif ($orderItem->order_node_id && isset($nodesBomMap[$orderItem->order_node_id])) {
|
||||
$nodeBom = $nodesBomMap[$orderItem->order_node_id];
|
||||
$bomItem = $nodeBom[$orderItem->item_name] ?? null;
|
||||
if ($bomItem && ! empty($bomItem['item_id']) && isset($itemProcessMap[$bomItem['item_id']])) {
|
||||
$processId = $itemProcessMap[$bomItem['item_id']];
|
||||
|
||||
$itemsByProcess = [
|
||||
$bendingProcess->id => [
|
||||
'process_id' => $bendingProcess->id,
|
||||
'items' => $order->items->all(),
|
||||
],
|
||||
];
|
||||
} else {
|
||||
// 기존 로직: order_nodes의 BOM 결과를 기반으로 공정별 자동 분류
|
||||
$bomItemIds = [];
|
||||
|
||||
foreach ($order->rootNodes as $node) {
|
||||
$bomResult = $node->options['bom_result'] ?? [];
|
||||
$bomItems = $bomResult['items'] ?? [];
|
||||
|
||||
foreach ($bomItems as $bomItem) {
|
||||
if (! empty($bomItem['item_id'])) {
|
||||
$bomItemIds[] = $bomItem['item_id'];
|
||||
$nodesBomMap[$node->id][$bomItem['item_name']] = $bomItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. fallback: 사전 구축된 맵에서 item_code → process 매핑 (N+1 제거)
|
||||
if ($processId === null && $orderItem->item_code) {
|
||||
$resolvedId = $codeToIdMap[$orderItem->item_code] ?? null;
|
||||
if ($resolvedId && isset($itemProcessMap[$resolvedId])) {
|
||||
$processId = $itemProcessMap[$resolvedId];
|
||||
$bomItemIds = array_unique($bomItemIds);
|
||||
|
||||
// process_items 테이블에서 item_id → process_id 매핑 조회
|
||||
$itemProcessMap = [];
|
||||
if (! empty($bomItemIds)) {
|
||||
$processItems = DB::table('process_items as pi')
|
||||
->join('processes as p', 'pi.process_id', '=', 'p.id')
|
||||
->where('p.tenant_id', $tenantId)
|
||||
->whereIn('pi.item_id', $bomItemIds)
|
||||
->where('pi.is_active', true)
|
||||
->select('pi.item_id', 'pi.process_id')
|
||||
->get();
|
||||
|
||||
foreach ($processItems as $pi) {
|
||||
$itemProcessMap[$pi->item_id] = $pi->process_id;
|
||||
}
|
||||
}
|
||||
|
||||
$key = $processId ?? 'none';
|
||||
|
||||
if (! isset($itemsByProcess[$key])) {
|
||||
$itemsByProcess[$key] = [
|
||||
'process_id' => $processId,
|
||||
'items' => [],
|
||||
];
|
||||
// item_code → item_id 매핑 구축 (fallback용 — N+1 방지를 위해 사전 일괄 조회)
|
||||
$codeToIdMap = [];
|
||||
if (! empty($bomItemIds)) {
|
||||
$codeToIdRows = DB::table('items')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('id', $bomItemIds)
|
||||
->whereNull('deleted_at')
|
||||
->select('id', 'code')
|
||||
->get();
|
||||
foreach ($codeToIdRows as $row) {
|
||||
$codeToIdMap[$row->code] = $row->id;
|
||||
}
|
||||
}
|
||||
|
||||
// order_items의 item_code로 추가 매핑 사전 구축 (루프 내 DB 조회 방지)
|
||||
$orderItemCodes = $order->items->pluck('item_code')->filter()->unique()->values()->all();
|
||||
$unmappedCodes = array_diff($orderItemCodes, array_keys($codeToIdMap));
|
||||
if (! empty($unmappedCodes)) {
|
||||
$extraRows = DB::table('items')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('code', $unmappedCodes)
|
||||
->whereNull('deleted_at')
|
||||
->select('id', 'code')
|
||||
->get();
|
||||
foreach ($extraRows as $row) {
|
||||
$codeToIdMap[$row->code] = $row->id;
|
||||
}
|
||||
}
|
||||
|
||||
// 사전 매핑된 item_id에 대한 process_items도 일괄 조회
|
||||
$allResolvedIds = array_values(array_unique(array_merge(
|
||||
array_keys($itemProcessMap),
|
||||
array_values($codeToIdMap)
|
||||
)));
|
||||
$unmappedProcessIds = array_diff($allResolvedIds, array_keys($itemProcessMap));
|
||||
if (! empty($unmappedProcessIds)) {
|
||||
$extraProcessItems = DB::table('process_items')
|
||||
->whereIn('item_id', $unmappedProcessIds)
|
||||
->where('is_active', true)
|
||||
->select('item_id', 'process_id')
|
||||
->get();
|
||||
foreach ($extraProcessItems as $pi) {
|
||||
$itemProcessMap[$pi->item_id] = $pi->process_id;
|
||||
}
|
||||
}
|
||||
|
||||
// order_items를 공정별로 그룹화 (BOM item_id → process 매핑 활용)
|
||||
$itemsByProcess = [];
|
||||
foreach ($order->items as $orderItem) {
|
||||
$processId = null;
|
||||
|
||||
// 1. order_item의 item_id가 있으면 직접 매핑
|
||||
if ($orderItem->item_id && isset($itemProcessMap[$orderItem->item_id])) {
|
||||
$processId = $itemProcessMap[$orderItem->item_id];
|
||||
}
|
||||
// 2. item_id가 없으면 노드의 BOM에서 item_name으로 찾기
|
||||
elseif ($orderItem->order_node_id && isset($nodesBomMap[$orderItem->order_node_id])) {
|
||||
$nodeBom = $nodesBomMap[$orderItem->order_node_id];
|
||||
$bomItem = $nodeBom[$orderItem->item_name] ?? null;
|
||||
if ($bomItem && ! empty($bomItem['item_id']) && isset($itemProcessMap[$bomItem['item_id']])) {
|
||||
$processId = $itemProcessMap[$bomItem['item_id']];
|
||||
}
|
||||
}
|
||||
|
||||
// 3. fallback: 사전 구축된 맵에서 item_code → process 매핑 (N+1 제거)
|
||||
if ($processId === null && $orderItem->item_code) {
|
||||
$resolvedId = $codeToIdMap[$orderItem->item_code] ?? null;
|
||||
if ($resolvedId && isset($itemProcessMap[$resolvedId])) {
|
||||
$processId = $itemProcessMap[$resolvedId];
|
||||
}
|
||||
}
|
||||
|
||||
$key = $processId ?? 'none';
|
||||
|
||||
if (! isset($itemsByProcess[$key])) {
|
||||
$itemsByProcess[$key] = [
|
||||
'process_id' => $processId,
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
$itemsByProcess[$key]['items'][] = $orderItem;
|
||||
}
|
||||
$itemsByProcess[$key]['items'][] = $orderItem;
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($order, $data, $tenantId, $userId, $itemsByProcess, $nodesBomMap) {
|
||||
return DB::transaction(function () use ($order, $data, $tenantId, $userId, $itemsByProcess, $nodesBomMap, $isStock) {
|
||||
$workOrders = [];
|
||||
|
||||
// 담당자 ID 배열 처리 (assignee_ids 우선, fallback으로 assignee_id)
|
||||
@@ -1339,6 +1396,7 @@ public function createProductionOrder(int $orderId, array $data)
|
||||
|
||||
// 공정 옵션 초기화 (보조 공정 플래그 포함)
|
||||
$workOrderOptions = null;
|
||||
$process = null;
|
||||
if ($processId) {
|
||||
$process = \App\Models\Process::find($processId);
|
||||
if ($process && ! empty($process->options['is_auxiliary'])) {
|
||||
@@ -1378,13 +1436,13 @@ public function createProductionOrder(int $orderId, array $data)
|
||||
'tenant_id' => $tenantId,
|
||||
'work_order_no' => $workOrderNo,
|
||||
'sales_order_id' => $order->id,
|
||||
'project_name' => $order->site_name ?? $order->client_name,
|
||||
'project_name' => $isStock ? '재고생산' : ($order->site_name ?? $order->client_name),
|
||||
'process_id' => $processId,
|
||||
'status' => (! empty($assigneeIds) || $teamId) ? WorkOrder::STATUS_WAITING : WorkOrder::STATUS_UNASSIGNED,
|
||||
'priority' => $priority,
|
||||
'assignee_id' => $primaryAssigneeId,
|
||||
'team_id' => $teamId,
|
||||
'scheduled_date' => $data['scheduled_date'] ?? $order->delivery_date,
|
||||
'scheduled_date' => $data['scheduled_date'] ?? ($isStock ? now()->toDateString() : $order->delivery_date),
|
||||
'memo' => $data['memo'] ?? null,
|
||||
'options' => $workOrderOptions,
|
||||
'is_active' => true,
|
||||
@@ -1473,7 +1531,7 @@ public function createProductionOrder(int $orderId, array $data)
|
||||
'item_id' => $itemId,
|
||||
'item_name' => $orderItem->item_name,
|
||||
'specification' => $orderItem->specification,
|
||||
'quantity' => $orderItem->quantity,
|
||||
'quantity' => (int) $orderItem->quantity,
|
||||
'unit' => $orderItem->unit,
|
||||
'sort_order' => $sortOrder++,
|
||||
'status' => 'pending',
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 계정과목 카테고리를 영문으로 통일
|
||||
*
|
||||
* tenant_id=1(코드브릿지엑스 본사)에 한글 카테고리가 섞여 있어
|
||||
* API 표준(영문)으로 통일한다.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$mapping = [
|
||||
'자산' => 'asset',
|
||||
'부채' => 'liability',
|
||||
'자본' => 'capital',
|
||||
'수익' => 'revenue',
|
||||
'비용' => 'expense',
|
||||
];
|
||||
|
||||
foreach ($mapping as $korean => $english) {
|
||||
DB::table('account_codes')
|
||||
->where('category', $korean)
|
||||
->update(['category' => $english]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$mapping = [
|
||||
'asset' => '자산',
|
||||
'liability' => '부채',
|
||||
'capital' => '자본',
|
||||
'revenue' => '수익',
|
||||
'expense' => '비용',
|
||||
];
|
||||
|
||||
// tenant_id=1만 원복 (다른 테넌트는 원래 영문)
|
||||
foreach ($mapping as $english => $korean) {
|
||||
DB::table('account_codes')
|
||||
->where('tenant_id', 1)
|
||||
->where('category', $english)
|
||||
->update(['category' => $korean]);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* tenant_id=1(코드브릿지엑스 본사)의 계정과목을 KIS 5자리 표준으로 완전 교체
|
||||
*
|
||||
* - 기존 3자리 코드 체계를 제거하고 서비스 표준(5자리 KIS)으로 통일
|
||||
* - 기존 전표의 account_code 매핑은 수동으로 진행
|
||||
* - 소스: tenant_id=287 (서비스 표준 시드 데이터)
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 소스 테넌트 (서비스 표준)
|
||||
$sourceTenantId = DB::table('account_codes')
|
||||
->where('tenant_id', '!=', 1)
|
||||
->whereRaw('LENGTH(code) = 5')
|
||||
->value('tenant_id');
|
||||
|
||||
if (! $sourceTenantId) {
|
||||
// 소스 테넌트가 없으면 스킵
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. tenant_id=1 기존 데이터 전체 삭제
|
||||
DB::table('account_codes')->where('tenant_id', 1)->delete();
|
||||
|
||||
// 2. 소스 테넌트의 5자리 코드를 tenant_id=1로 복사 (활성 상태)
|
||||
$sourceCodes = DB::table('account_codes')
|
||||
->where('tenant_id', $sourceTenantId)
|
||||
->whereRaw('LENGTH(code) = 5')
|
||||
->get();
|
||||
|
||||
$now = now();
|
||||
$inserts = $sourceCodes->map(fn ($row) => [
|
||||
'tenant_id' => 1,
|
||||
'code' => $row->code,
|
||||
'name' => $row->name,
|
||||
'category' => $row->category,
|
||||
'sub_category' => $row->sub_category,
|
||||
'parent_code' => $row->parent_code,
|
||||
'depth' => $row->depth,
|
||||
'department_type' => $row->department_type,
|
||||
'description' => $row->description,
|
||||
'sort_order' => $row->sort_order,
|
||||
'is_active' => true,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
])->toArray();
|
||||
|
||||
// chunk insert (한 번에 100건씩)
|
||||
foreach (array_chunk($inserts, 100) as $chunk) {
|
||||
DB::table('account_codes')->insert($chunk);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// 롤백 불가 (기존 3자리 데이터 복원 불가능)
|
||||
// 필요 시 백업에서 복원
|
||||
}
|
||||
};
|
||||
@@ -122,6 +122,7 @@
|
||||
'not_found' => 'Work order not found.',
|
||||
'cannot_delete_in_progress' => 'Cannot delete in progress or completed work order.',
|
||||
'not_bending_process' => 'This is not a bending process.',
|
||||
'bending_process_not_found' => 'Bending process not found. Please check process settings.',
|
||||
'invalid_transition' => "Cannot transition status from ':from' to ':to'. Allowed statuses: :allowed",
|
||||
],
|
||||
|
||||
|
||||
@@ -428,6 +428,7 @@
|
||||
'already_created_from_quote' => '이미 해당 견적에서 수주가 생성되었습니다.',
|
||||
'must_be_confirmed_for_production' => '확정 상태의 수주만 생산지시를 생성할 수 있습니다.',
|
||||
'production_order_already_exists' => '이미 생산지시가 존재합니다.',
|
||||
'bending_process_not_found' => '절곡 공정을 찾을 수 없습니다. 공정 설정을 확인해주세요.',
|
||||
'cannot_revert_completed' => '완료된 수주의 생산지시는 되돌릴 수 없습니다.',
|
||||
'cannot_revert_not_confirmed' => '수주확정 상태에서만 되돌리기가 가능합니다.',
|
||||
'cannot_revert_work_order_completed' => '완료 또는 출하된 작업지시는 취소할 수 없습니다.',
|
||||
|
||||
Reference in New Issue
Block a user