구매관리 입고관리 입고등록 + 입고일별 마감정보입력 + 매입마감
- backend purchaseInboundService 신설 — getInboundFormInit / saveInboundForm
(arrival_plan UPSERT 트랜잭션) / saveDeadlineInfo (8필드 일괄 UPDATE) /
closeArrival (이미 마감된 건 차단) + listWarehouseOptions / listAcctCodeOptions
- backend routes — GET /inbound-form/:pomObjid / POST /inbound-form/save /
POST /arrival/deadline / POST /arrival/close + 옵션 2개
- InboundFormDialog 신설 — wace deliveryAcceptanceFormPopUp_new.jsp 1:1
(좌 발주품목 read-only + 우 차수별 입고입력 + 미입고 일괄적용)
- DeadlineInfoDialog 신설 — wace swal 모달 1:1 (8필드 일괄, 단건 시 prefill)
- inbound 페이지 입고등록 / inbound-by-date 마감정보입력+매입마감 연결
- 입고등록 master SELECT 함정 수정 — RPS 에 POM.delivery_status 없어 reception_status fallback
- DataGrid 다중 frozen 누적 left 계산 인프라 추가 (frozenLeftPx props 보강)
— shadcn Table 기반이라 진짜 column pinning 불가 (자연 위치 도달 후 sticky),
입고 3페이지의 frozen 부여는 일단 제거. 진짜 pinning 은 별도 작업
This commit is contained in:
@@ -7,6 +7,7 @@ import { AuthenticatedRequest } from "../types/auth";
|
||||
import * as svc from "../services/purchaseService";
|
||||
import * as formSvc from "../services/purchaseOrderFormService";
|
||||
import * as mailSvc from "../services/purchaseOrderMailService";
|
||||
import * as inboundSvc from "../services/purchaseInboundService";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
function parseFilter(q: Record<string, any>): svc.PurchaseListFilter {
|
||||
@@ -156,6 +157,86 @@ export async function sendPurchaseOrderMail(req: AuthenticatedRequest, res: Resp
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 입고관리 (입고등록 / 마감정보 / 매입마감) ─────────────────
|
||||
|
||||
/** GET /api/purchase/inbound-form/:pomObjid */
|
||||
export async function getInboundFormInit(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const objid = String(req.params.pomObjid ?? "").trim();
|
||||
if (!objid) return res.status(400).json({ success: false, message: "pomObjid required" });
|
||||
const data = await inboundSvc.getInboundFormInit(objid);
|
||||
if (!data) return res.status(404).json({ success: false, message: "발주서를 찾을 수 없어요" });
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
logger.error("입고등록 폼 조회 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
/** POST /api/purchase/inbound-form/save */
|
||||
export async function saveInboundForm(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const { pomObjid, rows } = req.body as { pomObjid: string; rows: inboundSvc.InboundSaveRow[] };
|
||||
if (!pomObjid) return res.status(400).json({ success: false, message: "pomObjid required" });
|
||||
const writer = String(req.user?.userId ?? "");
|
||||
const result = await inboundSvc.saveInboundForm(pomObjid, rows ?? [], writer);
|
||||
return res.json({ success: true, data: result });
|
||||
} catch (e: any) {
|
||||
logger.error("입고등록 저장 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
/** POST /api/purchase/arrival/deadline */
|
||||
export async function saveArrivalDeadline(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const body = req.body as inboundSvc.DeadlineInfoBody;
|
||||
if (!body || !Array.isArray(body.objIds) || body.objIds.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "objIds required" });
|
||||
}
|
||||
const result = await inboundSvc.saveDeadlineInfo(body);
|
||||
return res.json({ success: true, data: result });
|
||||
} catch (e: any) {
|
||||
logger.error("마감정보 저장 실패", { error: e.message });
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
/** GET /api/purchase/options/warehouses */
|
||||
export async function getWarehouses(_req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await inboundSvc.listWarehouseOptions();
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
/** GET /api/purchase/options/acct-codes */
|
||||
export async function getAcctCodes(_req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await inboundSvc.listAcctCodeOptions();
|
||||
return res.json({ success: true, data });
|
||||
} catch (e: any) {
|
||||
return res.status(500).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
/** POST /api/purchase/arrival/close */
|
||||
export async function closeArrival(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const { objIds } = req.body as { objIds: string[] };
|
||||
if (!Array.isArray(objIds) || objIds.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "objIds required" });
|
||||
}
|
||||
const result = await inboundSvc.closeArrival(objIds);
|
||||
return res.json({ success: true, data: result });
|
||||
} catch (e: any) {
|
||||
logger.error("매입마감 처리 실패", { error: e.message });
|
||||
return res.status(400).json({ success: false, message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSuppliers(_req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const data = await svc.listSupplierOptions();
|
||||
|
||||
@@ -28,11 +28,19 @@ router.get ("/order-form/mail-info/:objid", ctrl.getPurchaseOrderMailInfo);
|
||||
router.get ("/order-form/:objid", ctrl.getPurchaseOrderForm); // 수정/조회
|
||||
router.delete("/order-form/:objid", ctrl.deletePurchaseOrderForm); // 삭제 cascade
|
||||
|
||||
// 입고관리 (wace deliveryAcceptanceFormPopUp_new 1:1)
|
||||
router.get ("/inbound-form/:pomObjid", ctrl.getInboundFormInit); // 입고등록 팝업 자동채움
|
||||
router.post("/inbound-form/save", ctrl.saveInboundForm); // arrival_plan 다수 UPSERT
|
||||
router.post("/arrival/deadline", ctrl.saveArrivalDeadline); // 마감정보 일괄 UPDATE
|
||||
router.post("/arrival/close", ctrl.closeArrival); // 매입마감 일괄
|
||||
|
||||
// 공통 옵션
|
||||
router.get("/options/suppliers", ctrl.getSuppliers);
|
||||
router.get("/options/vendors", ctrl.getVendors); // wace client_mng 기반
|
||||
router.get("/options/users", ctrl.getUsers);
|
||||
router.get("/options/projects", ctrl.getProjects);
|
||||
router.get("/options/partner-managers/:partnerObjid", ctrl.getPartnerManagers); // 발주서 메일 담당자
|
||||
router.get("/options/warehouses", ctrl.getWarehouses); // 입고창고 (warehouse_info)
|
||||
router.get("/options/acct-codes", ctrl.getAcctCodes); // 계정과목 (account_code_info)
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
// ============================================================
|
||||
// 구매관리 > 입고관리 — 입고등록 / 마감정보입력 / 매입마감
|
||||
//
|
||||
// wace_plm 1:1 이식:
|
||||
// - 입고등록 팝업: purchaseOrder/deliveryAcceptanceFormPopUp_new.do
|
||||
// - 입고 저장: purchaseOrder/saveDeliveryInfo.do
|
||||
// → supplyChainMgmt.saveDeliveryInfo (ARRIVAL_PLAN UPSERT)
|
||||
// - 마감정보: purchaseOrder/saveArrivalPlanDeadlineInfo.do
|
||||
// → purchaseOrder.saveArrivalPlanDeadlineInfo (8필드 조건부 UPDATE)
|
||||
// - 매입마감: purchaseOrder/purchaseCloseByArrival.do
|
||||
// → purchaseOrder.updateArrivalPlanCloseDate (PURCHASE_CLOSE_DATE)
|
||||
//
|
||||
// RPS 단순화:
|
||||
// - 동시발주(MULTI_YN), inventory_mgmt 동기, ERROR_QTY/ERROR_REASON 흐름은 추후
|
||||
// - 입고등록은 ARRIVAL_PLAN UPSERT 만 처리 (자재 신규/입고 이력은 차후 도메인)
|
||||
// ============================================================
|
||||
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
import { createObjId } from "../utils/objidUtil";
|
||||
|
||||
export interface InboundFormInitResult {
|
||||
master: {
|
||||
pom_objid: string;
|
||||
purchase_order_no: string;
|
||||
project_no: string;
|
||||
contract_mgmt_objid: string;
|
||||
partner_objid: string;
|
||||
partner_name: string;
|
||||
delivery_status: string;
|
||||
};
|
||||
/** 발주 품목 — 입고 등록 팝업 좌측 그리드 */
|
||||
parts: {
|
||||
order_part_objid: string;
|
||||
part_objid: string;
|
||||
part_no: string;
|
||||
part_name: string;
|
||||
spec: string;
|
||||
maker: string;
|
||||
unit_title: string;
|
||||
order_qty: number;
|
||||
arrival_qty: number; // 이미 입고된 수량
|
||||
non_arrival_qty: number; // 미입고 수량 (order_qty - arrival_qty)
|
||||
delivery_request_date: string;
|
||||
}[];
|
||||
/** 기존 입고 차수 — 팝업 우측 그리드 */
|
||||
arrivals: {
|
||||
objid: string;
|
||||
order_part_objid: string;
|
||||
part_objid: string;
|
||||
group_seq: string;
|
||||
seq: string;
|
||||
receipt_date: string;
|
||||
location: string;
|
||||
sub_location: string;
|
||||
receipt_qty: number;
|
||||
arrival_qty: number;
|
||||
inventory_status: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface InboundSaveRow {
|
||||
objid?: string;
|
||||
parent_objid: string; // PURCHASE_ORDER_MASTER objid
|
||||
order_part_objid: string;
|
||||
part_objid: string; // bigint as string OK
|
||||
group_seq: string;
|
||||
seq: string;
|
||||
receipt_date: string;
|
||||
location: string;
|
||||
sub_location: string;
|
||||
receipt_qty: number;
|
||||
arrival_qty: number;
|
||||
arrival_plan_date?: string;
|
||||
}
|
||||
|
||||
export interface DeadlineInfoBody {
|
||||
objIds: string[];
|
||||
taxType: string;
|
||||
taxInvoiceDate?: string;
|
||||
exportDeclNo?: string;
|
||||
loadingDate?: string;
|
||||
foreignType?: string;
|
||||
duty?: string;
|
||||
importVat?: string;
|
||||
exchangeRate?: string;
|
||||
}
|
||||
|
||||
/** GET /api/purchase/inbound-form/:pomObjid — 입고등록 팝업 자동채움 */
|
||||
export async function getInboundFormInit(pomObjid: string): Promise<InboundFormInitResult | null> {
|
||||
const pool = getPool();
|
||||
try {
|
||||
const m = await pool.query(
|
||||
`SELECT POM.OBJID AS pom_objid,
|
||||
POM.PURCHASE_ORDER_NO AS purchase_order_no,
|
||||
POM.CONTRACT_MGMT_OBJID AS contract_mgmt_objid,
|
||||
POM.PARTNER_OBJID AS partner_objid,
|
||||
CM.PROJECT_NO AS project_no,
|
||||
C.CLIENT_NM AS partner_name,
|
||||
COALESCE(POM.RECEPTION_STATUS, '') AS delivery_status
|
||||
FROM PURCHASE_ORDER_MASTER POM
|
||||
LEFT JOIN PROJECT_MGMT CM ON POM.CONTRACT_MGMT_OBJID = CM.OBJID
|
||||
LEFT JOIN CLIENT_MNG C ON C.OBJID = POM.PARTNER_OBJID
|
||||
WHERE POM.OBJID = $1
|
||||
LIMIT 1`,
|
||||
[pomObjid],
|
||||
);
|
||||
if (m.rows.length === 0) return null;
|
||||
const master = m.rows[0];
|
||||
|
||||
const p = await pool.query(
|
||||
`SELECT POP.OBJID AS order_part_objid,
|
||||
POP.PART_OBJID AS part_objid,
|
||||
POP.PART_NO AS part_no,
|
||||
POP.PART_NAME AS part_name,
|
||||
POP.SPEC AS spec,
|
||||
'' AS maker,
|
||||
(SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = POP.UNIT) AS unit_title,
|
||||
COALESCE(NULLIF(POP.ORDER_QTY,'')::numeric, 0) AS order_qty,
|
||||
COALESCE(POP.DELIVERY_REQUEST_DATE, '') AS delivery_request_date,
|
||||
COALESCE((SELECT SUM(COALESCE(NULLIF(AP.RECEIPT_QTY,'')::numeric, 0))
|
||||
FROM ARRIVAL_PLAN AP
|
||||
WHERE AP.ORDER_PART_OBJID = POP.OBJID), 0) AS arrival_qty
|
||||
FROM PURCHASE_ORDER_PART POP
|
||||
WHERE POP.PURCHASE_ORDER_MASTER_OBJID = $1
|
||||
ORDER BY POP.REGDATE`,
|
||||
[pomObjid],
|
||||
);
|
||||
|
||||
const parts = p.rows.map((r) => {
|
||||
const orderQty = Number(r.order_qty || 0);
|
||||
const arrivalQty = Number(r.arrival_qty || 0);
|
||||
return {
|
||||
order_part_objid: String(r.order_part_objid ?? ""),
|
||||
part_objid: String(r.part_objid ?? ""),
|
||||
part_no: String(r.part_no ?? ""),
|
||||
part_name: String(r.part_name ?? ""),
|
||||
spec: String(r.spec ?? ""),
|
||||
maker: String(r.maker ?? ""),
|
||||
unit_title: String(r.unit_title ?? ""),
|
||||
order_qty: orderQty,
|
||||
arrival_qty: arrivalQty,
|
||||
non_arrival_qty: Math.max(orderQty - arrivalQty, 0),
|
||||
delivery_request_date: String(r.delivery_request_date ?? ""),
|
||||
};
|
||||
});
|
||||
|
||||
const a = await pool.query(
|
||||
`SELECT OBJID, ORDER_PART_OBJID, PART_OBJID::VARCHAR, GROUP_SEQ, SEQ,
|
||||
RECEIPT_DATE, LOCATION, SUB_LOCATION,
|
||||
COALESCE(NULLIF(RECEIPT_QTY,'')::numeric, 0) AS receipt_qty,
|
||||
COALESCE(NULLIF(ARRIVAL_QTY,'')::numeric, 0) AS arrival_qty,
|
||||
COALESCE(INVENTORY_STATUS, '') AS inventory_status
|
||||
FROM ARRIVAL_PLAN
|
||||
WHERE PARENT_OBJID = $1
|
||||
ORDER BY GROUP_SEQ, SEQ`,
|
||||
[pomObjid],
|
||||
);
|
||||
|
||||
const arrivals = a.rows.map((r) => ({
|
||||
objid: String(r.objid ?? ""),
|
||||
order_part_objid: String(r.order_part_objid ?? ""),
|
||||
part_objid: String(r.part_objid ?? ""),
|
||||
group_seq: String(r.group_seq ?? "1"),
|
||||
seq: String(r.seq ?? ""),
|
||||
receipt_date: String(r.receipt_date ?? ""),
|
||||
location: String(r.location ?? ""),
|
||||
sub_location: String(r.sub_location ?? ""),
|
||||
receipt_qty: Number(r.receipt_qty || 0),
|
||||
arrival_qty: Number(r.arrival_qty || 0),
|
||||
inventory_status: String(r.inventory_status ?? ""),
|
||||
}));
|
||||
|
||||
return {
|
||||
master: {
|
||||
pom_objid: String(master.pom_objid ?? ""),
|
||||
purchase_order_no: String(master.purchase_order_no ?? ""),
|
||||
project_no: String(master.project_no ?? ""),
|
||||
contract_mgmt_objid: String(master.contract_mgmt_objid ?? ""),
|
||||
partner_objid: String(master.partner_objid ?? ""),
|
||||
partner_name: String(master.partner_name ?? ""),
|
||||
delivery_status: String(master.delivery_status ?? ""),
|
||||
},
|
||||
parts,
|
||||
arrivals,
|
||||
};
|
||||
} catch (e: any) {
|
||||
logger.error("getInboundFormInit 실패", { error: e.message, pomObjid });
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** POST /api/purchase/inbound-form/save — arrival_plan 다수 UPSERT 트랜잭션 */
|
||||
export async function saveInboundForm(
|
||||
pomObjid: string,
|
||||
rows: InboundSaveRow[],
|
||||
writer: string,
|
||||
): Promise<{ saved: number }> {
|
||||
if (!pomObjid) throw new Error("pomObjid is required");
|
||||
if (!Array.isArray(rows)) throw new Error("rows must be array");
|
||||
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
let saved = 0;
|
||||
for (const row of rows) {
|
||||
const qty = Number(row.receipt_qty || 0);
|
||||
// 입고수량 0 인 행은 저장 skip
|
||||
if (qty <= 0) continue;
|
||||
|
||||
const objid = row.objid || createObjId();
|
||||
// ARRIVAL_PLAN UPSERT (wace supplyChainMgmt.saveDeliveryInfo 1:1)
|
||||
await client.query(
|
||||
`INSERT INTO ARRIVAL_PLAN
|
||||
(OBJID, PARENT_OBJID, ORDER_PART_OBJID, PART_OBJID,
|
||||
RECEIPT_QTY, RECEIPT_DATE, LOCATION, SUB_LOCATION,
|
||||
WRITER, RECEIVER_ID, GROUP_SEQ, SEQ, ARRIVAL_QTY, ARRIVAL_PLAN_DATE)
|
||||
VALUES ($1, $2, $3, $4::bigint,
|
||||
$5, $6, $7, $8,
|
||||
$9, $9, $10, $11, $12, $13)
|
||||
ON CONFLICT (OBJID) DO UPDATE SET
|
||||
RECEIPT_QTY = EXCLUDED.RECEIPT_QTY,
|
||||
RECEIPT_DATE = EXCLUDED.RECEIPT_DATE,
|
||||
LOCATION = EXCLUDED.LOCATION,
|
||||
SUB_LOCATION = EXCLUDED.SUB_LOCATION,
|
||||
ARRIVAL_QTY = EXCLUDED.ARRIVAL_QTY,
|
||||
ARRIVAL_PLAN_DATE = EXCLUDED.ARRIVAL_PLAN_DATE,
|
||||
RECEIVER_ID = EXCLUDED.RECEIVER_ID`,
|
||||
[
|
||||
objid,
|
||||
pomObjid,
|
||||
row.order_part_objid,
|
||||
row.part_objid || null,
|
||||
String(qty),
|
||||
row.receipt_date || "",
|
||||
row.location || "",
|
||||
row.sub_location || "",
|
||||
writer || "",
|
||||
row.group_seq || "1",
|
||||
row.seq || "",
|
||||
String(row.arrival_qty || qty),
|
||||
row.arrival_plan_date || "",
|
||||
],
|
||||
);
|
||||
saved++;
|
||||
}
|
||||
await client.query("COMMIT");
|
||||
return { saved };
|
||||
} catch (e: any) {
|
||||
await client.query("ROLLBACK");
|
||||
logger.error("saveInboundForm 실패", { error: e.message, pomObjid });
|
||||
throw e;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/** POST /api/purchase/arrival/deadline — 마감정보 일괄 UPDATE */
|
||||
export async function saveDeadlineInfo(body: DeadlineInfoBody): Promise<{ updated: number }> {
|
||||
if (!Array.isArray(body.objIds) || body.objIds.length === 0) {
|
||||
throw new Error("objIds required");
|
||||
}
|
||||
const pool = getPool();
|
||||
|
||||
// wace 패턴: tax_type 은 항상 SET, 나머지는 비어있지 않을 때만
|
||||
const sets: string[] = [`tax_type = $1`];
|
||||
const vals: any[] = [body.taxType ?? ""];
|
||||
let i = 2;
|
||||
const addIf = (col: string, v?: string) => {
|
||||
if (v != null && v !== "") {
|
||||
sets.push(`${col} = $${i++}`);
|
||||
vals.push(v);
|
||||
}
|
||||
};
|
||||
const addNumIf = (col: string, v?: string) => {
|
||||
if (v != null && v !== "") {
|
||||
sets.push(`${col} = $${i++}::numeric`);
|
||||
vals.push(v);
|
||||
}
|
||||
};
|
||||
addIf("tax_invoice_date", body.taxInvoiceDate);
|
||||
if (body.exportDeclNo != null) { sets.push(`export_decl_no = $${i++}`); vals.push(body.exportDeclNo); }
|
||||
addIf("loading_date", body.loadingDate);
|
||||
addIf("foreign_type", body.foreignType);
|
||||
addNumIf("duty", body.duty);
|
||||
addNumIf("exchange_rate", body.exchangeRate);
|
||||
addNumIf("import_vat", body.importVat);
|
||||
|
||||
const idIdx = i;
|
||||
vals.push(body.objIds);
|
||||
|
||||
const sql = `UPDATE arrival_plan
|
||||
SET ${sets.join(", ")}
|
||||
WHERE OBJID = ANY($${idIdx}::text[])`;
|
||||
try {
|
||||
const r = await pool.query(sql, vals);
|
||||
return { updated: r.rowCount ?? 0 };
|
||||
} catch (e: any) {
|
||||
logger.error("saveDeadlineInfo 실패", { error: e.message });
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** 입고창고 옵션 — RPS warehouse_info 기반 (wace 는 WAREHOUSE_LOCATION) */
|
||||
export async function listWarehouseOptions(): Promise<{ code: string; label: string }[]> {
|
||||
const pool = getPool();
|
||||
try {
|
||||
const r = await pool.query(
|
||||
`SELECT WAREHOUSE_CODE AS code,
|
||||
WAREHOUSE_CODE || ' ' || COALESCE(WAREHOUSE_NAME, '') AS label
|
||||
FROM WAREHOUSE_INFO
|
||||
WHERE COALESCE(STATUS, 'active') NOT IN ('inactive', 'delete', 'D', 'N')
|
||||
AND WAREHOUSE_CODE IS NOT NULL AND WAREHOUSE_CODE <> ''
|
||||
ORDER BY WAREHOUSE_CODE`,
|
||||
);
|
||||
return r.rows;
|
||||
} catch (e: any) {
|
||||
logger.error("listWarehouseOptions 실패", { error: e.message });
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 계정과목 옵션 — RPS account_code_info 기반 (wace 는 ERP_ACCT_CODE) */
|
||||
export async function listAcctCodeOptions(): Promise<{ code: string; label: string }[]> {
|
||||
const pool = getPool();
|
||||
try {
|
||||
const r = await pool.query(
|
||||
`SELECT ACCOUNT_CODE AS code,
|
||||
ACCOUNT_CODE || ' ' || COALESCE(ACCOUNT_NAME, '') AS label
|
||||
FROM ACCOUNT_CODE_INFO
|
||||
WHERE COALESCE(USE_YN, 'Y') IN ('Y', 'y', '1')
|
||||
AND ACCOUNT_CODE IS NOT NULL AND ACCOUNT_CODE <> ''
|
||||
ORDER BY ACCOUNT_CODE`,
|
||||
);
|
||||
return r.rows;
|
||||
} catch (e: any) {
|
||||
logger.error("listAcctCodeOptions 실패", { error: e.message });
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** POST /api/purchase/arrival/close — 매입마감 일괄 (PURCHASE_CLOSE_DATE = 오늘) */
|
||||
export async function closeArrival(objIds: string[]): Promise<{ updated: number }> {
|
||||
if (!Array.isArray(objIds) || objIds.length === 0) {
|
||||
throw new Error("objIds required");
|
||||
}
|
||||
const pool = getPool();
|
||||
try {
|
||||
// 이미 마감된 건 차단 (wace fn_purchaseClose 와 동일)
|
||||
const check = await pool.query(
|
||||
`SELECT OBJID FROM ARRIVAL_PLAN
|
||||
WHERE OBJID = ANY($1::text[])
|
||||
AND COALESCE(PURCHASE_CLOSE_DATE, '') <> ''`,
|
||||
[objIds],
|
||||
);
|
||||
if (check.rows.length > 0) {
|
||||
const dup = check.rows.map((r: any) => r.objid).join(", ");
|
||||
throw new Error(`이미 매입마감된 건이 포함돼 있어요 (${dup})`);
|
||||
}
|
||||
const r = await pool.query(
|
||||
`UPDATE ARRIVAL_PLAN
|
||||
SET PURCHASE_CLOSE_DATE = TO_CHAR(NOW(), 'YYYY-MM-DD')
|
||||
WHERE OBJID = ANY($1::text[])`,
|
||||
[objIds],
|
||||
);
|
||||
return { updated: r.rowCount ?? 0 };
|
||||
} catch (e: any) {
|
||||
logger.error("closeArrival 실패", { error: e.message });
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user