d23b305990
- getRevenueList SQL: contract_item / user_info / comm_code(CC_RES) / attach_file_info LATERAL 4개 JOIN 추가. receipt_date / payment_type / request_date / customer_request / order_status_name / manager_name / incoterms / cu01_cnt SELECT 보강 - RevenueListRow 타입: 9개 신규 필드 보강 - GRID_COLUMNS 9개 추가: 접수일/유무상/요청납기/고객사요청사항/수주상태/주문서첨부/출하방법/담당자/인도조건 (wace 35/35 일치) - docs/migration/sales/04-revenue-verify.md, scripts/verify-revenue.sql 영업 4개 메뉴 (견적·주문·판매·매출) 모두 구조적 검증 1차 종결. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
457 lines
22 KiB
TypeScript
457 lines
22 KiB
TypeScript
// ============================================================
|
|
// 영업관리 > 판매관리 + 매출관리 (wace_plm 도메인 이식)
|
|
// 메인 테이블: sales_registration (판매 헤더, 프로젝트당 1건)
|
|
// + shipment_log (분할출하/매출 라인)
|
|
// 라인 표시는 contract_item + (sales_registration | shipment_log) JOIN
|
|
// ============================================================
|
|
|
|
import { getPool } from "../database/db";
|
|
import { logger } from "../utils/logger";
|
|
|
|
export interface SaleListFilter {
|
|
orderType?: string; // contract_mgmt.category_cd
|
|
poNo?: string; // contract_mgmt.po_no
|
|
customer_objid?: string;
|
|
search_partObjId?: string;
|
|
search_partName?: string;
|
|
serialNo?: string;
|
|
shippingStatus?: string;
|
|
orderDateFrom?: string;
|
|
orderDateTo?: string;
|
|
shippingDateFrom?: string;
|
|
shippingDateTo?: string;
|
|
salesStatus?: string; // 판매상태 (registered/cancelled 등)
|
|
productType?: string; // project_mgmt.product (제품구분)
|
|
nation?: string; // project_mgmt.area_cd (국내/해외)
|
|
// 매출관리 전용
|
|
revenueMode?: string;
|
|
salesDeadlineFrom?: string;
|
|
salesDeadlineTo?: string;
|
|
}
|
|
|
|
export interface SaleRegisterBody {
|
|
project_no: string; // = contract_mgmt.objid 또는 별도 식별자
|
|
shipping_order_status?: string;
|
|
serial_no?: string;
|
|
sales_quantity?: number;
|
|
sales_unit_price?: number;
|
|
sales_supply_price?: number;
|
|
sales_vat?: number;
|
|
sales_total_amount?: number;
|
|
sales_currency?: string;
|
|
sales_exchange_rate?: number;
|
|
shipping_date?: string;
|
|
shipping_method?: string;
|
|
manager_user_id?: string;
|
|
incoterms?: string;
|
|
has_split_shipment?: boolean;
|
|
}
|
|
|
|
export interface DeadlineInfoBody {
|
|
log_id?: number; // shipment_log.log_id (기존이면 update)
|
|
parent_sale_no: number; // sales_registration.sale_no
|
|
target_objid: string; // contract_mgmt.objid
|
|
sales_deadline_date?: string;
|
|
tax_type?: string;
|
|
tax_invoice_date?: string;
|
|
export_decl_no?: string;
|
|
loading_date?: string;
|
|
sales_slip_date?: string;
|
|
sales_slip_menu_sq?: number;
|
|
remark?: string;
|
|
}
|
|
|
|
// ─── 판매 그리드 (project_mgmt 라인 단위) ─────────────────────
|
|
// project_mgmt가 메인 (수주 확정된 라인 1건당 1행). sales_registration은 LEFT JOIN.
|
|
// wace mapper salesNcollectMgmt.xml getSalesMgmtGridList(line 815~) 패턴.
|
|
|
|
export async function getSaleList(filter: SaleListFilter) {
|
|
const pool = getPool();
|
|
const conditions: string[] = ["T.project_no IS NOT NULL", "T.project_no <> ''"];
|
|
const params: any[] = [];
|
|
let idx = 1;
|
|
|
|
if (filter.orderType) { conditions.push(`T.category_cd = $${idx++}`); params.push(filter.orderType); }
|
|
if (filter.poNo) { conditions.push(`T.po_no ILIKE $${idx++}`); params.push(`%${filter.poNo}%`); }
|
|
if (filter.customer_objid) { conditions.push(`T.customer_objid = $${idx++}`); params.push(filter.customer_objid); }
|
|
if (filter.search_partObjId) { conditions.push(`T.part_objid = $${idx++}`); params.push(filter.search_partObjId); }
|
|
if (filter.serialNo) {
|
|
conditions.push(`EXISTS (SELECT 1 FROM contract_item_serial CIS
|
|
WHERE CIS.item_objid = T.contract_item_objid AND CIS.status='ACTIVE'
|
|
AND UPPER(CIS.serial_no) LIKE UPPER($${idx++}))`);
|
|
params.push(`%${filter.serialNo}%`);
|
|
}
|
|
if (filter.orderDateFrom) { conditions.push(`COALESCE(T.contract_date, CM.order_date) >= $${idx++}`); params.push(filter.orderDateFrom); }
|
|
if (filter.orderDateTo) { conditions.push(`COALESCE(T.contract_date, CM.order_date) <= $${idx++}`); params.push(filter.orderDateTo); }
|
|
if (filter.shippingDateFrom) { conditions.push(`SR.shipping_date >= $${idx++}`); params.push(filter.shippingDateFrom); }
|
|
if (filter.shippingDateTo) { conditions.push(`SR.shipping_date <= $${idx++}`); params.push(filter.shippingDateTo); }
|
|
if (filter.shippingStatus) { conditions.push(`SR.shipping_order_status = $${idx++}`); params.push(filter.shippingStatus); }
|
|
|
|
const where = `WHERE ${conditions.join(" AND ")}`;
|
|
|
|
const sql = `
|
|
SELECT
|
|
T.objid AS project_objid
|
|
,T.project_no AS project_no
|
|
,T.contract_objid
|
|
,T.contract_item_objid
|
|
,T.contract_no AS contract_no
|
|
,T.category_cd AS order_type
|
|
,CC_CAT.code_name AS order_type_name
|
|
,COALESCE(T.contract_date, CM.order_date) AS order_date
|
|
,T.po_no
|
|
,COALESCE(NULLIF(CI.due_date, ''), NULLIF(T.due_date, ''), NULLIF(CM.due_date, '')) AS request_date
|
|
,T.due_date
|
|
,T.customer_objid
|
|
,C.customer_name AS customer
|
|
,T.product AS product_type
|
|
,CC_PRD.code_name AS product_type_name
|
|
,T.area_cd AS nation
|
|
,CC_AREA.code_name AS nation_name
|
|
,T.part_objid
|
|
,T.part_no AS product_no
|
|
,T.part_name AS product_name
|
|
,T.quantity AS order_quantity
|
|
,COALESCE(SR.sales_quantity, 0) AS sales_quantity
|
|
,GREATEST(
|
|
COALESCE(CAST(NULLIF(REPLACE(T.quantity, ',', ''), '') AS NUMERIC), 0)
|
|
- COALESCE(SR.sales_quantity, 0),
|
|
0
|
|
) AS remaining_quantity
|
|
,SR.sale_no
|
|
,SR.sales_unit_price
|
|
,SR.sales_supply_price
|
|
,SR.sales_vat
|
|
,SR.sales_total_amount
|
|
,CASE
|
|
WHEN SR.sales_exchange_rate IS NOT NULL AND SR.sales_exchange_rate <> 0
|
|
THEN ROUND(COALESCE(SR.sales_total_amount, 0) * SR.sales_exchange_rate, 2)
|
|
ELSE COALESCE(SR.sales_total_amount, 0)
|
|
END AS sales_total_amount_krw
|
|
,CASE
|
|
WHEN SR.sales_exchange_rate IS NOT NULL AND SR.sales_exchange_rate <> 0
|
|
THEN ROUND(
|
|
GREATEST(
|
|
COALESCE(CAST(NULLIF(REPLACE(T.quantity, ',', ''), '') AS NUMERIC), 0)
|
|
- COALESCE(SR.sales_quantity, 0),
|
|
0
|
|
) * COALESCE(SR.sales_unit_price, 0) * SR.sales_exchange_rate, 2)
|
|
ELSE 0
|
|
END AS remaining_amount_krw
|
|
/* 환종: sales_registration 우선, 없으면 project_mgmt.contract_currency (wace 패턴) */
|
|
,COALESCE(NULLIF(SR.sales_currency, ''), T.contract_currency) AS sales_currency
|
|
,COALESCE(CC_CUR_S.code_name, CC_CUR.code_name) AS sales_currency_name
|
|
,T.contract_currency
|
|
,CC_CUR.code_name AS contract_currency_name
|
|
,SR.sales_exchange_rate
|
|
,SR.shipping_date
|
|
,SR.shipping_method
|
|
,SR.shipping_order_status
|
|
,SR.manager_user_id
|
|
,U_MGR.user_name AS manager_name
|
|
,SR.incoterms
|
|
,SR.has_split_shipment
|
|
/* S/N: contract_item_serial 집계 우선, 없으면 sales_registration 텍스트 fallback (wace 패턴) */
|
|
,COALESCE(NULLIF(CIS_AGG.serial_list, ''), SR.serial_no) AS serial_no
|
|
,T.contract_result AS order_status
|
|
,CC_RES.code_name AS order_status_name
|
|
/* 판매상태 — wace 로직: 합계가 수주량 이상이면 완판, 일부만 판매면 분할판매, 0이면 미판매 */
|
|
,CASE
|
|
WHEN COALESCE(SR_AGG.sales_qty_sum, 0) = 0 THEN '미판매'
|
|
WHEN COALESCE(SR_AGG.sales_qty_sum, 0) >= COALESCE(CAST(NULLIF(REPLACE(T.quantity, ',', ''), '') AS NUMERIC), 0) THEN '완판'
|
|
WHEN COALESCE(SR_AGG.sales_qty_sum, 0) > 0 THEN '분할판매'
|
|
ELSE ''
|
|
END AS sales_status
|
|
,T.sales_status AS sales_status_raw
|
|
,CM.paid_type AS payment_type
|
|
,CASE WHEN CM.paid_type='paid' THEN '유상' WHEN CM.paid_type='free' THEN '무상' ELSE CM.paid_type END AS payment_type_name
|
|
,CM.receipt_date
|
|
,CM.customer_request
|
|
,T.sales_deadline_date
|
|
,T.regdate
|
|
/* 출하지시상태/생산상태/분할S/N/거래명세서 — 1차 placeholder */
|
|
,NULL::text AS production_status
|
|
,NULL::text AS split_serial_no
|
|
,'N'::text AS has_transaction_statement
|
|
/* 주문서첨부 카운트 — contract_mgmt.objid 기반 (wace 동일) */
|
|
,COALESCE(AF.cu01_cnt, 0) AS cu01_cnt
|
|
FROM project_mgmt T
|
|
LEFT JOIN contract_mgmt CM ON CM.objid = T.contract_objid
|
|
LEFT JOIN contract_item CI ON CI.objid = T.contract_item_objid AND CI.status = 'ACTIVE'
|
|
LEFT JOIN customer_mng C
|
|
ON C.customer_code = CASE WHEN T.customer_objid LIKE 'C_%' THEN substring(T.customer_objid, 3) ELSE T.customer_objid END
|
|
LEFT JOIN sales_registration SR ON SR.project_no = T.project_no
|
|
LEFT JOIN user_info U_MGR ON U_MGR.user_id = SR.manager_user_id
|
|
LEFT JOIN comm_code CC_CAT ON CC_CAT.code_id = T.category_cd AND CC_CAT.status='active'
|
|
LEFT JOIN comm_code CC_AREA ON CC_AREA.code_id = T.area_cd AND CC_AREA.status='active'
|
|
LEFT JOIN comm_code CC_PRD ON CC_PRD.code_id = T.product AND CC_PRD.status='active'
|
|
LEFT JOIN comm_code CC_RES ON CC_RES.code_id = T.contract_result AND CC_RES.status='active'
|
|
LEFT JOIN comm_code CC_CUR ON CC_CUR.code_id = T.contract_currency AND CC_CUR.status='active'
|
|
LEFT JOIN comm_code CC_CUR_S ON CC_CUR_S.code_id = SR.sales_currency AND CC_CUR_S.status='active'
|
|
/* 판매수량 합계 (분할 판매 LIKE 패턴 — wace 동일) */
|
|
LEFT JOIN (
|
|
SELECT SR2.project_no, SUM(SR2.sales_quantity) AS sales_qty_sum
|
|
FROM sales_registration SR2
|
|
GROUP BY SR2.project_no
|
|
) SR_AGG ON SR_AGG.project_no LIKE T.project_no || '%'
|
|
/* contract_item_serial 집계 (S/N 표시) */
|
|
LEFT JOIN (
|
|
SELECT CIS.item_objid,
|
|
STRING_AGG(CIS.serial_no, ',' ORDER BY CIS.seq) AS serial_list
|
|
FROM contract_item_serial CIS
|
|
WHERE UPPER(CIS.status) = 'ACTIVE'
|
|
AND CIS.serial_no IS NOT NULL AND CIS.serial_no != ''
|
|
GROUP BY CIS.item_objid
|
|
) CIS_AGG ON CIS_AGG.item_objid = T.contract_item_objid
|
|
/* 주문서첨부 카운트 (contract_mgmt.objid 기반, doc_type FTC_ORDER/ORDER — wace 동일) */
|
|
LEFT JOIN (
|
|
SELECT target_objid,
|
|
COUNT(*) FILTER (WHERE doc_type IN ('FTC_ORDER','ORDER')) AS cu01_cnt
|
|
FROM attach_file_info
|
|
WHERE UPPER(status) = 'ACTIVE'
|
|
GROUP BY target_objid
|
|
) AF ON AF.target_objid = T.contract_objid
|
|
${where}
|
|
ORDER BY T.regdate DESC NULLS LAST, T.project_no DESC
|
|
`;
|
|
|
|
const res = await pool.query(sql, params);
|
|
logger.info("판매 목록 조회", { count: res.rowCount, filter });
|
|
return res.rows;
|
|
}
|
|
|
|
// ─── 매출 그리드 (shipment_log 기반) ─────────────────────────
|
|
|
|
export async function getRevenueList(filter: SaleListFilter) {
|
|
const pool = getPool();
|
|
const conditions: string[] = [];
|
|
const params: any[] = [];
|
|
let idx = 1;
|
|
|
|
if (filter.orderType) { conditions.push(`T.category_cd = $${idx++}`); params.push(filter.orderType); }
|
|
if (filter.poNo) { conditions.push(`T.po_no ILIKE $${idx++}`); params.push(`%${filter.poNo}%`); }
|
|
if (filter.customer_objid) { conditions.push(`T.customer_objid = $${idx++}`); params.push(filter.customer_objid); }
|
|
if (filter.productType) { conditions.push(`T.product = $${idx++}`); params.push(filter.productType); }
|
|
if (filter.nation) { conditions.push(`T.area_cd = $${idx++}`); params.push(filter.nation); }
|
|
if (filter.search_partObjId) { conditions.push(`T.part_objid = $${idx++}`); params.push(filter.search_partObjId); }
|
|
if (filter.serialNo) {
|
|
conditions.push(`EXISTS (SELECT 1 FROM contract_item_serial CIS
|
|
WHERE CIS.item_objid = T.contract_item_objid AND CIS.status='ACTIVE'
|
|
AND UPPER(CIS.serial_no) LIKE UPPER($${idx++}))`);
|
|
params.push(`%${filter.serialNo}%`);
|
|
}
|
|
if (filter.orderDateFrom) { conditions.push(`COALESCE(T.contract_date, CM.order_date) >= $${idx++}`); params.push(filter.orderDateFrom); }
|
|
if (filter.orderDateTo) { conditions.push(`COALESCE(T.contract_date, CM.order_date) <= $${idx++}`); params.push(filter.orderDateTo); }
|
|
if (filter.salesDeadlineFrom) { conditions.push(`T.sales_deadline_date >= $${idx++}`); params.push(filter.salesDeadlineFrom); }
|
|
if (filter.salesDeadlineTo) { conditions.push(`T.sales_deadline_date <= $${idx++}`); params.push(filter.salesDeadlineTo); }
|
|
if (filter.shippingDateFrom) { conditions.push(`SR.shipping_date >= $${idx++}`); params.push(filter.shippingDateFrom); }
|
|
if (filter.shippingDateTo) { conditions.push(`SR.shipping_date <= $${idx++}`); params.push(filter.shippingDateTo); }
|
|
|
|
conditions.push("T.project_no IS NOT NULL");
|
|
conditions.push("T.project_no <> ''");
|
|
/* wace mapper: shippingDateRequired='Y' — 매출관리는 출하 등록된 프로젝트만 */
|
|
conditions.push(`EXISTS (
|
|
SELECT 1 FROM sales_registration SR_X
|
|
WHERE SR_X.project_no = T.project_no
|
|
AND SR_X.shipping_date IS NOT NULL
|
|
)`);
|
|
const where = `WHERE ${conditions.join(" AND ")}`;
|
|
|
|
const sql = `
|
|
SELECT
|
|
T.objid AS project_objid
|
|
,T.project_no AS project_no
|
|
,T.contract_objid
|
|
,T.contract_no AS contract_no
|
|
,T.category_cd AS order_type
|
|
,CC_CAT.code_name AS order_type_name
|
|
,COALESCE(T.contract_date, CM.order_date) AS order_date
|
|
,T.po_no
|
|
,T.customer_objid
|
|
,C.customer_name AS customer
|
|
,T.product AS product_type
|
|
,CC_PRD.code_name AS product_type_name
|
|
,T.area_cd AS nation
|
|
,CC_AREA.code_name AS nation_name
|
|
,T.part_no AS product_no
|
|
,T.part_name AS product_name
|
|
,T.quantity AS sales_quantity_raw
|
|
,COALESCE(SR.sales_quantity,
|
|
CAST(NULLIF(REPLACE(T.quantity, ',', ''), '') AS NUMERIC), 0) AS sales_quantity
|
|
,SR.sales_unit_price
|
|
,SR.sales_supply_price
|
|
,SR.sales_vat
|
|
,SR.sales_total_amount
|
|
,CASE
|
|
WHEN SR.sales_exchange_rate IS NOT NULL AND SR.sales_exchange_rate <> 0
|
|
THEN ROUND(COALESCE(SR.sales_total_amount, 0) * SR.sales_exchange_rate, 2)
|
|
ELSE COALESCE(SR.sales_total_amount, 0)
|
|
END AS sales_total_amount_krw
|
|
,T.contract_currency
|
|
,CC_CUR.code_name AS contract_currency_name
|
|
,SR.sales_currency
|
|
,CC_CUR_S.code_name AS sales_currency_name
|
|
,SR.sales_exchange_rate
|
|
,SR.shipping_date
|
|
,SR.shipping_method
|
|
,SR.serial_no
|
|
,T.sales_deadline_date
|
|
,T.tax_type
|
|
,T.tax_invoice_date
|
|
,T.export_decl_no
|
|
,T.loading_date
|
|
,T.sales_slip_date
|
|
,T.sales_slip_menu_sq
|
|
,T.sales_status
|
|
/* 분할S/N · 거래명세서 — 1차 placeholder */
|
|
,NULL::text AS split_serial_no
|
|
,'N'::text AS has_transaction_statement
|
|
/* V1 컬럼 보강 (wace 일치) — 접수일/유무상/요청납기/고객사요청사항/수주상태/주문서첨부/담당자/인도조건 */
|
|
,CM.receipt_date AS receipt_date
|
|
,CM.paid_type AS payment_type
|
|
,CASE WHEN CM.paid_type='paid' THEN '유상' WHEN CM.paid_type='free' THEN '무상' ELSE CM.paid_type END AS payment_type_name
|
|
,COALESCE(NULLIF(CI.due_date, ''), NULLIF(T.due_date, ''), NULLIF(CM.due_date, '')) AS request_date
|
|
,CM.customer_request AS customer_request
|
|
,T.contract_result AS order_status
|
|
,CC_RES.code_name AS order_status_name
|
|
,U_MGR.user_name AS manager_name
|
|
,SR.incoterms AS incoterms
|
|
,COALESCE(AF.cu01_cnt, 0) AS cu01_cnt
|
|
FROM project_mgmt T
|
|
LEFT JOIN contract_mgmt CM ON CM.objid = T.contract_objid
|
|
LEFT JOIN contract_item CI ON CI.objid = T.contract_item_objid AND CI.status='ACTIVE'
|
|
LEFT JOIN customer_mng C
|
|
ON C.customer_code = CASE WHEN T.customer_objid LIKE 'C_%' THEN substring(T.customer_objid, 3) ELSE T.customer_objid END
|
|
LEFT JOIN sales_registration SR ON SR.project_no = T.project_no
|
|
LEFT JOIN user_info U_MGR ON U_MGR.user_id = SR.manager_user_id
|
|
LEFT JOIN comm_code CC_CAT ON CC_CAT.code_id = T.category_cd AND CC_CAT.status='active'
|
|
LEFT JOIN comm_code CC_AREA ON CC_AREA.code_id = T.area_cd AND CC_AREA.status='active'
|
|
LEFT JOIN comm_code CC_PRD ON CC_PRD.code_id = T.product AND CC_PRD.status='active'
|
|
LEFT JOIN comm_code CC_RES ON CC_RES.code_id = T.contract_result AND CC_RES.status='active'
|
|
LEFT JOIN comm_code CC_CUR ON CC_CUR.code_id = T.contract_currency AND CC_CUR.status='active'
|
|
LEFT JOIN comm_code CC_CUR_S ON CC_CUR_S.code_id = SR.sales_currency AND CC_CUR_S.status='active'
|
|
/* 주문서첨부 카운트 — contract_mgmt.objid 기반 (wace 동일) */
|
|
LEFT JOIN (
|
|
SELECT target_objid,
|
|
COUNT(*) FILTER (WHERE doc_type IN ('FTC_ORDER','ORDER')) AS cu01_cnt
|
|
FROM attach_file_info WHERE UPPER(status)='ACTIVE'
|
|
GROUP BY target_objid
|
|
) AF ON AF.target_objid = T.contract_objid
|
|
${where}
|
|
ORDER BY T.regdate DESC NULLS LAST, T.project_no DESC
|
|
`;
|
|
|
|
const res = await pool.query(sql, params);
|
|
logger.info("매출 목록 조회", { count: res.rowCount, filter });
|
|
return res.rows;
|
|
}
|
|
|
|
// ─── 판매 등록/수정 (sales_registration upsert) ─────────────
|
|
|
|
export async function registerSale(userId: string, body: SaleRegisterBody) {
|
|
const pool = getPool();
|
|
const sql = `
|
|
INSERT INTO sales_registration (
|
|
project_no, shipping_order_status, serial_no,
|
|
sales_quantity, sales_unit_price, sales_supply_price, sales_vat, sales_total_amount,
|
|
sales_currency, sales_exchange_rate,
|
|
shipping_date, shipping_method, manager_user_id, incoterms,
|
|
has_split_shipment, reg_date, reg_user_id
|
|
) VALUES (
|
|
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,NOW(),$16
|
|
)
|
|
ON CONFLICT (project_no) DO UPDATE SET
|
|
shipping_order_status=EXCLUDED.shipping_order_status,
|
|
serial_no=EXCLUDED.serial_no,
|
|
sales_quantity=EXCLUDED.sales_quantity,
|
|
sales_unit_price=EXCLUDED.sales_unit_price,
|
|
sales_supply_price=EXCLUDED.sales_supply_price,
|
|
sales_vat=EXCLUDED.sales_vat,
|
|
sales_total_amount=EXCLUDED.sales_total_amount,
|
|
sales_currency=EXCLUDED.sales_currency,
|
|
sales_exchange_rate=EXCLUDED.sales_exchange_rate,
|
|
shipping_date=EXCLUDED.shipping_date,
|
|
shipping_method=EXCLUDED.shipping_method,
|
|
manager_user_id=EXCLUDED.manager_user_id,
|
|
incoterms=EXCLUDED.incoterms,
|
|
has_split_shipment=EXCLUDED.has_split_shipment,
|
|
upd_date=NOW(), upd_user_id=$16
|
|
RETURNING sale_no
|
|
`;
|
|
const params = [
|
|
body.project_no, body.shipping_order_status || null, body.serial_no || null,
|
|
body.sales_quantity ?? null, body.sales_unit_price ?? null,
|
|
body.sales_supply_price ?? null, body.sales_vat ?? null, body.sales_total_amount ?? null,
|
|
body.sales_currency || null, body.sales_exchange_rate ?? null,
|
|
body.shipping_date || null, body.shipping_method || null,
|
|
body.manager_user_id || null, body.incoterms || null,
|
|
body.has_split_shipment ?? false, userId,
|
|
];
|
|
const res = await pool.query(sql, params);
|
|
logger.info("판매 등록/수정", { project_no: body.project_no, sale_no: res.rows[0]?.sale_no });
|
|
return res.rows[0];
|
|
}
|
|
|
|
export async function deleteSale(projectNo: string) {
|
|
const pool = getPool();
|
|
await pool.query(`DELETE FROM sales_registration WHERE project_no = $1`, [projectNo]);
|
|
logger.info("판매 삭제", { projectNo });
|
|
}
|
|
|
|
// ─── 매출 마감정보 입력/저장 (shipment_log upsert) ───────────
|
|
|
|
export async function saveDeadlineInfo(userId: string, body: DeadlineInfoBody) {
|
|
const pool = getPool();
|
|
if (body.log_id) {
|
|
// 수정
|
|
await pool.query(
|
|
`UPDATE shipment_log SET
|
|
sales_deadline_date=$2, tax_type=$3, tax_invoice_date=$4,
|
|
export_decl_no=$5, loading_date=$6,
|
|
sales_slip_date=$7, sales_slip_menu_sq=$8,
|
|
remark=$9
|
|
WHERE log_id=$1`,
|
|
[
|
|
body.log_id, body.sales_deadline_date || null, body.tax_type || null,
|
|
body.tax_invoice_date || null, body.export_decl_no || null, body.loading_date || null,
|
|
body.sales_slip_date || null, body.sales_slip_menu_sq ?? null,
|
|
body.remark || null,
|
|
],
|
|
);
|
|
return { log_id: body.log_id };
|
|
}
|
|
// 신규
|
|
const res = await pool.query(
|
|
`INSERT INTO shipment_log (
|
|
target_objid, log_type, parent_sale_no, is_split_record,
|
|
sales_deadline_date, tax_type, tax_invoice_date,
|
|
export_decl_no, loading_date,
|
|
sales_slip_date, sales_slip_menu_sq, remark,
|
|
reg_date, reg_user_id
|
|
) VALUES ($1, 'DEADLINE_INFO', $2, false,
|
|
$3,$4,$5,$6,$7,$8,$9,$10, NOW(), $11)
|
|
RETURNING log_id`,
|
|
[
|
|
body.target_objid, body.parent_sale_no,
|
|
body.sales_deadline_date || null, body.tax_type || null, body.tax_invoice_date || null,
|
|
body.export_decl_no || null, body.loading_date || null,
|
|
body.sales_slip_date || null, body.sales_slip_menu_sq ?? null, body.remark || null,
|
|
userId,
|
|
],
|
|
);
|
|
logger.info("매출 마감정보 등록", { log_id: res.rows[0]?.log_id, target: body.target_objid });
|
|
return res.rows[0];
|
|
}
|
|
|
|
export async function confirmSalesDeadline(_userId: string, logIds: number[]) {
|
|
const pool = getPool();
|
|
// 마감 확정 = sales_deadline_date NOT NULL 로직만 적용. 잠금 컬럼이 별도 없어
|
|
// 추후 별도 컬럼 추가하거나 정책 결정 후 강화. 지금은 단순 Touch.
|
|
await pool.query(
|
|
`UPDATE shipment_log SET log_message=COALESCE(log_message,'') || ' [DEADLINE_CONFIRMED ' || NOW()::text || ']'
|
|
WHERE log_id = ANY($1::int[])`,
|
|
[logIds],
|
|
);
|
|
logger.info("매출 마감 확정", { count: logIds.length });
|
|
}
|