Enhance Item Inspection and Outbound Functionality
- Added `apply_process_name` to the item inspection controller, allowing for better clarity in process identification by joining with the `process_mng` table. - Updated the outbound controller to include additional delivery details, such as `delivery_destination_name` and `customer_name`, with fallback logic for improved data accuracy. - Enhanced the query logic to ensure proper handling of delivery addresses and customer information, improving the overall data retrieval process. (TASK: ERP-XXX)
This commit is contained in:
@@ -18,6 +18,7 @@ interface InspectionRow {
|
||||
inspection_item_name: string | null;
|
||||
inspection_method: string | null;
|
||||
apply_process: string | null;
|
||||
apply_process_name: string | null;
|
||||
classification: string | null;
|
||||
pass_criteria: string | null;
|
||||
upper_limit: string | null;
|
||||
@@ -96,16 +97,25 @@ export async function getGroupedList(req: AuthenticatedRequest, res: Response) {
|
||||
}
|
||||
|
||||
// 3) 페이지 item_code들의 모든 검사항목 row
|
||||
// 적용공정(apply_process)은 공정 코드(P001 등)이므로 process_mng JOIN으로 공정명 해석.
|
||||
// 공정 매핑이 깨졌거나 코드가 없으면 apply_process_name 은 NULL → 프론트에서 코드 fallback.
|
||||
const detailQuery = `
|
||||
SELECT
|
||||
id, item_code, item_name, inspection_type, inspection_standard, inspection_standard_id,
|
||||
inspection_item_name, inspection_method, apply_process, classification,
|
||||
pass_criteria, upper_limit, lower_limit, is_required, is_active, manager_id, memo,
|
||||
sort_order, change_record, created_date, updated_date
|
||||
FROM item_inspection_info
|
||||
WHERE company_code = $1
|
||||
AND item_code = ANY($2::text[])
|
||||
ORDER BY item_code, sort_order, created_date;
|
||||
iii.id, iii.item_code, iii.item_name, iii.inspection_type, iii.inspection_standard,
|
||||
iii.inspection_standard_id, iii.inspection_item_name, iii.inspection_method,
|
||||
iii.apply_process,
|
||||
pm.process_name AS apply_process_name,
|
||||
iii.classification,
|
||||
iii.pass_criteria, iii.upper_limit, iii.lower_limit, iii.is_required, iii.is_active,
|
||||
iii.manager_id, iii.memo, iii.sort_order, iii.change_record,
|
||||
iii.created_date, iii.updated_date
|
||||
FROM item_inspection_info iii
|
||||
LEFT JOIN process_mng pm
|
||||
ON pm.process_code = iii.apply_process
|
||||
AND pm.company_code = iii.company_code
|
||||
WHERE iii.company_code = $1
|
||||
AND iii.item_code = ANY($2::text[])
|
||||
ORDER BY iii.item_code, iii.sort_order, iii.created_date;
|
||||
`;
|
||||
const detailRes = await pool.query(detailQuery, [companyCode, itemCodes]);
|
||||
const rowsByItem: Record<string, InspectionRow[]> = {};
|
||||
|
||||
@@ -75,11 +75,45 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
const query = `
|
||||
SELECT
|
||||
om.*,
|
||||
wh.warehouse_name
|
||||
wh.warehouse_name,
|
||||
-- 납품처 (TASK:ERP-097): 출고 자체 납품처 정보 우선,
|
||||
-- 없으면 출처 출하지시(shipment_instruction_detail → shipment_instruction)에서 폴백
|
||||
COALESCE(
|
||||
NULLIF(om.delivery_destination, ''),
|
||||
NULLIF(om.delivery_address, ''),
|
||||
NULLIF(sid.delivery_address, ''),
|
||||
NULLIF(si.delivery_address, ''),
|
||||
''
|
||||
) AS delivery_destination_name,
|
||||
-- 거래처명 (TASK:ERP-098 항목20): 출고 행 customer_name 우선,
|
||||
-- 없으면 customer_mng JOIN, 매핑 깨지면 코드 fallback
|
||||
COALESCE(
|
||||
NULLIF(om.customer_name, ''),
|
||||
NULLIF(c.customer_name, ''),
|
||||
NULLIF(om.customer_code, ''),
|
||||
''
|
||||
) AS customer_name,
|
||||
-- 품목 규격 (TASK:ERP-098 항목19): 유리업종 가로/세로/두께 (item_info JOIN)
|
||||
COALESCE(ii.width::text, '') AS width,
|
||||
COALESCE(ii.height::text, '') AS height,
|
||||
COALESCE(ii.thickness::text, '') AS thickness
|
||||
FROM outbound_mng om
|
||||
LEFT JOIN warehouse_info wh
|
||||
ON om.warehouse_code = wh.warehouse_code
|
||||
AND om.company_code = wh.company_code
|
||||
LEFT JOIN shipment_instruction_detail sid
|
||||
ON om.source_table = 'shipment_instruction_detail'
|
||||
AND om.source_id = sid.id::text
|
||||
AND om.company_code = sid.company_code
|
||||
LEFT JOIN shipment_instruction si
|
||||
ON sid.instruction_id = si.id
|
||||
AND sid.company_code = si.company_code
|
||||
LEFT JOIN customer_mng c
|
||||
ON om.customer_code = c.customer_code
|
||||
AND om.company_code = c.company_code
|
||||
LEFT JOIN item_info ii
|
||||
ON om.item_code = ii.item_number
|
||||
AND om.company_code = ii.company_code
|
||||
${whereClause}
|
||||
ORDER BY om.created_date DESC
|
||||
`;
|
||||
@@ -581,7 +615,17 @@ export async function getShipmentInstructions(
|
||||
let paramIdx = 2;
|
||||
|
||||
if (customer) {
|
||||
conditions.push(`si.partner_id = $${paramIdx}`);
|
||||
// 출하지시(shipment_instruction.partner_id)에는 거래처코드 또는 거래처명이
|
||||
// 혼재 저장될 수 있음(출하지시 등록 화면이 partner_code 미확보 시 거래처명을
|
||||
// fallback 저장). POP 출고는 거래처코드로 조회하므로 코드/명 양쪽 + 거래처
|
||||
// 마스터의 코드/명 모두로 매칭해 누락을 방지한다.
|
||||
conditions.push(
|
||||
`(
|
||||
si.partner_id = $${paramIdx}
|
||||
OR c.customer_code = $${paramIdx}
|
||||
OR c.customer_name = $${paramIdx}
|
||||
)`,
|
||||
);
|
||||
params.push(customer);
|
||||
paramIdx++;
|
||||
}
|
||||
@@ -632,7 +676,7 @@ export async function getShipmentInstructions(
|
||||
ON si.id = sid.instruction_id
|
||||
AND si.company_code = sid.company_code
|
||||
LEFT JOIN customer_mng c
|
||||
ON si.partner_id = c.customer_code
|
||||
ON (si.partner_id = c.customer_code OR si.partner_id = c.customer_name)
|
||||
AND si.company_code = c.company_code
|
||||
LEFT JOIN sales_order_detail sod
|
||||
ON sod.id::text = sid.detail_id::text
|
||||
@@ -641,7 +685,7 @@ export async function getShipmentInstructions(
|
||||
ON ii.item_number = sid.item_code
|
||||
AND ii.company_code = sid.company_code
|
||||
LEFT JOIN customer_item_mapping cim
|
||||
ON cim.customer_id = si.partner_id
|
||||
ON cim.customer_id = COALESCE(c.customer_code, si.partner_id)
|
||||
AND cim.company_code = sid.company_code
|
||||
AND (cim.item_id = sid.item_code OR cim.item_id = ii.id::text)
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
|
||||
@@ -237,8 +237,8 @@ export async function createPkgUnitItem(
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO pkg_unit_item (company_code, pkg_code, item_number, pkg_qty, writer)
|
||||
VALUES ($1,$2,$3,$4,$5)
|
||||
`INSERT INTO pkg_unit_item (id, company_code, pkg_code, item_number, pkg_qty, writer, created_date)
|
||||
VALUES (gen_random_uuid()::text, $1,$2,$3,$4,$5, NOW())
|
||||
RETURNING *`,
|
||||
[companyCode, pkg_code, item_number, pkg_qty, req.user!.userId]
|
||||
);
|
||||
@@ -513,8 +513,8 @@ export async function createLoadingUnitPkg(
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO loading_unit_pkg (company_code, loading_code, pkg_code, max_load_qty, load_method, writer)
|
||||
VALUES ($1,$2,$3,$4,$5,$6)
|
||||
`INSERT INTO loading_unit_pkg (id, company_code, loading_code, pkg_code, max_load_qty, load_method, writer, created_date)
|
||||
VALUES (gen_random_uuid()::text, $1,$2,$3,$4,$5,$6, NOW())
|
||||
RETURNING *`,
|
||||
[companyCode, loading_code, pkg_code, max_load_qty, load_method, req.user!.userId]
|
||||
);
|
||||
|
||||
@@ -3631,6 +3631,11 @@ export const getChecklistItems = async (
|
||||
.json({ success: false, message: "processId 필수" });
|
||||
}
|
||||
|
||||
// 검사기준 마스터(inspection_standard) JOIN — 판단기준(judgment_criteria) 전달용.
|
||||
// - 체크리스트 row 의 inspection_code 는 비어있는 경우가 많아(템플릿 미연결)
|
||||
// 1차로 검사항목명(detail_content/item_title) ↔ inspection_standard.inspection_item 로 매칭,
|
||||
// 2차로 inspection_code 매칭을 fallback 으로 둔다.
|
||||
// - 매칭된 검사기준의 unit/lower_limit/upper_limit 도 보조 노출(체크리스트 row 자체값 우선).
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
pwr.id,
|
||||
@@ -3647,7 +3652,7 @@ export const getChecklistItems = async (
|
||||
pwr.is_required,
|
||||
pwr.inspection_code,
|
||||
pwr.inspection_method,
|
||||
pwr.unit,
|
||||
COALESCE(NULLIF(pwr.unit, ''), ist.unit) AS unit,
|
||||
pwr.lower_limit,
|
||||
pwr.upper_limit,
|
||||
pwr.input_type,
|
||||
@@ -3665,11 +3670,28 @@ export const getChecklistItems = async (
|
||||
pwr.group_paused_at,
|
||||
pwr.group_total_paused_time,
|
||||
pwr.group_completed_at,
|
||||
ist.judgment_criteria
|
||||
ist.judgment_criteria,
|
||||
ist.selection_options,
|
||||
ist.inspection_code AS matched_inspection_code
|
||||
FROM process_work_result pwr
|
||||
LEFT JOIN inspection_standard ist
|
||||
ON pwr.inspection_code = ist.inspection_code
|
||||
AND pwr.company_code = ist.company_code
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT s.judgment_criteria, s.unit, s.inspection_code, s.selection_options
|
||||
FROM inspection_standard s
|
||||
WHERE s.company_code = pwr.company_code
|
||||
AND (
|
||||
(NULLIF(pwr.inspection_code, '') IS NOT NULL
|
||||
AND s.inspection_code = pwr.inspection_code)
|
||||
-- detail_content 에는 UI 구분자(" |") 가 함께 저장된 경우가 있어 양끝 공백/'|' 제거 후 비교
|
||||
OR TRIM(BOTH ' |' FROM COALESCE(s.inspection_item, ''))
|
||||
= TRIM(BOTH ' |' FROM COALESCE(pwr.detail_content, ''))
|
||||
OR TRIM(BOTH ' |' FROM COALESCE(s.inspection_item, ''))
|
||||
= TRIM(BOTH ' |' FROM COALESCE(pwr.item_title, ''))
|
||||
)
|
||||
-- inspection_code 정확매칭을 최우선, 그 다음 항목명 매칭
|
||||
ORDER BY CASE WHEN NULLIF(pwr.inspection_code, '') IS NOT NULL
|
||||
AND s.inspection_code = pwr.inspection_code THEN 0 ELSE 1 END
|
||||
LIMIT 1
|
||||
) ist ON true
|
||||
WHERE pwr.work_order_process_id = $1
|
||||
AND pwr.company_code = $2
|
||||
ORDER BY
|
||||
|
||||
@@ -59,6 +59,9 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
SELECT
|
||||
si.*,
|
||||
COALESCE(c.customer_name, '') AS customer_name,
|
||||
-- 납품처 (TASK:ERP-097): 출하지시 헤더의 납품주소 우선,
|
||||
-- 없으면 거래처 기본 납품처(delivery_destination)로 폴백
|
||||
COALESCE(NULLIF(si.delivery_address, ''), NULLIF(dd.destination_name, ''), '') AS delivery_destination_name,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
@@ -91,8 +94,15 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
WHERE item_number = sid.item_code AND company_code = si.company_code
|
||||
LIMIT 1
|
||||
) i ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT destination_name FROM delivery_destination
|
||||
WHERE customer_code = si.partner_id AND company_code = si.company_code
|
||||
ORDER BY CASE WHEN is_default = 'true' OR is_default = 'Y' OR is_default = '1' THEN 0 ELSE 1 END,
|
||||
created_date ASC NULLS LAST
|
||||
LIMIT 1
|
||||
) dd ON true
|
||||
${where}
|
||||
GROUP BY si.id, c.customer_name
|
||||
GROUP BY si.id, c.customer_name, dd.destination_name
|
||||
ORDER BY si.created_date DESC
|
||||
`;
|
||||
|
||||
|
||||
@@ -257,7 +257,10 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
COALESCE(NULLIF(d.ship_qty,'')::numeric, m.ship_qty, 0) AS shipped_qty,
|
||||
COALESCE(NULLIF(d.width, ''), i.width::text, '') AS width,
|
||||
COALESCE(NULLIF(d.height, ''), i.height::text, '') AS height,
|
||||
COALESCE(NULLIF(d.thickness, ''), i.thickness::text, '') AS thickness
|
||||
COALESCE(NULLIF(d.thickness, ''), i.thickness::text, '') AS thickness,
|
||||
-- 납품처 (TASK:ERP-097): 수주상세/마스터의 납품장소 텍스트 우선,
|
||||
-- 없으면 거래처 기본 납품처(delivery_destination)로 폴백
|
||||
COALESCE(NULLIF(d.delivery_location, ''), NULLIF(m.delivery_address, ''), NULLIF(dd.destination_name, ''), '') AS delivery_destination_name
|
||||
FROM shipment_plan sp
|
||||
LEFT JOIN sales_order_mng m
|
||||
ON sp.sales_order_id = m.id AND sp.company_code = m.company_code
|
||||
@@ -274,6 +277,14 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
LEFT JOIN customer_mng c
|
||||
ON COALESCE(NULLIF(m.partner_id, ''), NULLIF(d.delivery_partner_code, '')) = c.customer_code
|
||||
AND sp.company_code = c.company_code
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT destination_name FROM delivery_destination
|
||||
WHERE customer_code = COALESCE(NULLIF(m.partner_id, ''), NULLIF(d.delivery_partner_code, ''))
|
||||
AND company_code = sp.company_code
|
||||
ORDER BY CASE WHEN is_default = 'true' OR is_default = 'Y' OR is_default = '1' THEN 0 ELSE 1 END,
|
||||
created_date ASC NULLS LAST
|
||||
LIMIT 1
|
||||
) dd ON true
|
||||
${whereClause}
|
||||
ORDER BY sp.created_date DESC
|
||||
`;
|
||||
|
||||
@@ -388,10 +388,36 @@ export async function save(req: AuthenticatedRequest, res: Response) {
|
||||
wiId = insertRes.rows[0].id;
|
||||
}
|
||||
|
||||
// 품목별 기본 라우팅 버전 자동 해석 (TASK:ERP-node-104)
|
||||
// 절단계획 적용 등으로 item.routing 이 비어 들어오는 경로를 위해,
|
||||
// 해당 품목의 활성(기본) 라우팅 버전을 조회해 자동 세팅한다.
|
||||
// - is_default=true 우선, 없으면 최신(created_date DESC) 1건
|
||||
// - 품목에 라우팅 버전이 0건이면 빈값 유지(작업지시 생성은 차단하지 않음)
|
||||
const routingCache = new Map<string, string | null>();
|
||||
const resolveDefaultRouting = async (itemCode: string): Promise<string | null> => {
|
||||
const key = String(itemCode || "").trim();
|
||||
if (!key) return null;
|
||||
if (routingCache.has(key)) return routingCache.get(key) || null;
|
||||
const rv = await client.query(
|
||||
`SELECT id FROM item_routing_version
|
||||
WHERE item_code = $1 AND company_code = $2
|
||||
ORDER BY COALESCE(is_default, false) DESC, created_date DESC
|
||||
LIMIT 1`,
|
||||
[key, companyCode]
|
||||
);
|
||||
const resolved = rv.rowCount && rv.rowCount > 0 ? rv.rows[0].id : null;
|
||||
routingCache.set(key, resolved);
|
||||
return resolved;
|
||||
};
|
||||
|
||||
let totalQty = 0;
|
||||
let firstRouting: string | null = null;
|
||||
for (const item of items) {
|
||||
const itemRouting = item.routing || null;
|
||||
// 프론트가 명시한 routing 우선, 없으면 품목 기본 라우팅 버전 자동 조회
|
||||
let itemRouting: string | null = item.routing || null;
|
||||
if (!itemRouting) {
|
||||
itemRouting = await resolveDefaultRouting(item.itemNumber || item.itemCode || item.partCode || "");
|
||||
}
|
||||
if (!firstRouting && itemRouting) firstRouting = itemRouting;
|
||||
totalQty += Number(item.qty || 0);
|
||||
await client.query(
|
||||
|
||||
Reference in New Issue
Block a user