Enhance Shipping Order and Plan Functionality

- Updated the shipping order controller to improve customer name retrieval by removing unnecessary partner_id fallback.
- Implemented shipment plan number allocation logic in the shipping plan controller, ensuring unique numbering based on defined rules or fallback mechanisms.
- Enhanced the batch save functionality to include the new shipment plan number in the database insertions.
- Added new state management for production and shipment plans in the Cutting Plan page, allowing for better organization and retrieval of related data.
- Introduced delivery location field in the sales order page, improving data entry for shipping details.

(TASK: ERP-XXX)
This commit is contained in:
kjs
2026-05-12 16:24:33 +09:00
parent 1003273709
commit bd978ff80c
8 changed files with 603 additions and 104 deletions

View File

@@ -58,7 +58,7 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
const query = `
SELECT
si.*,
COALESCE(c.customer_name, si.partner_id, '') AS customer_name,
COALESCE(c.customer_name, '') AS customer_name,
COALESCE(
json_agg(
json_build_object(

View File

@@ -11,6 +11,32 @@ import { Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import { getPool } from "../database/db";
import { logger } from "../utils/logger";
import { numberingRuleService } from "../services/numberingRuleService";
// shipment_plan_no 채번 — 채번규칙 우선, 없으면 SP-YYYYMMDD-NNN fallback
async function allocateShipmentPlanNo(
client: any,
companyCode: string,
planDate: string | null
): Promise<string> {
try {
const rule = await numberingRuleService.getNumberingRuleByColumn(
companyCode, "shipment_plan", "shipment_plan_no"
);
if (rule) {
return await numberingRuleService.allocateCode(
rule.ruleId, companyCode, { plan_date: planDate }
);
}
} catch { /* 채번규칙 조회 실패 시 fallback */ }
const today = new Date().toISOString().split("T")[0].replace(/-/g, "");
const seqRes = await client.query(
`SELECT COUNT(*) + 1 AS seq FROM shipment_plan WHERE company_code = $1 AND shipment_plan_no LIKE $2`,
[companyCode, `SP-${today}-%`]
);
const seq = String(seqRes.rows[0].seq).padStart(3, "0");
return `SP-${today}-${seq}`;
}
// UUID 포맷 감지 (하이픈 포함 36자)
const isUUID = (val: string) =>
@@ -95,7 +121,8 @@ async function getNormalizedOrders(
dueDate: r.due_date || "",
orderQty: Number(r.order_qty || 0),
shipQty: Number(r.ship_qty || 0),
balanceQty: Number(r.balance_qty || 0),
// balance_qty가 NULL/0이면 orderQty - shipQty fallback (수주 등록 시 채워지지 않은 데이터 보정)
balanceQty: Number(r.balance_qty) || (Number(r.order_qty || 0) - Number(r.ship_qty || 0)),
}));
} else {
// 마스터 기준 → 거래처 JOIN
@@ -139,7 +166,8 @@ async function getNormalizedOrders(
dueDate: r.due_date || "",
orderQty: Number(r.order_qty || 0),
shipQty: Number(r.ship_qty || 0),
balanceQty: Number(r.balance_qty || 0),
// balance_qty가 NULL/0이면 orderQty - shipQty fallback (수주 등록 시 채워지지 않은 데이터 보정)
balanceQty: Number(r.balance_qty) || (Number(r.order_qty || 0) - Number(r.ship_qty || 0)),
}));
}
}
@@ -451,10 +479,11 @@ export async function getAggregate(req: AuthenticatedRequest, res: Response) {
.json({ success: false, message: "해당 수주를 찾을 수 없습니다" });
}
// 2) 품목별 그룹핑
// 2) 품목별 그룹핑 — part_code가 비어있으면 detail/master ID 단위로 분리해
// 품번 없는 직접 입력 품목들이 한 그룹으로 병합되지 않도록 한다
const partCodeMap = new Map<string, NormalizedOrder[]>();
for (const order of orders) {
const key = order.partCode || "UNKNOWN";
const key = order.partCode || `__no_part__${order.detailId || order.masterId || Math.random()}`;
if (!partCodeMap.has(key)) partCodeMap.set(key, []);
partCodeMap.get(key)!.push(order);
}
@@ -637,12 +666,13 @@ export async function batchSave(req: AuthenticatedRequest, res: Response) {
);
}
const planNo = await allocateShipmentPlanNo(client, companyCode, planDateValue);
const insertRes = await client.query(
`INSERT INTO shipment_plan
(company_code, detail_id, sales_order_id, plan_qty, plan_date, status, created_by)
VALUES ($1, $2, $3, $4, COALESCE($5::date, CURRENT_DATE), 'READY', $6)
(company_code, shipment_plan_no, detail_id, sales_order_id, plan_qty, plan_date, status, created_by)
VALUES ($1, $2, $3, $4, $5, COALESCE($6::date, CURRENT_DATE), 'READY', $7)
RETURNING *`,
[companyCode, sourceId, detail.master_id, planQty, planDateValue, userId]
[companyCode, planNo, sourceId, detail.master_id, planQty, planDateValue, userId]
);
savedPlans.push(insertRes.rows[0]);
@@ -679,12 +709,13 @@ export async function batchSave(req: AuthenticatedRequest, res: Response) {
);
}
const planNo = await allocateShipmentPlanNo(client, companyCode, planDateValue);
const insertRes = await client.query(
`INSERT INTO shipment_plan
(company_code, sales_order_id, plan_qty, plan_date, status, created_by)
VALUES ($1, $2, $3, COALESCE($4::date, CURRENT_DATE), 'READY', $5)
(company_code, shipment_plan_no, sales_order_id, plan_qty, plan_date, status, created_by)
VALUES ($1, $2, $3, $4, COALESCE($5::date, CURRENT_DATE), 'READY', $6)
RETURNING *`,
[companyCode, masterId, planQty, planDateValue, userId]
[companyCode, planNo, masterId, planQty, planDateValue, userId]
);
savedPlans.push(insertRes.rows[0]);

View File

@@ -30,11 +30,13 @@ export async function getMaterials(companyCode: string, cutType: string) {
GROUP BY item_code
) inv ON inv.item_code = ii.item_number
WHERE ii.company_code = $1
-- division(관리품목) 컬럼에 '원자재' 또는 '구매관리' 라벨이 포함된 품목 매칭
-- (구매관리 품목도 원판으로 취급하라는 사용자 요청, "구매관리,원자재" 다중 등록도 자동 매칭)
AND EXISTS (
SELECT 1 FROM category_values cv
WHERE cv.table_name = 'item_info'
AND cv.column_name = 'division'
AND cv.value_label = '원자재'
AND cv.value_label IN ('원자재', '구매관리')
AND cv.is_active = true
AND (cv.company_code = $1 OR cv.company_code IS NULL OR cv.company_code = '')
AND cv.value_code = ANY(string_to_array(REPLACE(ii.division, ' ', ''), ','))