CoreERP 발주(PO) ↔ 입고(Inbound) 연결 작업 기록

최병현·2026년 2월 22일

coreerp project

목록 보기
14/44
post-thumbnail

이번 작업의 목표는 “업무 흐름이 연결된다”를 백엔드에서 증명하는 것이다. 단순히 발주를 저장하는 수준이 아니라, 입고 확정 시 발주 라인의 입고 누계(qty_received)가 갱신되고,
그 결과로 발주 상태(OPEN / PARTIAL_RECEIVED / RECEIVED)가 자동으로 바뀌는 흐름을 완성했다.


2. 완료 범위

  • Inbound 확정 API에서 poLineId를 optional로 받아 연결 가능하게 구성
  • 입고 확정 시 purchase_order_line.qty_received 증가 처리
  • PO 전체 라인 기준으로 PO 상태 자동 업데이트(OPEN / PARTIAL_RECEIVED / RECEIVED)
  • 실무 사고 방지: poLineId의 품목과 inbound 품목이 일치하는지 검증 추가
  • 재고 스냅샷(Inventory) 반영 + 원장(StockTx) 기록 유지

3. 설계 의도: “연결”은 데이터 무결성으로 증명한다

ERP에서 중요한 건 화면이 아니라 데이터의 흐름이다. 발주가 존재해도 입고가 발주와 연결되지 않으면, 실제 운영에서는 “어떤 발주가 얼마나 들어왔는지”를 추적할 수 없다. 이번 단계는 Inbound 확정이라는 단일 트랜잭션 안에서 아래 3개가 함께 수행되도록 구성했다.

  • 입고 확정 저장(Inbound / InboundLine)
  • 재고 스냅샷 갱신(Inventory 증가)
  • 발주 라인 누계 갱신 + PO 상태 업데이트

4. 핵심 로직: Inbound 확정 시 PO 라인 갱신 + 상태 업데이트

Inbound 확정 로직은 기존대로 재고 반영과 원장 기록을 수행하되, poLineId가 존재하는 경우에만 발주 라인 누계를 갱신하고 PO 상태를 자동 계산하도록 확장했다.

if (line.poLineId() != null) {
    PurchaseOrderLine poLine = purchaseOrderLineRepository.findById(line.poLineId())
            .orElseThrow(() -> new IllegalArgumentException(
                    "존재하지 않는 발주 라인입니다. poLineId=" + line.poLineId()
            ));

    if (!poLine.getItem().getItemId().equals(item.getItemId())) {
        throw new IllegalArgumentException("poLineId의 품목과 inbound 품목이 일치하지 않습니다.");
    }

    poLine.receive(line.qty());
    purchaseOrderLineRepository.save(poLine);

    PurchaseOrder po = poLine.getPurchaseOrder();
    PurchaseOrderStatus next = resolvePoStatus(
            purchaseOrderLineRepository.findByPurchaseOrder_PoId(po.getPoId())
    );

    po.changeStatus(next);
    purchaseOrderRepository.save(po);
}

5. 상태 계산 규칙

PO 상태는 “전체 발주 수량 대비 누적 입고 수량”을 기준으로 계산한다. 실제 운영에서 가장 중요한 건 중간 단계(부분입고)가 자연스럽게 표현되는 것이다.

  • received == 0OPEN
  • 0 < received < orderedPARTIAL_RECEIVED
  • received >= orderedRECEIVED
private PurchaseOrderStatus resolvePoStatus(List&lt;PurchaseOrderLine&gt; lines) {
    int ordered = 0;
    int received = 0;

    for (PurchaseOrderLine l : lines) {
        ordered += l.getQtyOrdered();
        received += l.getQtyReceived();
    }

    if (received == 0) return PurchaseOrderStatus.OPEN;
    if (received < ordered) return PurchaseOrderStatus.PARTIAL_RECEIVED;
    return PurchaseOrderStatus.RECEIVED;
}

6. 테스트 시나리오 (Postman)

테스트는 “실제 존재하는 po_line_id를 사용”하는 게 핵심이다. 기존에 임의로 7, 8 같은 값을 넣으면 400이 나는 게 정상이고, DB에 존재하는 poLineId로 요청하면 연결이 완료된다.


POST /api/inbounds/confirm

{
  "warehouseId": 1,
  "vendorId": 1,
  "memo": "PO 연동 입고",
  "createdBy": 1001,
  "lines": [
    { "itemId": 2, "qty": 10, "poLineId": 2, "memo": "line1" },
    { "itemId": 3, "qty": 5,  "poLineId": 3, "memo": "line2" }
  ]
}

7. 이번 단계의 의미

이번 단계에서 CoreERP는 “데이터가 서로 연결되는 ERP 흐름”으로 넘어왔다. 발주(PO)가 단순 저장 데이터가 아니라, 입고 확정을 통해 누계와 상태가 자동으로 갱신되는 구조가 고정되었다.


8. 다음 단계

  • PO 상세/리스트 조회 API 구성 (PO + Lines)
  • Inbound 상세/리스트 조회 API 구성 (Inbound + Lines + poLineId)
  • 입고/출고/이동의 공통 원장 조회(StockTx History) 화면 연결
  • (선택) poLine.receive에서 초과 입고 방지 규칙 강화

9. 핵심 정리

  • Inbound 확정이 PO 라인과 연결되면 운영 추적 가능성이 급상승한다.
  • qty_received 갱신 + PO 상태 자동 업데이트로 업무 흐름이 이어진다.
  • poLineId ↔ itemId 매칭 검증은 실무 사고를 막는 필수 안전장치다.
profile
Develop

0개의 댓글