Skip to content

Purchasing & Approvals

How goods get ordered and how changes get signed off. The chain runs min/max → requisition (PB) → purchase order (PO) → goods receipt, with a generic approval workflow gating price, order, min/max, and tag changes.

Table relationships

erDiagram
    tbTr_UsulanOrderSupplier ||--o{ tbTr_PermintaanBarang_D : "suggests"
    tbTr_PermintaanBarang_H  ||--|{ tbTr_PermintaanBarang_D : "header / detail"
    tbTr_PermintaanBarang_H  ||--o{ tbTr_PurchaseOrder_H    : "raises PO"
    tbTr_PurchaseOrder_H     ||--|{ tbTr_PurchaseOrder_D    : "header / detail"
    tbMaster_Supplier        ||--o{ tbTr_PurchaseOrder_H    : "vendor"
    tbMaster_Barang          ||--o{ tbTr_PurchaseOrder_D    : "item"
    tbMaster_HargaBeli       ||--o{ tbTr_PurchaseOrder_D    : "buy price"
    tbTr_PurchaseOrder_H     ||--o{ tbTr_BackOffice_H       : "received by (closes)"
    tbTr_PurchaseOrder_H {
        string POH_KODECABANG PK
        string POH_NOPO PK
        string POH_NOPB FK
        string POH_KODESUP FK
        string POH_RECORDID "status ''->1->2/X"
        double POH_QTYPO
    }
    tbTr_PurchaseOrder_D {
        string POD_NOPO PK
        int    POD_NOURUT PK
        string POD_PRDCD FK
        double POD_QTYPO
        double POD_QTYPB "received"
        double POD_HRGBELI
    }
    tbTr_PermintaanBarang_H {
        string PBH_KODECABANG PK
        string PBH_NOPB PK
        string PBH_JENISPB
        string PBH_RECORDID
    }

The purchasing chain

flowchart LR
    MM["Min/Max on item<br/>PRD_MINSTOCK / PRD_MAXSTOCK"] -->|"stock ≤ min"| UO["Usulan Order<br/>(suggested order)"]
    UO --> PB["Permintaan Barang (PB)<br/>tbTr_PermintaanBarang_H/D"]
    PB --> PO["Purchase Order (PO)<br/>tbTr_PurchaseOrder_H/D"]
    PO --> RCV["Goods Receipt (Penerimaan)<br/>tbTr_BackOffice_H/D + stock"]
    RCV -->|"closes"| PO
    style UO fill:#e3f2fd,stroke:#1976d2
    style RCV fill:#e8f5e9,stroke:#388e3c

1. Suggested order (Usulan Order), driven by min/max

Automatic replenishment (frmProsesPBOtomatis.vbProsesPB()):

  • Selects items where PRD_QTYSTOCK <= PRD_MINSTOCK, not a bundle (PRD_PAKET<>'Y'), and flagged auto-order (tbMaster_Tag.TAG_AUTOORDER='Y'); honours the company PB schedule PRSH_JADWALPB.
  • Order qty = PRD_MAXSTOCK − PRD_QTYSTOCK − PB_HOLD, where PB_HOLD = Σ(POD_QTYPO − POD_QTYPB) over open POs, rounded to multiples of PRD_MINOR.
  • Writes tbTr_PermintaanBarang_H/D (PBH_JENISPB="B", keterangan "PB OTOMATIS").

The suggestion is queued at supplier level in tbTr_UsulanOrderSupplier (UOS_PRDCD, UOS_QTY, UOS_TGLORDER/UOS_TGLPB/UOS_TGLPO, UOS_RECORDID) and, for warehouse proposals, tbTr_UsulanOrderGudang (UOG_KODELOK1UOG_KODELOK2, UOG_QTY). Manual over-max suggestions raise approval "004".

On UOH_/UOD_ naming

Some forms refer to a tbTr_UsulanOrder_H/_D (UOH_/UOD_) header/detail, but there is no typed data-access class for it — the verified proposal tables are the two UOS_/UOG_ ones above. Treat UOS_/UOG_ as authoritative.

2. Requisition (Permintaan Barang, PB)

tbTr_PermintaanBarang_H/D:

Level Key columns
Header PBH_ NOPB (PK), TGLPB, JENISPB ("B" = buy/PO-bound), QTYPB, RECORDID, NOUO, NOPO, inter-branch transfer fields
Detail PBD_ NOPB, NOURUT, PRDCD, QTYPB, HRGBELI, KODESUP, RECORDID

Entry is manual (frmInputPermintaanBarang.vb) or automatic (above). PBD_RECORDID='1' / PBH_RECORDID='1' mark lines converted to a PO.

3. Purchase order (PO)

tbTr_PurchaseOrder_H/D. Raised by ProsesPO() from an entered PB:

  • Doc number via HAKFUNC.GetNomor("PO-<branch>-<yyyyMM>-", 5).
  • Rejects any PB line lacking a buy price in tbMaster_HargaBeli.
  • Groups PB detail by supplier + PRD_DIVISIPO + PRD_BKP, splitting to a new PO every PRM_MAXIMUMITEMPO items; pulls price/PPN/discount/bonus from tv_Data_HargaBeli.
  • Advances the supplier queue (UOS_RECORDID 2→3), updates PRD_LASTPO, prints BuktiPurchaseOrder.
stateDiagram-v2
    [*] --> New: POH_RECORDID = ''
    New --> Sent: '1' (sent to branch/supplier)
    Sent --> Received: '2' (fully received)
    Sent --> Cancelled: 'X' (receiving cancelled)
    Received --> [*]
    note right of Sent
        Open line = POD_QTYPO > POD_QTYPB
    end note

Header key columns (POH_): NOPO (PK), TGLPO, NOPB, KODESUP, KODECABANGUO, QTYPO, GROSS, PPN, TOP, STATUS, RECORDID. Detail (POD_): NOPO, NOURUT, PRDCD, QTYPO, QTYPB (qty received against the line), HRGBELI, AVGSALES, LASTPO, RECORDID.

4. Goods receipt closes the PO

Receiving (frmInputPenerimaanBarang.vb, and the posting Class/PenerimaanBarang.vb) matches the PO by POH_NOPO, receives into tbTr_BackOffice_H/D + tbTr_Receiving, and updates stock/cost (see Transactions). On completion it sets POD_RECORDID='1'; when no open line remains it sets POH_RECORDID='2' and back-fills POH_NOPB with the receipt (BTB) number. Outstanding POs are viewed via tv_DataOutstandingPO (frmTransaksiOutstandingPO.vb).

The approval workflow engine

A single generic engine routes several kinds of change (prices, orders, min/max, tags) through configurable multi-level sign-off.

flowchart TD
    subgraph Config["Config"]
        WF["tbMaster_WorkFlowApproval (WFL_)<br/>APPR_ID + LEVEL → USERID list"]
        AT["tbTabel_Approval<br/>(APPR_ID → name)"]
    end
    subgraph Queue["Runtime"]
        PND["tbTr_PendingApproval (PND_)<br/>APPR_ID + LEVEL waiting"]
        PROP["Proposal rows (*_RECORDID = current level)"]
    end
    Config --> Queue
    PROP -->|"approve at level N"| CHK{"Next level exists?<br/>(WFL_LEVEL = N+1)"}
    CHK -->|Yes| BUMP["RECORDID → N+1; ensure PND row at N+1"]
    CHK -->|No, final| APPLY["Apply change to live master;<br/>delete PND row"]
    style APPLY fill:#e8f5e9,stroke:#388e3c
Piece Role
tbMaster_WorkFlowApproval (WFL_) Config: KODECABANG+APPR_ID+LEVELWFL_USERID (semicolon-delimited approver list). Maintained in frmMasterWorkFlow.vb.
tbTabel_Approval Lookup of approval types (APPR_IDAPPR_NAME).
tbTr_PendingApproval (PND_) The queue: a row means work of this type is waiting at this level.

Routing mechanism (identical across the four Usulan forms): the menu launcher encodes "APPR_ID-LEVEL" into the form's AccessibleName; the form splits it, loads proposal rows whose *_RECORDID equals the current level, and on approve either applies the change to the live master (if it is the final level) or bumps the row to the next level and ensures a tbTr_PendingApproval row exists there. When the grid empties, the PND row for that level is deleted.

Approval IDs seen: 001 Harga Jual Usulan (price, raised on goods receipt), 002/003 (buy price / mutation), 004 Usulan Order over-max. Forms: frmHargaJualUsulan, frmMinMaxStockUsulan, frmTagUsulan, frmUsulanOrder, frmApprovalTargetSales.

Task list is separate from approvals

tbTabel_TaskList (TASK_SQLQUERY, TASK_CRITERIA, TASK_SOUND, TASK_LEADTIME) drives user-defined SQL alerts/reminders on a dashboard — a different mechanism from the PND approval queue.

Approval & pricing relationships

erDiagram
    tbMaster_WorkFlowApproval  ||--o{ tbTr_PendingApproval      : "defines levels"
    tbMaster_HargaJualUsulan   }o--o{ tbTr_PendingApproval      : "queued (APPR 001)"
    tbMaster_MinMaxStockUsulan }o--o{ tbTr_PendingApproval      : "queued (APPR)"
    tbMaster_TagUsulan         }o--o{ tbTr_PendingApproval      : "queued (APPR)"
    tbMaster_HargaJualUsulan   ||--o| tbMaster_HargaJual        : "final approve -> live"
    tbMaster_TagUsulan         ||--o| tbTr_PerubahanTag         : "final approve -> live"
    tbMaster_WorkFlowApproval {
        string WFL_KODECABANG PK
        string WFL_APPR_ID PK
        int    WFL_LEVEL PK
        string WFL_USERID "approver list (;)"
    }
    tbTr_PendingApproval {
        string PND_KODECABANG PK
        string PND_APPR_ID PK
        int    PND_LEVEL PK
    }
    tbMaster_HargaJualUsulan {
        string HGJU_PRDCD PK
        string HGJU_NODOC PK
        string HGJU_RECORDID "current level"
        double HGJU_HRGJUALUSULAN
        string HGJU_APPROVALBY
    }

Pricing

Prices are versioned and (for selling price) approval-gated.

flowchart LR
    subgraph Sell["Selling price"]
        RCV["Goods receipt:<br/>cost changed?"] -->|"PRM_HRGJUALUSULAN=Y"| PROP["tbMaster_HargaJualUsulan (HGJU_)<br/>= cost × (1 + markup)"]
        PROP -->|"approval 001"| LIVE["tbMaster_HargaJual (HGJ_)<br/>+ PRD_HRGJUAL, PRD_HRGJUALOLD"]
    end
    subgraph Buy["Buy price"]
        HB["tbMaster_HargaBeli (HGB_)"] --> HIST["tbHist_HargaBeli (HHGB_)<br/>old → new, supplier change"]
    end
    style PROP fill:#fff3e0,stroke:#fb8c00
    style LIVE fill:#e8f5e9,stroke:#388e3c
Concern Live Proposal History
Selling price tbMaster_HargaJual (HGJ_, RECORDID ''=future / '1'=active); mirror PRD_HRGJUAL/PRD_HRGJUALOLD; wholesale tbMaster_HargaGrosir tbMaster_HargaJualUsulan (HGJU_) → approval 001 (blocked for items on active promo)
Buy price tbMaster_HargaBeli (HGB_, disc/bonus tiers) tbHist_HargaBeli (HHGB_, old/new price + supplier), written from price edits and receipts
Min/Max PRD_MINSTOCK/PRD_MAXSTOCK/PRD_MINOR; master tbMaster_MinMaxStock tbMaster_MinMaxStockUsulan (MINU_) → approval
Price tag tbMaster_Tag (TAG_AUTOORDER); item PRD_KODETAG tbMaster_TagUsulan (TAGU_) → approval → tbTr_PerubahanTag

Selling-price proposals are auto-created on goods receipt when landed cost changes: Class/PenerimaanBarang.vb writes tbHist_HargaBeli and, if PRM_HRGJUALUSULAN="Y", creates a tbMaster_HargaJualUsulan at cost × (100 + PRD_MARKUP)/100 (rounded by PRM_PEMBULATANHRGJUAL) and raises approval 001. This is the link between Transactions and this page: receiving stock can trigger a pricing approval.