Three-Way Matching & Invoice Verification
Overview
- What you’ll learn:
- The three-way matching concept: Purchase Order, Material Receipt, and Vendor Invoice
- The MMatchPO API: how iDempiere links PO lines to receipt lines and invoice lines
- The MMatchInv API: how iDempiere links receipt lines to invoice lines
- AP invoice processing via MInvoice (IsSOTrx=false)
- MInvoiceLine methods for copying from orders and receipts
- Purchase Price Variance (PPV) calculation and accounting
- Landed cost allocation through invoice lines
- End-to-end document flow with real code examples
- Prerequisites: Lesson 2 — Purchase Orders, Lesson 3 — Material Receipt & Goods Matching
- Estimated reading time: 25 minutes
1. Introduction: The Three-Way Matching Concept
Three-way matching is a fundamental internal control in procurement accounting. It ensures that an organization only pays for goods it actually ordered and received. The three documents involved form a triangle:
- Purchase Order (PO) — what you ordered (quantity and agreed price)
- Material Receipt (MR) — what you physically received (quantity)
- Vendor Invoice (VI) — what the vendor is charging (quantity and price)
When all three documents agree on the product, quantity, and price, the invoice is considered fully verified and can be approved for payment. When they disagree, the system detects the variance and may require management approval before payment is released.
1.1 Why Three-Way Matching Matters
Without three-way matching, an organization is exposed to several risks:
- Overpayment: Paying for goods that were never received
- Price discrepancies: Paying more than the agreed purchase order price
- Quantity discrepancies: Paying for more units than were physically delivered
- Duplicate payments: Paying the same invoice twice because it could not be linked to a specific receipt
- Inventory costing errors: Goods received but never invoiced create “Not Invoiced Receipts” that distort the balance sheet
1.2 iDempiere’s Matching Architecture
iDempiere implements three-way matching through two dedicated matching tables, each with its own document base type used for accounting purposes:
| Matching Table | Class | DocBaseType Constant | Value | Links |
|---|---|---|---|---|
| M_MatchPO | MMatchPO | DOCBASETYPE_MatchPO | “MXP” | PO Line ↔ Receipt Line and/or Invoice Line |
| M_MatchInv | MMatchInv | DOCBASETYPE_MatchInvoice | “MXI” | Receipt Line ↔ Invoice Line |
These constants are defined in the generated class X_C_DocType:
// From X_C_DocType (inherited by MDocType)
public static final String DOCBASETYPE_MatchPO = "MXP";
public static final String DOCBASETYPE_MatchInvoice = "MXI";
public static final String DOCBASETYPE_APInvoice = "API";
public static final String DOCBASETYPE_APCreditMemo = "APC";
Together, M_MatchPO and M_MatchInv form the complete matching picture. A M_MatchPO record can reference a PO line and a receipt line (PO-Receipt match), a PO line and an invoice line (PO-Invoice match), or all three (fully matched). A M_MatchInv record always references both a receipt line and an invoice line (Receipt-Invoice match). When all three records exist for a given product line, the three-way match is complete.
2. MInvoice for Accounts Payable
The MInvoice class serves double duty in iDempiere — it handles both AR (Accounts Receivable) invoices and AP (Accounts Payable) invoices. The critical distinction is the IsSOTrx flag. When IsSOTrx = false, the invoice is an AP invoice (vendor invoice). The class is located at:
org.adempiere.base/src/org/compiere/model/MInvoice.java
2.1 AP Document Base Types
For AP processing, two DocBaseType values are relevant:
| Constant | Value | Description |
|---|---|---|
| DOCBASETYPE_APInvoice | “API” | Standard vendor invoice — creates a payable |
| DOCBASETYPE_APCreditMemo | “APC” | Vendor credit memo — reduces a payable |
The setC_DocTypeTarget_ID(String DocBaseType) method can be used to set the target document type by base type:
// Set Target Document Type by DocBaseType
public void setC_DocTypeTarget_ID(String DocBaseType) {
String sql = "SELECT C_DocType_ID FROM C_DocType "
+ "WHERE AD_Client_ID=? AND AD_Org_ID in (0,?) AND DocBaseType=?"
+ " AND IsActive='Y' "
+ "ORDER BY IsDefault DESC, AD_Org_ID DESC";
int C_DocType_ID = DB.getSQLValueEx(null, sql,
getAD_Client_ID(), getAD_Org_ID(), DocBaseType);
// ...
setC_DocTypeTarget_ID(C_DocType_ID);
boolean isSOTrx = MDocType.DOCBASETYPE_ARInvoice.equals(DocBaseType)
|| MDocType.DOCBASETYPE_ARCreditMemo.equals(DocBaseType);
setIsSOTrx(isSOTrx);
}
When no target is explicitly specified, the default method uses the SOTrx flag:
// Set Target Document Type based on SOTrx flag
public void setC_DocTypeTarget_ID() {
if (getC_DocTypeTarget_ID() > 0)
return;
if (isSOTrx())
setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_ARInvoice);
else
setC_DocTypeTarget_ID(MDocType.DOCBASETYPE_APInvoice);
}
2.2 Constructor from MOrder (Purchase Order)
The most common way to create an AP invoice is from a purchase order. The constructor copies all relevant header fields from the order:
/**
* Create Invoice from Order
* @param order order
* @param C_DocTypeTarget_ID target document type
* @param invoiceDate date or null
*/
public MInvoice(MOrder order, int C_DocTypeTarget_ID, Timestamp invoiceDate) {
this(order.getCtx(), 0, order.get_TrxName());
setClientOrg(order);
setOrder(order); // set base settings
//
if (C_DocTypeTarget_ID <= 0) {
MDocType odt = MDocType.get(order.getC_DocType_ID());
if (odt != null) {
C_DocTypeTarget_ID = odt.getC_DocTypeInvoice_ID();
if (C_DocTypeTarget_ID <= 0)
throw new AdempiereException(
"@NotFound@ @C_DocTypeInvoice_ID@ - @C_DocType_ID@:"
+ odt.get_Translation(MDocType.COLUMNNAME_Name));
}
}
setC_DocTypeTarget_ID(C_DocTypeTarget_ID);
if (invoiceDate != null)
setDateInvoiced(invoiceDate);
setDateAcct(getDateInvoiced());
//
setSalesRep_ID(order.getSalesRep_ID());
//
setC_BPartner_ID(order.getBill_BPartner_ID());
setC_BPartner_Location_ID(order.getBill_Location_ID());
setAD_User_ID(order.getBill_User_ID());
}
The setOrder(MOrder order) method copies detailed settings including price list, currency, conversion type, payment rule, payment terms, PO reference, project, campaign, activity, and dimension fields:
public void setOrder(MOrder order) {
if (order == null) return;
setC_Order_ID(order.getC_Order_ID());
setIsSOTrx(order.isSOTrx());
setIsDiscountPrinted(order.isDiscountPrinted());
setM_PriceList_ID(order.getM_PriceList_ID());
setIsTaxIncluded(order.isTaxIncluded());
setC_Currency_ID(order.getC_Currency_ID());
setC_ConversionType_ID(order.getC_ConversionType_ID());
setPaymentRule(order.getPaymentRule());
setC_PaymentTerm_ID(order.getC_PaymentTerm_ID());
setPOReference(order.getPOReference());
setDescription(order.getDescription());
setDateOrdered(order.getDateOrdered());
// Accounting dimensions
setAD_OrgTrx_ID(order.getAD_OrgTrx_ID());
setC_Project_ID(order.getC_Project_ID());
setC_Campaign_ID(order.getC_Campaign_ID());
setC_Activity_ID(order.getC_Activity_ID());
setUser1_ID(order.getUser1_ID());
setUser2_ID(order.getUser2_ID());
setC_CostCenter_ID(order.getC_CostCenter_ID());
setC_Department_ID(order.getC_Department_ID());
}
2.3 Constructor from MInOut (Material Receipt)
An AP invoice can also be created directly from a material receipt. This is commonly used when the vendor invoice arrives after the goods have already been received:
/**
* Create Invoice from Shipment/Receipt
* @param ship shipment/receipt
* @param invoiceDate date or null
*/
public MInvoice(MInOut ship, Timestamp invoiceDate) {
this(ship.getCtx(), 0, ship.get_TrxName());
setClientOrg(ship);
setShipment(ship); // set base settings
//
setC_DocTypeTarget_ID();
if (invoiceDate != null)
setDateInvoiced(invoiceDate);
setDateAcct(getDateInvoiced());
//
if (getSalesRep_ID() == 0)
setSalesRep_ID(ship.getSalesRep_ID());
}
2.4 setBPartner() — AP-Specific Behavior
A critical detail for AP invoices: the setBPartner() method selects the location based on the transaction type. For sales (IsSOTrx=true) it uses the BillTo location, but for purchasing (IsSOTrx=false) it uses the PayFrom location:
public void setBPartner(MBPartner bp) {
if (bp == null) return;
setC_BPartner_ID(bp.getC_BPartner_ID());
// Payment Term: SO uses customer term, PO uses vendor term
int ii = 0;
if (isSOTrx())
ii = bp.getC_PaymentTerm_ID();
else
ii = bp.getPO_PaymentTerm_ID();
if (ii != 0)
setC_PaymentTerm_ID(ii);
// Price List: SO uses customer list, PO uses vendor list
if (isSOTrx())
ii = bp.getM_PriceList_ID();
else
ii = bp.getPO_PriceList_ID();
if (ii != 0)
setM_PriceList_ID(ii);
// Set Locations - BillTo for SO, PayFrom for PO
MBPartnerLocation[] locs = bp.getLocations(false);
if (locs != null) {
for (int i = 0; i < locs.length; i++) {
if ((locs[i].isBillTo() && isSOTrx())
|| (locs[i].isPayFrom() && !isSOTrx()))
setC_BPartner_Location_ID(
locs[i].getC_BPartner_Location_ID());
}
// fallback to first location
if (getC_BPartner_Location_ID() == 0 && locs.length > 0)
setC_BPartner_Location_ID(
locs[0].getC_BPartner_Location_ID());
}
// ...
}
2.5 completeIt() — Matching Logic for AP Invoices
The most important part of the AP invoice lifecycle is what happens during completeIt(). The method iterates over each invoice line and creates matching records. There are two distinct matching paths:
Path 1: Invoice line has an M_InOutLine_ID (linked to a receipt line) — Creates an MMatchInv record directly:
// Inside MInvoice.completeIt() - for each invoice line:
if (!isSOTrx()
&& line.getM_InOutLine_ID() != 0
&& line.getM_Product_ID() != 0
&& !isReversal())
{
MInOutLine receiptLine = new MInOutLine(getCtx(),
line.getM_InOutLine_ID(), get_TrxName());
MInOut receipt = receiptLine.getParent();
if (receipt.isProcessed()) {
BigDecimal matchQty = isCreditMemo()
? line.getQtyInvoiced().negate()
: line.getQtyInvoiced();
MMatchInv inv = new MMatchInv(line, getDateInvoiced(), matchQty);
if (!inv.save(get_TrxName())) {
m_processMsg = CLogger.retrieveErrorString(
"Could not create Invoice Matching");
return DocAction.STATUS_Invalid;
}
matchInv++;
addDocsPostProcess(inv);
}
}
Path 2: Invoice line has a C_OrderLine_ID (linked to a PO line) — Creates an MMatchPO record, which may in turn auto-create an MMatchInv:
// Inside MInvoice.completeIt() - for PO-linked lines with products:
else if (!isSOTrx()
&& line.getM_Product_ID() != 0
&& !isReversal())
{
BigDecimal matchQty = isCreditMemo()
? line.getQtyInvoiced().negate()
: line.getQtyInvoiced();
MMatchPO po = MMatchPO.create(line, null,
getDateInvoiced(), matchQty);
if (po != null) {
if (!po.save(get_TrxName())) {
m_processMsg = "Could not create PO Matching";
return DocAction.STATUS_Invalid;
}
matchPO++;
if (!po.isPosted())
addDocsPostProcess(po);
}
}
2.6 Key MInvoice Methods Reference
| Method | Return Type | Description |
|---|---|---|
MInvoice(MOrder, int, Timestamp) |
Constructor | Creates AP invoice from purchase order with target doc type and date |
MInvoice(MInOut, Timestamp) |
Constructor | Creates AP invoice from material receipt with date |
setOrder(MOrder) |
void | Copies order header fields (currency, payment terms, PO reference, etc.) |
setShipment(MInOut) |
void | Copies shipment header fields and resolves order linkages |
setBPartner(MBPartner) |
void | Sets partner defaults; uses PayFrom location for AP, BillTo for AR |
setC_DocTypeTarget_ID(String) |
void | Sets target doc type by DocBaseType; auto-sets IsSOTrx |
setC_DocTypeTarget_ID() |
void | Sets default target doc type based on IsSOTrx flag |
prepareIt() |
String | Validates period, lines, and doc type; returns STATUS_InProgress or STATUS_Invalid |
completeIt() |
String | Creates matching records (MMatchPO, MMatchInv), updates order line quantities |
getLines(boolean) |
MInvoiceLine[] | Returns invoice lines, optionally requerying from database |
copyFrom(MInvoice, ...) |
MInvoice (static) | Creates a new invoice by copying from an existing one |
getGrandTotal(boolean) |
BigDecimal | Returns grand total, optionally negated for credit memos |
getNotFullyMatchedToReceipt(...) |
List<MatchingRecord> (static) | Finds AP invoices not fully matched to receipts |
getFullOrPartiallyMatchedToReceipt(...) |
List<MatchingRecord> (static) | Finds AP invoices with full or partial receipt matches |
3. MInvoiceLine Deep Dive
The MInvoiceLine class represents a single line on an invoice. For AP invoice processing, it serves as the bridge between purchase order lines, receipt lines, and the invoice itself. The class is located at:
org.adempiere.base/src/org/compiere/model/MInvoiceLine.java
3.1 setOrderLine() — Copying from a PO Line
The setOrderLine(MOrderLine oLine) method copies pricing and descriptive information from a purchase order line to the invoice line. This is the mechanism that carries the agreed PO price into the invoice for later variance comparison. Note that it does not set quantities — those must be set separately:
/**
* Set values from Order Line. Does not set quantity!
* @param oLine line
*/
public void setOrderLine(MOrderLine oLine) {
setC_OrderLine_ID(oLine.getC_OrderLine_ID());
setLine(oLine.getLine());
setIsDescription(oLine.isDescription());
setDescription(oLine.getDescription());
//
if (oLine.getM_Product_ID() == 0)
setC_Charge_ID(oLine.getC_Charge_ID());
//
setM_Product_ID(oLine.getM_Product_ID());
setM_AttributeSetInstance_ID(oLine.getM_AttributeSetInstance_ID());
setS_ResourceAssignment_ID(oLine.getS_ResourceAssignment_ID());
setC_UOM_ID(oLine.getC_UOM_ID());
// Pricing copied from PO line
setPriceEntered(oLine.getPriceEntered());
setPriceActual(oLine.getPriceActual());
setPriceLimit(oLine.getPriceLimit());
setPriceList(oLine.getPriceList());
//
setC_Tax_ID(oLine.getC_Tax_ID());
setLineNetAmt(oLine.getLineNetAmt());
// Accounting dimensions
setC_Project_ID(oLine.getC_Project_ID());
setC_ProjectPhase_ID(oLine.getC_ProjectPhase_ID());
setC_ProjectTask_ID(oLine.getC_ProjectTask_ID());
setC_Activity_ID(oLine.getC_Activity_ID());
setC_Campaign_ID(oLine.getC_Campaign_ID());
setAD_OrgTrx_ID(oLine.getAD_OrgTrx_ID());
setUser1_ID(oLine.getUser1_ID());
setUser2_ID(oLine.getUser2_ID());
setC_CostCenter_ID(oLine.getC_CostCenter_ID());
setC_Department_ID(oLine.getC_Department_ID());
//
setRRAmt(oLine.getRRAmt());
setRRStartDate(oLine.getRRStartDate());
}
3.2 setShipLine() — Copying from a Receipt Line
The setShipLine(MInOutLine sLine) method copies product, pricing (from the linked order line), and dimension information from a material receipt line. If the receipt line references a PO order line, the pricing comes from the PO; otherwise it falls back to the product's standard price list pricing:
/**
* Set values from Shipment Line. Does not set quantity!
* @param sLine ship line
*/
public void setShipLine(MInOutLine sLine) {
setM_InOutLine_ID(sLine.getM_InOutLine_ID());
setC_OrderLine_ID(sLine.getC_OrderLine_ID());
setM_RMALine_ID(sLine.getM_RMALine_ID());
//
setLine(sLine.getLine());
setIsDescription(sLine.isDescription());
setDescription(sLine.getDescription());
setM_Product_ID(sLine.getM_Product_ID());
if (sLine.sameOrderLineUOM() || getProduct() == null)
setC_UOM_ID(sLine.getC_UOM_ID());
else
setC_UOM_ID(getProduct().getC_UOM_ID());
setM_AttributeSetInstance_ID(sLine.getM_AttributeSetInstance_ID());
//
int C_OrderLine_ID = sLine.getC_OrderLine_ID();
if (C_OrderLine_ID != 0) {
MOrderLine oLine = new MOrderLine(getCtx(),
C_OrderLine_ID, get_TrxName());
// Pricing from PO order line
if (sLine.sameOrderLineUOM())
setPriceEntered(oLine.getPriceEntered());
else
setPriceEntered(oLine.getPriceActual());
setPriceActual(oLine.getPriceActual());
setPriceLimit(oLine.getPriceLimit());
setPriceList(oLine.getPriceList());
setC_Tax_ID(oLine.getC_Tax_ID());
setLineNetAmt(oLine.getLineNetAmt());
setC_Project_ID(oLine.getC_Project_ID());
} else {
// No PO link - use standard pricing
setPrice();
setTax();
}
// Copy accounting dimensions from receipt line
setC_Project_ID(sLine.getC_Project_ID());
setC_ProjectPhase_ID(sLine.getC_ProjectPhase_ID());
setC_ProjectTask_ID(sLine.getC_ProjectTask_ID());
setC_Activity_ID(sLine.getC_Activity_ID());
setC_Campaign_ID(sLine.getC_Campaign_ID());
setAD_OrgTrx_ID(sLine.getAD_OrgTrx_ID());
setUser1_ID(sLine.getUser1_ID());
setUser2_ID(sLine.getUser2_ID());
setC_CostCenter_ID(sLine.getC_CostCenter_ID());
setC_Department_ID(sLine.getC_Department_ID());
}
3.3 getMatchedQty() — Querying Match Status
The getMatchedQty() method queries the M_MatchInv table to determine how much of an invoice line has already been matched to receipt lines:
/**
* @return matched qty
*/
public BigDecimal getMatchedQty() {
String sql = "SELECT COALESCE(SUM(" + MMatchInv.COLUMNNAME_Qty + "),0)"
+ " FROM " + MMatchInv.Table_Name
+ " WHERE " + MMatchInv.COLUMNNAME_C_InvoiceLine_ID + "=?"
+ " AND " + MMatchInv.COLUMNNAME_Processed + "=?";
return DB.getSQLValueBDEx(get_TrxName(), sql,
getC_InvoiceLine_ID(), true);
}
3.4 allocateLandedCosts() — Cost Distribution
The allocateLandedCosts() method is used to distribute freight, customs duties, and other landed costs across received product lines. It supports three distribution modes based on the MLandedCost configuration:
- Entire Receipt: Distributes cost proportionally across all product lines of a receipt
- Single Receipt Line: Allocates the entire cost to a specific receipt line
- Single Product: Allocates cost to a specific product regardless of receipt
/**
* Allocate Landed Costs
* @return error message or ""
*/
public String allocateLandedCosts() {
if (isProcessed())
return "Processed";
// Delete existing allocations
StringBuilder sql = new StringBuilder(
"DELETE FROM C_LandedCostAllocation WHERE C_InvoiceLine_ID=")
.append(getC_InvoiceLine_ID());
int no = DB.executeUpdate(sql.toString(), get_TrxName());
MLandedCost[] lcs = MLandedCost.getLandedCosts(this);
if (lcs.length == 0)
return "";
// For each receipt line, create an allocation record
// proportional to the base (quantity, amount, weight, or volume)
// ...
}
3.5 Static Lookup Methods
MInvoiceLine provides two static methods for finding invoice lines linked to receipt lines:
/**
* Get Invoice Line referencing InOut Line (direct FK link)
* @param sLine shipment line
* @return (first) invoice line
*/
public static MInvoiceLine getOfInOutLine(MInOutLine sLine) {
final String whereClause =
"C_InvoiceLine.M_InOutLine_ID=? AND C_Invoice.Processed='Y'";
// Uses JOIN to C_Invoice for Processed check
// ...
}
/**
* Get Invoice Line referencing InOut Line from M_MatchInv table
* @param sLine shipment line
* @return (first) invoice line
*/
public static MInvoiceLine getOfInOutLineFromMatchInv(MInOutLine sLine) {
final String whereClause =
"C_InvoiceLine_ID IN (SELECT C_InvoiceLine_ID "
+ "FROM M_MatchInv WHERE M_InOutLine_ID=?)";
// ...
}
3.6 Key MInvoiceLine Fields Reference
| Field/Column | Type | Description |
|---|---|---|
| C_InvoiceLine_ID | int (PK) | Primary key of the invoice line |
| C_Invoice_ID | int (FK) | Parent invoice header |
| C_OrderLine_ID | int (FK) | Link to purchase order line (for PO-based invoices) |
| M_InOutLine_ID | int (FK) | Link to material receipt line (for receipt-based invoices) |
| M_Product_ID | int (FK) | Product being invoiced |
| QtyInvoiced | BigDecimal | Quantity invoiced in product UOM |
| QtyEntered | BigDecimal | Quantity entered in entry UOM |
| PriceActual | BigDecimal | Actual unit price (not updateable after set) |
| PriceEntered | BigDecimal | Price entered by user (may differ from PriceActual due to UOM conversion) |
| PriceList | BigDecimal | List price for reference |
| PriceLimit | BigDecimal | Minimum price (for enforcement) |
| LineNetAmt | BigDecimal | Line total = PriceEntered * QtyEntered |
| M_AttributeSetInstance_ID | int (FK) | Attribute set instance (lot, serial number, etc.) |
4. MMatchPO Deep Dive
The MMatchPO class is the central model for PO matching. Every record in M_MatchPO links a purchase order line to either a receipt line, an invoice line, or both. It is located at:
org.adempiere.base/src/org/compiere/model/MMatchPO.java
The class Javadoc summarizes its role:
/**
* Match PO Model.
* Created when processing Shipment or Order
* - Updates Order (delivered, invoiced)
* - Creates PPV acct
*/
public class MMatchPO extends X_M_MatchPO { ... }
4.1 Static Query Methods
MMatchPO provides several static methods for querying existing match records:
/**
* Get PO Match with order line and invoice line
* @param ctx context
* @param C_OrderLine_ID order line
* @param C_InvoiceLine_ID invoice line
* @param trxName transaction
* @return array of matches
*/
public static MMatchPO[] get(Properties ctx,
int C_OrderLine_ID, int C_InvoiceLine_ID, String trxName)
{
if (C_OrderLine_ID == 0 || C_InvoiceLine_ID == 0)
return new MMatchPO[]{};
String sql = "SELECT * FROM M_MatchPO "
+ "WHERE C_OrderLine_ID=? AND C_InvoiceLine_ID=?";
// ...
}
/**
* Get PO Match of Receipt Line
* @param ctx context
* @param M_InOutLine_ID receipt line
* @param trxName transaction
* @return array of matches
*/
public static MMatchPO[] get(Properties ctx,
int M_InOutLine_ID, String trxName)
{
String sql = "SELECT * FROM M_MatchPO WHERE M_InOutLine_ID=?";
// ...
}
/**
* Get PO Matches of receipt (entire document)
*/
public static MMatchPO[] getInOut(Properties ctx,
int M_InOut_ID, String trxName)
{
String sql = "SELECT * FROM M_MatchPO m"
+ " INNER JOIN M_InOutLine l"
+ " ON (m.M_InOutLine_ID=l.M_InOutLine_ID) "
+ "WHERE l.M_InOut_ID=?";
// ...
}
/**
* Get PO Matches of Invoice
*/
public static MMatchPO[] getInvoice(Properties ctx,
int C_Invoice_ID, String trxName)
{
String sql = "SELECT * FROM M_MatchPO mi"
+ " INNER JOIN C_InvoiceLine il"
+ " ON (mi.C_InvoiceLine_ID=il.C_InvoiceLine_ID) "
+ "WHERE il.C_Invoice_ID=?";
// ...
}
/**
* Get PO Matches for OrderLine
*/
public static MMatchPO[] getOrderLine(Properties ctx,
int C_OrderLine_ID, String trxName)
{
String sql = "SELECT * FROM M_MatchPO WHERE C_OrderLine_ID=?";
// ...
}
4.2 Constructors
MMatchPO has two specialized constructors for creating match records from receipt lines and invoice lines respectively:
/**
* Shipment Line Constructor
* @param sLine shipment line
* @param dateTrx optional date
* @param qty matched quantity
*/
public MMatchPO(MInOutLine sLine, Timestamp dateTrx, BigDecimal qty) {
this(sLine.getCtx(), 0, sLine.get_TrxName());
setClientOrg(sLine);
setM_InOutLine_ID(sLine.getM_InOutLine_ID());
setC_OrderLine_ID(sLine.getC_OrderLine_ID());
if (dateTrx != null)
setDateTrx(dateTrx);
setM_Product_ID(sLine.getM_Product_ID());
setM_AttributeSetInstance_ID(sLine.getM_AttributeSetInstance_ID());
setQty(qty);
setProcessed(true); // auto
}
/**
* Invoice Line Constructor
* @param iLine invoice line
* @param dateTrx optional date
* @param qty matched quantity
*/
public MMatchPO(MInvoiceLine iLine, Timestamp dateTrx, BigDecimal qty) {
this(iLine.getCtx(), 0, iLine.get_TrxName());
setClientOrg(iLine);
setC_InvoiceLine_ID(iLine);
if (iLine.getC_OrderLine_ID() != 0)
setC_OrderLine_ID(iLine.getC_OrderLine_ID());
if (dateTrx != null)
setDateTrx(dateTrx);
setM_Product_ID(iLine.getM_Product_ID());
setM_AttributeSetInstance_ID(iLine.getM_AttributeSetInstance_ID());
setQty(qty);
setProcessed(true); // auto
}
4.3 The create() Method — Main Entry Point
The create() method is the primary entry point for creating or updating MMatchPO records. It is called from MInvoice.completeIt() and MInOut.completeIt(). The method has a sophisticated algorithm:
/**
* Update or Create Match PO record
* @param iLine invoice line (may be null)
* @param sLine receipt line (may be null)
* @param dateTrx transaction date
* @param qty qty to match
* @return Match PO Record
*/
public static MMatchPO create(MInvoiceLine iLine,
MInOutLine sLine, Timestamp dateTrx, BigDecimal qty)
{
String trxName = null;
Properties ctx = null;
int C_OrderLine_ID = 0;
// Determine C_OrderLine_ID from iLine or sLine
if (iLine != null) {
trxName = iLine.get_TrxName();
ctx = iLine.getCtx();
C_OrderLine_ID = iLine.getC_OrderLine_ID();
}
if (sLine != null) {
trxName = sLine.get_TrxName();
ctx = sLine.getCtx();
C_OrderLine_ID = sLine.getC_OrderLine_ID();
}
if (C_OrderLine_ID > 0) {
return create(ctx, iLine, sLine, C_OrderLine_ID,
dateTrx, qty, trxName);
} else {
// No order line - try to find via existing MatchPO records
if (sLine != null && iLine != null) {
MMatchPO[] matchpos = MMatchPO.get(ctx,
sLine.getM_InOutLine_ID(), trxName);
for (MMatchPO matchpo : matchpos) {
C_OrderLine_ID = matchpo.getC_OrderLine_ID();
MOrderLine orderLine = new MOrderLine(ctx,
C_OrderLine_ID, trxName);
BigDecimal toInvoice = orderLine.getQtyOrdered()
.subtract(orderLine.getQtyInvoiced());
// ... match up to remaining quantity
}
}
return null;
}
}
4.4 Internal create() Logic — Finding and Updating Existing Matches
The internal create() method (with the full parameter list) implements the "update or create" pattern. It first looks for existing unmatched MMatchPO records through the MatchPOAutoMatch utility class:
protected static MMatchPO create(Properties ctx, MInvoiceLine iLine,
MInOutLine sLine, int C_OrderLine_ID, Timestamp dateTrx,
BigDecimal qty, String trxName)
{
MMatchPO retValue = null;
List<MMatchPO> matchPOList =
MatchPOAutoMatch.getNotMatchedMatchPOList(ctx,
C_OrderLine_ID, trxName);
if (!matchPOList.isEmpty()) {
for (MMatchPO mpo : matchPOList) {
if (qty.compareTo(mpo.getQty()) >= 0) {
// Update existing record with invoice or receipt info
if (iLine != null)
mpo.setC_InvoiceLine_ID(iLine);
if (sLine != null)
mpo.setM_InOutLine_ID(sLine.getM_InOutLine_ID());
// Auto-create MMatchInv when both invoice and receipt
// lines are now present
if ((iLine != null || mpo.getC_InvoiceLine_ID() > 0)
&& (sLine != null || mpo.getM_InOutLine_ID() > 0))
{
int cnt = DB.getSQLValue(mpo.get_TrxName(),
"SELECT Count(*) FROM M_MatchInv "
+ "WHERE M_InOutLine_ID=" + M_InOutLine_ID
+ " AND C_InvoiceLine_ID=" + C_InvoiceLine_ID);
if (cnt <= 0) {
MMatchInv matchInv = createMatchInv(mpo,
C_InvoiceLine_ID, M_InOutLine_ID,
mpo.getQty(), dateTrx, trxName);
mpo.setMatchInvCreated(matchInv);
}
}
mpo.save();
// ...
}
}
}
// If no existing record found, create new
if (retValue == null) {
// Create new MMatchPO from sLine or iLine
// ...
}
return retValue;
}
4.5 createMatchInv() — Auto-Creating Receipt-Invoice Matches
When an MMatchPO record gains both a receipt line and an invoice line reference, the system automatically creates a corresponding MMatchInv record. This is done through the createMatchInv() method, which uses a database savepoint for safe rollback on failure:
/**
* Create MatchInv record
* @param mpo the MatchPO record
* @param C_InvoiceLine_ID invoice line
* @param M_InOutLine_ID receipt line
* @param qty quantity
* @param dateTrx transaction date
* @param trxName transaction
* @return Match Inv record
*/
protected static MMatchInv createMatchInv(MMatchPO mpo,
int C_InvoiceLine_ID, int M_InOutLine_ID,
BigDecimal qty, Timestamp dateTrx, String trxName)
{
Savepoint savepoint = null;
Trx trx = null;
MMatchInv matchInv = null;
try {
trx = trxName != null ? Trx.get(trxName, false) : null;
savepoint = trx != null
? trx.getConnection().setSavepoint() : null;
matchInv = new MMatchInv(mpo.getCtx(), 0, mpo.get_TrxName());
matchInv.setC_InvoiceLine_ID(C_InvoiceLine_ID);
matchInv.setM_Product_ID(mpo.getM_Product_ID());
matchInv.setM_InOutLine_ID(M_InOutLine_ID);
matchInv.setAD_Client_ID(mpo.getAD_Client_ID());
matchInv.setAD_Org_ID(mpo.getAD_Org_ID());
matchInv.setM_AttributeSetInstance_ID(
mpo.getM_AttributeSetInstance_ID());
matchInv.setQty(qty);
matchInv.setDateTrx(dateTrx);
matchInv.setProcessed(true);
if (!matchInv.save()) {
if (savepoint != null) {
trx.getConnection().rollback(savepoint);
savepoint = null;
}
matchInv = null;
}
} catch (Exception e) {
matchInv = null;
} finally {
if (savepoint != null) {
try { trx.getConnection().releaseSavepoint(savepoint); }
catch (Exception e) {}
}
}
return matchInv;
}
4.6 getOrCreate() Pattern
The getOrCreate() method provides a convenient way to find an existing unposted MMatchPO record for a given order line and quantity, or create a new one if none exists. This is used during receipt processing:
/**
* Get or create Match PO record for order line.
* @param C_OrderLine_ID order line
* @param qty quantity
* @param sLine receipt line
* @param trxName transaction
* @return new or existing MMatchPO record
*/
public static MMatchPO getOrCreate(int C_OrderLine_ID,
BigDecimal qty, MInOutLine sLine, String trxName)
{
Query query = new Query(Env.getCtx(), MMatchPO.Table_Name,
"C_OrderLine_ID=? AND Qty=? AND Posted IN (?,?) "
+ "AND M_InOutLine_ID IS NULL", trxName);
MMatchPO matchPO = query.setParameters(
C_OrderLine_ID, qty,
Doc.STATUS_NotPosted, Doc.STATUS_Deferred).first();
if (matchPO != null) {
matchPO.setM_InOutLine_ID(sLine.getM_InOutLine_ID());
return matchPO;
} else {
return new MMatchPO(sLine, null, qty);
}
}
4.7 getInvoicePriceActual() — PPV Calculation Helper
The getInvoicePriceActual() method retrieves the actual invoice price and converts it to the order's currency. This is essential for calculating Purchase Price Variance:
/**
* Get PriceActual from Invoice and convert to Order Currency.
* @return Price Actual in Order Currency
*/
public BigDecimal getInvoicePriceActual() {
MInvoiceLine iLine = getInvoiceLine();
MInvoice invoice = iLine.getParent();
MOrder order = getOrderLine().getParent();
BigDecimal priceActual = iLine.getPriceActual();
int invoiceCurrency_ID = invoice.getC_Currency_ID();
int orderCurrency_ID = order.getC_Currency_ID();
if (invoiceCurrency_ID != orderCurrency_ID) {
priceActual = MConversionRate.convert(getCtx(),
priceActual,
invoiceCurrency_ID, orderCurrency_ID,
invoice.getDateInvoiced(),
invoice.getC_ConversionType_ID(),
getAD_Client_ID(), getAD_Org_ID());
if (priceActual == null)
throw new AdempiereException(
MConversionRateUtil.getErrorMessage(getCtx(),
"ErrorConvertingCurrencyToBaseCurrency",
invoiceCurrency_ID, orderCurrency_ID,
invoice.getC_ConversionType_ID(),
invoice.getDateInvoiced(), get_TrxName()));
}
return priceActual;
}
4.8 reverse() Method
The reverse() method creates a reversal record with negated quantity and handles the complex task of maintaining correct order line quantities and matching state:
/**
* Reverse this MatchPO document.
* @param reversalDate reversal date
* @param reverseMatchingOnly true if MR is not reversed
* @return true if reversed
*/
public boolean reverse(Timestamp reversalDate,
boolean reverseMatchingOnly)
{
if (this.isProcessed() && this.getReversal_ID() == 0) {
MMatchPO reversal = new MMatchPO(getCtx(), 0, get_TrxName());
reversal.setC_InvoiceLine_ID(getC_InvoiceLine_ID());
reversal.setM_InOutLine_ID(getM_InOutLine_ID());
if (getC_OrderLine_ID() != 0)
reversal.setC_OrderLine_ID(getC_OrderLine_ID());
reversal.setM_Product_ID(getM_Product_ID());
reversal.setM_AttributeSetInstance_ID(
getM_AttributeSetInstance_ID());
reversal.setAD_Org_ID(this.getAD_Org_ID());
reversal.setDescription("(->" + this.getDocumentNo() + ")");
reversal.setQty(this.getQty().negate());
reversal.setDateAcct(reversalDate);
reversal.setDateTrx(reversalDate);
reversal.setPosted(false);
reversal.setProcessed(true);
reversal.setReversal_ID(getM_MatchPO_ID());
reversal.saveEx();
this.setDescription("(" + reversal.getDocumentNo() + "<-)");
this.setReversal_ID(reversal.getM_MatchPO_ID());
this.saveEx();
// If reverseMatchingOnly, restore order line reserved qty
// and auto-create new MatchPO for invoice line if needed
// ...
return true;
}
return false;
}
4.9 beforeSave() — PPV and Tolerance Calculation
The beforeSave() method in MMatchPO performs the critical Purchase Price Variance calculation when both a PO line and an invoice line are present:
// In MMatchPO.beforeSave():
// Set PriceMatchDifference to difference between PO and Invoice price
if (getC_OrderLine_ID() != 0
&& getC_InvoiceLine_ID() != 0
&& (newRecord
|| is_ValueChanged("C_OrderLine_ID")
|| is_ValueChanged("C_InvoiceLine_ID")))
{
BigDecimal poPrice = getOrderLine().getPriceActual();
BigDecimal invPrice = getInvoicePriceActual();
BigDecimal difference = poPrice.subtract(invPrice);
if (difference.signum() != 0) {
difference = difference.multiply(getQty());
setPriceMatchDifference(difference);
// Validate against PriceMatchTolerance
MBPGroup group = MBPGroup.getOfBPartner(getCtx(),
getOrderLine().getC_BPartner_ID());
BigDecimal mt = group.getPriceMatchTolerance();
if (mt != null && mt.signum() != 0) {
BigDecimal poAmt = poPrice.multiply(getQty());
BigDecimal maxTolerance = poAmt.multiply(mt)
.abs()
.divide(Env.ONEHUNDRED, 2, RoundingMode.HALF_UP);
difference = difference.abs();
boolean ok = difference.compareTo(maxTolerance) <= 0;
setIsApproved(ok);
}
} else {
setPriceMatchDifference(difference);
setIsApproved(true);
}
}
4.10 afterSave() — Quantity Validation and Order Updates
The afterSave() method performs critical validations and updates the parent order line:
// In MMatchPO.afterSave():
// 1. Validate matched qty against receipt movement qty
if (getM_InOutLine_ID() > 0) {
MInOutLine line = new MInOutLine(getCtx(),
getM_InOutLine_ID(), get_TrxName());
BigDecimal matchedQty = DB.getSQLValueBD(get_TrxName(),
"SELECT Coalesce(SUM(Qty),0) FROM M_MatchPO "
+ "WHERE M_InOutLine_ID=?", getM_InOutLine_ID());
if (matchedQty.compareTo(line.getMovementQty()) > 0) {
throw new IllegalStateException(
"Total matched qty > movement qty.");
}
}
// 2. Validate matched qty against invoice qty
if (getC_InvoiceLine_ID() > 0) {
MInvoiceLine line = new MInvoiceLine(getCtx(),
getC_InvoiceLine_ID(), get_TrxName());
BigDecimal matchedQty = DB.getSQLValueBD(get_TrxName(),
"SELECT Coalesce(SUM(Qty),0) FROM M_MatchPO "
+ "WHERE C_InvoiceLine_ID=? AND Reversal_ID IS NULL",
getC_InvoiceLine_ID());
if (matchedQty.compareTo(line.getQtyInvoiced()) > 0) {
throw new IllegalStateException(
"Total matched qty > invoiced qty.");
}
}
// 3. Update order line QtyDelivered and QtyInvoiced
if (getC_OrderLine_ID() != 0) {
MOrderLine orderLine = getOrderLine();
if (m_isInOutLineChange)
orderLine.setQtyDelivered(
orderLine.getQtyDelivered().add(getQty()));
if (m_isInvoiceLineChange)
orderLine.setQtyInvoiced(
orderLine.getQtyInvoiced().add(getQty()));
return orderLine.save();
}
4.11 Key MMatchPO Methods Reference
| Method | Return Type | Description |
|---|---|---|
get(ctx, C_OrderLine_ID, C_InvoiceLine_ID, trx) |
MMatchPO[] (static) | Get matches by order line and invoice line |
get(ctx, M_InOutLine_ID, trx) |
MMatchPO[] (static) | Get matches by receipt line |
getInOut(ctx, M_InOut_ID, trx) |
MMatchPO[] (static) | Get all matches for a receipt document |
getInvoice(ctx, C_Invoice_ID, trx) |
MMatchPO[] (static) | Get all matches for an invoice document |
getOrderLine(ctx, C_OrderLine_ID, trx) |
MMatchPO[] (static) | Get all matches for an order line |
create(iLine, sLine, dateTrx, qty) |
MMatchPO (static) | Main entry: update or create MatchPO record |
createMatchInv(mpo, ...) |
MMatchInv (static, protected) | Auto-create MatchInv when both receipt and invoice present |
getOrCreate(C_OrderLine_ID, qty, sLine, trx) |
MMatchPO (static) | Find existing unposted match or create new |
getInvoicePriceActual() |
BigDecimal | Invoice price converted to order currency |
reverse(reversalDate) |
boolean | Reverse this match (negated qty) |
reverse(reversalDate, reverseMatchingOnly) |
boolean | Reverse with optional matching-only mode |
getInvoiceLine() |
MInvoiceLine | Get cached or loaded invoice line |
getOrderLine() |
MOrderLine | Get cached or loaded order line |
getMatchInvCreated() |
MMatchInv | Get auto-created MatchInv for immediate posting (one-time read) |
5. MMatchInv Deep Dive
The MMatchInv class handles the Receipt-to-Invoice matching. Each M_MatchInv record links a specific receipt line (M_InOutLine_ID) to a specific invoice line (C_InvoiceLine_ID). The class is located at:
org.adempiere.base/src/org/compiere/model/MMatchInv.java
The class Javadoc summarizes its accounting role:
/**
* Match Invoice (Receipt<>Invoice) Model.
* Accounting:
* - Not Invoiced Receipts (relief)
* - IPV (Invoice Price Variance)
*/
public class MMatchInv extends X_M_MatchInv { ... }
5.1 Static Query Methods
/**
* Get InOut-Invoice Matches by both IDs
* @param ctx context
* @param M_InOutLine_ID shipment line
* @param C_InvoiceLine_ID invoice line
* @param trxName transaction
* @return array of matches
*/
public static MMatchInv[] get(Properties ctx,
int M_InOutLine_ID, int C_InvoiceLine_ID, String trxName)
{
if (M_InOutLine_ID <= 0 || C_InvoiceLine_ID <= 0)
return new MMatchInv[]{};
final String whereClause =
"M_InOutLine_ID=? AND C_InvoiceLine_ID=?";
List<MMatchInv> list = new Query(ctx,
I_M_MatchInv.Table_Name, whereClause, trxName)
.setParameters(M_InOutLine_ID, C_InvoiceLine_ID)
.list();
return list.toArray(new MMatchInv[list.size()]);
}
/**
* Get InOut Matches for an InvoiceLine
*/
public static MMatchInv[] getInvoiceLine(Properties ctx,
int C_InvoiceLine_ID, String trxName)
{
if (C_InvoiceLine_ID <= 0)
return new MMatchInv[]{};
String whereClause = "C_InvoiceLine_ID=?";
List<MMatchInv> list = new Query(ctx,
I_M_MatchInv.Table_Name, whereClause, trxName)
.setParameters(C_InvoiceLine_ID)
.list();
return list.toArray(new MMatchInv[list.size()]);
}
/**
* Get Invoice Matches for an InOutLine
*/
public static MMatchInv[] getInOutLine(Properties ctx,
int M_InOutLine_ID, String trxName)
{
if (M_InOutLine_ID <= 0)
return new MMatchInv[]{};
final String whereClause =
MMatchInv.COLUMNNAME_M_InOutLine_ID + "=?";
List<MMatchInv> list = new Query(ctx,
I_M_MatchInv.Table_Name, whereClause, trxName)
.setParameters(M_InOutLine_ID)
.list();
return list.toArray(new MMatchInv[list.size()]);
}
/**
* Get InOut Matches for an Invoice
*/
public static MMatchInv[] getInvoice(Properties ctx,
int C_Invoice_ID, String trxName)
{
if (C_Invoice_ID == 0)
return new MMatchInv[]{};
final String whereClause =
" EXISTS (SELECT 1 FROM C_InvoiceLine il"
+ " WHERE M_MatchInv.C_InvoiceLine_ID"
+ "=il.C_InvoiceLine_ID AND il.C_Invoice_ID=?)";
List<MMatchInv> list = new Query(ctx,
I_M_MatchInv.Table_Name, whereClause, trxName)
.setParameters(C_Invoice_ID)
.setOrderBy(COLUMNNAME_ProcessedOn)
.list();
return list.toArray(new MMatchInv[list.size()]);
}
/**
* Get InOut Matches for Invoice filtered by DateAcct
*/
public static MMatchInv[] getInvoiceByDateAcct(Properties ctx,
int C_Invoice_ID, Timestamp DateAcct, String trxName)
{
// Uses raw SQL with JOIN to reversal for date ordering
// Filters by DateAcct >= parameter
// Orders by DateAcct, reversal grouping, M_MatchInv_ID
// ...
}
5.2 Invoice Line Constructor
The constructor from an MInvoiceLine automatically sets the receipt line reference and marks the record as processed:
/**
* Invoice Line Constructor
* @param iLine invoice line
* @param dateTrx optional date
* @param qty matched quantity
*/
public MMatchInv(MInvoiceLine iLine, Timestamp dateTrx, BigDecimal qty) {
this(iLine.getCtx(), 0, iLine.get_TrxName());
setClientOrg(iLine);
setC_InvoiceLine_ID(iLine.getC_InvoiceLine_ID());
setM_InOutLine_ID(iLine.getM_InOutLine_ID());
if (dateTrx != null)
setDateTrx(dateTrx);
setM_Product_ID(iLine.getM_Product_ID());
setM_AttributeSetInstance_ID(
iLine.getM_AttributeSetInstance_ID());
setQty(qty);
setProcessed(true); // auto
}
5.3 beforeSave() — Date Accounting Logic
The beforeSave() method sets the accounting date to the later of the invoice date or receipt date. This ensures the matching entry is recorded in the correct accounting period:
@Override
protected boolean beforeSave(boolean newRecord) {
if (getDateTrx() == null)
setDateTrx(new Timestamp(System.currentTimeMillis()));
if (getDateAcct() == null) {
Timestamp ts = getNewerDateAcct();
if (ts == null)
ts = getDateTrx();
setDateAcct(ts);
}
// Set ASI from receipt line if not set
if (getM_AttributeSetInstance_ID() == 0
&& getM_InOutLine_ID() != 0)
{
MInOutLine iol = new MInOutLine(getCtx(),
getM_InOutLine_ID(), get_TrxName());
setM_AttributeSetInstance_ID(
iol.getM_AttributeSetInstance_ID());
}
return true;
}
/**
* Get the newer DateAcct between invoice and shipment
*/
public Timestamp getNewerDateAcct() {
// Query invoice DateAcct via C_InvoiceLine JOIN C_Invoice
Timestamp invoiceDate = ...;
// Query receipt DateAcct via M_InOutLine JOIN M_InOut
Timestamp shipDate = ...;
if (invoiceDate == null) return shipDate;
if (shipDate == null) return invoiceDate;
if (invoiceDate.after(shipDate))
return invoiceDate;
return shipDate;
}
5.4 afterSave() — Quantity Validation
After saving, MMatchInv validates that the total matched quantity does not exceed the receipt's movement quantity or the invoice line's invoiced quantity:
@Override
protected boolean afterSave(boolean newRecord, boolean success) {
if (!success) return false;
// Validate: total matched qty <= receipt movement qty
if (getM_InOutLine_ID() > 0) {
MInOutLine line = new MInOutLine(getCtx(),
getM_InOutLine_ID(), get_TrxName());
BigDecimal matchedQty = DB.getSQLValueBD(get_TrxName(),
"SELECT Coalesce(SUM(Qty),0) FROM M_MatchInv "
+ "WHERE M_InOutLine_ID=?", getM_InOutLine_ID());
BigDecimal movementQty = line.getMovementQty();
// Handle negative movement qty (returns)
if (movementQty.signum() < 0) {
movementQty = movementQty.negate();
matchedQty = matchedQty.negate();
}
if (matchedQty != null
&& matchedQty.compareTo(movementQty) > 0)
{
throw new IllegalStateException(
"Total matched qty > movement qty.");
}
}
// Validate: total matched qty <= invoice line qty
if (getC_InvoiceLine_ID() > 0) {
MInvoiceLine line = new MInvoiceLine(getCtx(),
getC_InvoiceLine_ID(), get_TrxName());
BigDecimal matchedQty = DB.getSQLValueBD(get_TrxName(),
"SELECT Coalesce(SUM(Qty),0) FROM M_MatchInv "
+ "WHERE C_InvoiceLine_ID=?",
getC_InvoiceLine_ID());
BigDecimal qtyInvoiced = line.getQtyInvoiced();
if (matchedQty != null
&& matchedQty.compareTo(qtyInvoiced) > 0)
{
throw new IllegalStateException(
"Total matched qty > invoiced qty.");
}
}
return true;
}
5.5 reverse() Method
/**
* Reverse this MatchInv document.
* @param reversalDate reversal date
* @return true if reversed
*/
public boolean reverse(Timestamp reversalDate) {
if (this.isProcessed() && this.getReversal_ID() == 0) {
MMatchInv reversal = new MMatchInv(getCtx(),
0, get_TrxName());
PO.copyValues(this, reversal);
reversal.setAD_Org_ID(this.getAD_Org_ID());
reversal.setDescription(
"(->" + this.getDocumentNo() + ")");
reversal.setQty(this.getQty().negate());
reversal.setDateAcct(reversalDate);
reversal.setDateTrx(reversalDate);
reversal.set_ValueNoCheck("DocumentNo", null);
reversal.setPosted(false);
reversal.setReversal_ID(getM_MatchInv_ID());
reversal.saveEx();
this.setDescription(
"(" + reversal.getDocumentNo() + "<-)");
this.setReversal_ID(reversal.getM_MatchInv_ID());
this.saveEx();
return true;
}
return false;
}
5.6 beforeDelete() — Period and Posting Check
@Override
protected boolean beforeDelete() {
if (isPosted()) {
MPeriod.testPeriodOpen(getCtx(), getDateTrx(),
MDocType.DOCBASETYPE_MatchInvoice, getAD_Org_ID());
setPosted(false);
MFactAcct.deleteEx(Table_ID, get_ID(), get_TrxName());
}
return true;
}
5.7 Key MMatchInv Methods Reference
| Method | Return Type | Description |
|---|---|---|
get(ctx, M_InOutLine_ID, C_InvoiceLine_ID, trx) |
MMatchInv[] (static) | Get matches by both receipt and invoice line |
getInvoiceLine(ctx, C_InvoiceLine_ID, trx) |
MMatchInv[] (static) | Get all matches for an invoice line |
getInOutLine(ctx, M_InOutLine_ID, trx) |
MMatchInv[] (static) | Get all matches for a receipt line |
getInvoice(ctx, C_Invoice_ID, trx) |
MMatchInv[] (static) | Get all matches for an invoice document |
getInOut(ctx, M_InOut_ID, trx) |
MMatchInv[] (static) | Get all matches for a receipt document |
getInvoiceByDateAcct(ctx, C_Invoice_ID, DateAcct, trx) |
MMatchInv[] (static) | Get matches for invoice filtered by accounting date |
MMatchInv(MInvoiceLine, Timestamp, BigDecimal) |
Constructor | Create from invoice line with date and qty |
getNewerDateAcct() |
Timestamp | Returns the later DateAcct between invoice and receipt |
reverse(Timestamp) |
boolean | Reverse this match (negated qty, linked via Reversal_ID) |
isReversal() |
boolean | True if this record was created to reverse another |
deleteMatchInvCostDetail() |
String (protected) | Delete MCostDetail records for all account schemas |
6. Document Flow: End-to-End Matching
Understanding the sequence in which matching records are created is essential for debugging and certification. There are two primary flows depending on which document arrives first after the PO.
6.1 Standard Flow: PO → Receipt → Invoice
This is the most common procurement flow:
| Step | Action | Records Created | Accounting Effect |
|---|---|---|---|
| 1 | Purchase Order completed | No matching records yet | Commitment accounting (if enabled) |
| 2 | Material Receipt completed | M_MatchPO (C_OrderLine_ID + M_InOutLine_ID, no C_InvoiceLine_ID) | Dr: Inventory, Cr: Not Invoiced Receipts (NIR) |
| 3 | Vendor Invoice completed | M_MatchPO updated (adds C_InvoiceLine_ID) + M_MatchInv auto-created | Dr: NIR relief, Cr: Accounts Payable; PPV if price differs |
In code, when the receipt is completed (MInOut.completeIt()), for each receipt line with a PO reference:
// During receipt completion (simplified):
MMatchPO po = MMatchPO.create(null, sLine, dateTrx, qty);
// Creates MMatchPO with M_InOutLine_ID and C_OrderLine_ID
// C_InvoiceLine_ID is 0 at this point
Later, when the invoice is completed (MInvoice.completeIt()):
// During invoice completion:
MMatchPO po = MMatchPO.create(line, null, getDateInvoiced(), matchQty);
// The create() method finds the existing MMatchPO (receipt match)
// Updates it with C_InvoiceLine_ID
// Auto-creates MMatchInv linking receipt line to invoice line
6.2 Alternative Flow: PO → Invoice → Receipt
Sometimes the vendor invoice arrives before the goods. In this case:
| Step | Action | Records Created | Accounting Effect |
|---|---|---|---|
| 1 | Purchase Order completed | No matching records | Commitment accounting (if enabled) |
| 2 | Vendor Invoice completed | M_MatchPO (C_OrderLine_ID + C_InvoiceLine_ID, no M_InOutLine_ID) | Dr: Expense/Asset, Cr: Accounts Payable (no NIR yet) |
| 3 | Material Receipt completed | M_MatchPO updated (adds M_InOutLine_ID) + M_MatchInv auto-created | Dr: Inventory, Cr: NIR; then NIR relief via MMatchInv |
6.3 Matching State Transitions
An MMatchPO record progresses through states as documents are completed:
State 1: Receipt-Only Match
C_OrderLine_ID = set
M_InOutLine_ID = set
C_InvoiceLine_ID = 0
(Created when receipt is completed before invoice)
State 2: Invoice-Only Match
C_OrderLine_ID = set
M_InOutLine_ID = 0
C_InvoiceLine_ID = set
(Created when invoice is completed before receipt)
State 3: Fully Matched
C_OrderLine_ID = set
M_InOutLine_ID = set
C_InvoiceLine_ID = set
(Updated when the missing document arrives)
+ M_MatchInv auto-created for Receipt-Invoice link
6.4 Period Checks During Matching
Both MMatchPO and MMatchInv enforce period openness checks before deleting posted records. This uses their respective DocBaseType constants:
// MMatchPO.beforeDelete():
MPeriod.testPeriodOpen(getCtx(), getDateTrx(),
MDocType.DOCBASETYPE_MatchPO, getAD_Org_ID());
// MMatchInv.beforeDelete():
MPeriod.testPeriodOpen(getCtx(), getDateTrx(),
MDocType.DOCBASETYPE_MatchInvoice, getAD_Org_ID());
This means that the accounting periods for both "MXP" (Match PO) and "MXI" (Match Invoice) must be open in order to delete or reverse matching records.
7. Price Variance Accounting
One of the most important outputs of three-way matching is the detection and accounting of price variances. iDempiere handles two types of variances through the matching process.
7.1 Not Invoiced Receipts (NIR)
When goods are received but not yet invoiced, the system creates a "Not Invoiced Receipts" liability entry. This is an interim account that represents goods the company has received but has not yet been billed for.
The accounting entries at receipt time (when MMatchPO is created with receipt line only):
| Account | Debit | Credit |
|---|---|---|
| Inventory (Product Asset) | PO Price * Qty | |
| Not Invoiced Receipts | PO Price * Qty |
When the invoice arrives and MMatchInv is created, the NIR account is relieved:
| Account | Debit | Credit |
|---|---|---|
| Not Invoiced Receipts | PO Price * Qty | |
| Inventory Clearing / Product Expense | Invoice Price * Qty | |
| Invoice Price Variance (IPV) | Difference (if any) |
7.2 Purchase Price Variance (PPV)
PPV is calculated in MMatchPO.beforeSave() when both a PO line and an invoice line are present. The variance is the difference between the PO price and the actual invoice price:
PPV = (PO Price - Invoice Price) * Matched Quantity
Key points about PPV calculation:
- Currency conversion: The
getInvoicePriceActual()method converts the invoice price to the order currency before comparison - The result is stored in:
M_MatchPO.PriceMatchDifference - Positive PPV: PO price > Invoice price (favorable variance — you paid less than expected)
- Negative PPV: PO price < Invoice price (unfavorable variance — you paid more than expected)
7.3 Price Match Tolerance
iDempiere supports a tolerance threshold for price variances, configured at the Business Partner Group level via the PriceMatchTolerance field. The tolerance is a percentage of the PO amount:
// From MMatchPO.beforeSave():
MBPGroup group = MBPGroup.getOfBPartner(getCtx(),
getOrderLine().getC_BPartner_ID());
BigDecimal mt = group.getPriceMatchTolerance();
if (mt != null && mt.signum() != 0) {
BigDecimal poAmt = poPrice.multiply(getQty());
BigDecimal maxTolerance = poAmt.multiply(mt)
.abs()
.divide(Env.ONEHUNDRED, 2, RoundingMode.HALF_UP);
difference = difference.abs();
boolean ok = difference.compareTo(maxTolerance) <= 0;
setIsApproved(ok);
}
If the absolute price difference exceeds the tolerance percentage, the MMatchPO record is marked as IsApproved = false, which can be used to flag invoices requiring managerial review before payment.
7.4 Accounting Entries Summary
| Document | DocBaseType | Accounting Entries |
|---|---|---|
| M_MatchPO (Receipt only) | MXP | Dr: Inventory, Cr: Not Invoiced Receipts |
| M_MatchPO (Invoice + Receipt) | MXP | Dr/Cr: Purchase Price Variance (if any) |
| M_MatchInv | MXI | Dr: Not Invoiced Receipts (relief), Cr: Inventory Clearing; Dr/Cr: Invoice Price Variance |
| C_Invoice (AP) | API | Dr: Expense/Inventory Clearing, Cr: Accounts Payable |
8. Complete Code Examples
8.1 Creating an AP Invoice from a Purchase Order
/**
* Create and complete an AP Invoice from a Purchase Order.
* Assumes the order is already completed.
*/
public MInvoice createAPInvoiceFromPO(MOrder order) {
// Create invoice header from order
MInvoice invoice = new MInvoice(order, 0, null);
// IsSOTrx is automatically set to false from the PO
// DocTypeTarget is set from the PO's doc type mapping
invoice.setDateInvoiced(new Timestamp(System.currentTimeMillis()));
invoice.setDateAcct(invoice.getDateInvoiced());
invoice.saveEx();
// Create invoice lines from order lines
MOrderLine[] oLines = order.getLines(true, null);
for (MOrderLine oLine : oLines) {
MInvoiceLine iLine = new MInvoiceLine(invoice);
iLine.setOrderLine(oLine); // copies pricing from PO
iLine.setQtyEntered(oLine.getQtyEntered());
iLine.setQtyInvoiced(oLine.getQtyOrdered());
iLine.saveEx();
}
// Complete the invoice
// This triggers MMatchPO.create() for each line with a product
if (!invoice.processIt(DocAction.ACTION_Complete)) {
throw new AdempiereException(
"Cannot complete invoice: " + invoice.getProcessMsg());
}
invoice.saveEx();
return invoice;
}
8.2 Creating an AP Invoice from a Material Receipt
/**
* Create and complete an AP Invoice from a Material Receipt.
* Creates MMatchInv records automatically during completion.
*/
public MInvoice createAPInvoiceFromReceipt(MInOut receipt) {
// Create invoice header from receipt
MInvoice invoice = new MInvoice(receipt, null);
invoice.setDateInvoiced(new Timestamp(System.currentTimeMillis()));
invoice.setDateAcct(invoice.getDateInvoiced());
invoice.saveEx();
// Create invoice lines from receipt lines
MInOutLine[] sLines = receipt.getLines(false);
for (MInOutLine sLine : sLines) {
if (sLine.isDescription() || sLine.getM_Product_ID() == 0)
continue;
MInvoiceLine iLine = new MInvoiceLine(invoice);
iLine.setShipLine(sLine); // copies pricing from PO via receipt
iLine.setQtyEntered(sLine.getMovementQty());
iLine.setQtyInvoiced(sLine.getMovementQty());
iLine.saveEx();
}
// Complete the invoice
// For lines with M_InOutLine_ID: creates MMatchInv directly
// For lines with C_OrderLine_ID: creates MMatchPO which may
// auto-create MMatchInv
if (!invoice.processIt(DocAction.ACTION_Complete)) {
throw new AdempiereException(
"Cannot complete invoice: " + invoice.getProcessMsg());
}
invoice.saveEx();
return invoice;
}
8.3 Querying Match Records
/**
* Query all matching records for a given purchase order line.
*/
public void inspectMatchingForOrderLine(int C_OrderLine_ID) {
Properties ctx = Env.getCtx();
String trxName = null;
// Get all MMatchPO records for this order line
MMatchPO[] matchPOs = MMatchPO.getOrderLine(ctx,
C_OrderLine_ID, trxName);
System.out.println("=== MMatchPO Records ===");
for (MMatchPO mpo : matchPOs) {
System.out.println(" M_MatchPO_ID=" + mpo.getM_MatchPO_ID()
+ " Qty=" + mpo.getQty()
+ " M_InOutLine_ID=" + mpo.getM_InOutLine_ID()
+ " C_InvoiceLine_ID=" + mpo.getC_InvoiceLine_ID()
+ " PriceMatchDiff=" + mpo.getPriceMatchDifference()
+ " IsApproved=" + mpo.isApproved()
+ " Reversal_ID=" + mpo.getReversal_ID());
// For each match with an invoice, check MMatchInv
if (mpo.getC_InvoiceLine_ID() > 0
&& mpo.getM_InOutLine_ID() > 0)
{
MMatchInv[] matchInvs = MMatchInv.get(ctx,
mpo.getM_InOutLine_ID(),
mpo.getC_InvoiceLine_ID(), trxName);
System.out.println(" === MMatchInv Records ===");
for (MMatchInv mi : matchInvs) {
System.out.println(" M_MatchInv_ID="
+ mi.getM_MatchInv_ID()
+ " Qty=" + mi.getQty()
+ " DateAcct=" + mi.getDateAcct());
}
}
}
}
8.4 Handling Partial Matches
/**
* Example: PO for 100 units, Receipt for 60 units, Invoice for 60.
* Shows how partial matching works.
*/
public void partialMatchExample(Properties ctx, String trxName) {
// After Receipt of 60 units is completed:
// M_MatchPO record 1: C_OrderLine_ID=X, M_InOutLine_ID=Y,
// C_InvoiceLine_ID=0, Qty=60
// After Invoice for 60 units is completed:
// MMatchPO.create(invoiceLine, null, dateInvoiced, 60)
// -> Finds existing MMatchPO (record 1)
// -> Updates C_InvoiceLine_ID
// -> Auto-creates MMatchInv for receipt-invoice link
// Remaining 40 units on the PO are still unmatched
// The order line shows:
// QtyOrdered=100, QtyDelivered=60, QtyInvoiced=60
// When 2nd receipt of 40 arrives:
// M_MatchPO record 2: C_OrderLine_ID=X, M_InOutLine_ID=Z,
// C_InvoiceLine_ID=0, Qty=40
// When 2nd invoice of 40 arrives:
// M_MatchPO record 2 updated with C_InvoiceLine_ID
// + M_MatchInv auto-created
// Order line: QtyOrdered=100, QtyDelivered=100, QtyInvoiced=100
}
8.5 Verifying Three-Way Matching Completeness
/**
* Check if a purchase order line is fully three-way matched.
* @param C_OrderLine_ID the order line to check
* @return true if fully matched
*/
public boolean isFullyThreeWayMatched(int C_OrderLine_ID) {
Properties ctx = Env.getCtx();
String trxName = null;
MOrderLine oLine = new MOrderLine(ctx,
C_OrderLine_ID, trxName);
BigDecimal qtyOrdered = oLine.getQtyOrdered();
// Check delivered qty
BigDecimal qtyDelivered = oLine.getQtyDelivered();
if (qtyDelivered.compareTo(qtyOrdered) < 0) {
System.out.println("Not fully received: "
+ qtyDelivered + " of " + qtyOrdered);
return false;
}
// Check invoiced qty
BigDecimal qtyInvoiced = oLine.getQtyInvoiced();
if (qtyInvoiced.compareTo(qtyOrdered) < 0) {
System.out.println("Not fully invoiced: "
+ qtyInvoiced + " of " + qtyOrdered);
return false;
}
// Verify all MMatchPO records have both receipt and invoice
MMatchPO[] matchPOs = MMatchPO.getOrderLine(ctx,
C_OrderLine_ID, trxName);
for (MMatchPO mpo : matchPOs) {
if (mpo.getReversal_ID() > 0) continue; // skip reversals
if (mpo.getM_InOutLine_ID() == 0) {
System.out.println("MMatchPO " + mpo.getM_MatchPO_ID()
+ " missing receipt line");
return false;
}
if (mpo.getC_InvoiceLine_ID() == 0) {
System.out.println("MMatchPO " + mpo.getM_MatchPO_ID()
+ " missing invoice line");
return false;
}
// Verify corresponding MMatchInv exists
MMatchInv[] matchInvs = MMatchInv.get(ctx,
mpo.getM_InOutLine_ID(),
mpo.getC_InvoiceLine_ID(), trxName);
if (matchInvs.length == 0) {
System.out.println("Missing MMatchInv for "
+ "M_InOutLine_ID=" + mpo.getM_InOutLine_ID()
+ ", C_InvoiceLine_ID="
+ mpo.getC_InvoiceLine_ID());
return false;
}
}
System.out.println("Order line " + C_OrderLine_ID
+ " is fully three-way matched.");
return true;
}
8.6 Finding Unmatched AP Invoices
/**
* Find all AP invoices that are not fully matched to receipts.
* Uses MInvoice's built-in static method.
*/
public void findUnmatchedInvoices() {
String trxName = null;
// Parameters: C_BPartner_ID, M_Product_ID, M_InOutLine_ID,
// dateFrom, dateTo, trxName
List<MInvoice.MatchingRecord> unmatched =
MInvoice.getNotFullyMatchedToReceipt(
0, // all business partners
0, // all products
0, // all receipt lines
null, // no date filter (from)
null, // no date filter (to)
trxName);
for (MInvoice.MatchingRecord rec : unmatched) {
System.out.println("Invoice " + rec.documentNo()
+ " Line " + rec.line()
+ " Product: " + rec.productName()
+ " QtyInvoiced: " + rec.qtyInvoiced()
+ " MatchedQty: " + rec.matchedQty()
+ " Unmatched: "
+ rec.qtyInvoiced().subtract(rec.matchedQty()));
}
}
9. Common Pitfalls and Best Practices
9.1 Matching Tolerance Issues
- Pitfall: The
PriceMatchToleranceis set at the Business Partner Group level, not the individual partner level. If you need different tolerances for different vendors, you must use separate BP Groups. - Pitfall: A tolerance of zero means no tolerance check is performed (the code checks
mt.signum() != 0). This is different from "zero tolerance" (which would reject any variance). To enforce strict zero tolerance, set a very small tolerance value like 0.01%. - Best Practice: Review unapproved MMatchPO records (
IsApproved = false) regularly to ensure price variances are investigated and resolved before payment.
9.2 Partial Receipt and Invoice Scenarios
- Pitfall: When a receipt has a different quantity than the invoice (e.g., receive 80 of 100 ordered, invoice for 100), the system creates separate MMatchPO records. The afterSave() validation throws
IllegalStateExceptionif total matched qty exceeds receipt movement qty or invoice qty. - Pitfall: The
create()method processes matching in order of existing MMatchPO records. If multiple unmatched records exist for the same order line, the system matches in sequence, potentially creating unexpected partial matches. - Best Practice: Ensure receipt quantities are verified at the warehouse before processing. Use the "Match PO" and "Match Invoice" windows in iDempiere to manually review and correct matching when quantities diverge.
9.3 Period Closing with Unmatched Records
- Pitfall: Unmatched records create Not Invoiced Receipts (NIR) balances that carry forward. If you close a period with significant NIR balances, they will distort the balance sheet.
- Pitfall: The period must be open for DocBaseType "MXP" (Match PO) and "MXI" (Match Invoice) to create, reverse, or delete matching records. If you close these period types, you cannot perform matching corrections.
- Best Practice: Before closing a period, run the "Not Fully Matched Invoices" report using
MInvoice.getNotFullyMatchedToReceipt()to identify and resolve outstanding matches.
9.4 Credit Memo Handling
- Pitfall: When creating MMatchPO from a credit memo (
isCreditMemo() = true), the system negates the quantity:line.getQtyInvoiced().negate(). This means the match quantity is negative, which reverses the original match rather than creating a new forward match. - Best Practice: Always create vendor credit memos using the proper AP Credit Memo document type (DocBaseType = "APC") rather than manually creating negative AP invoices.
9.5 Attribute Set Instance (ASI) Matching
- Pitfall: MMatchPO's
create()method comparesM_AttributeSetInstance_IDbetween existing records and new lines. If the receipt has a specific ASI (lot/serial number) but the invoice does not, the system may set the ASI on the match record from the receipt line. - Pitfall: If both records have different non-zero ASI values, the
create()method skips the match (thecontinuestatement is reached), potentially leaving records unmatched. - Best Practice: Ensure that ASI values are consistent across PO, receipt, and invoice. If using lot tracking, carry the lot number through all documents.
9.6 Multi-Currency Matching
- Pitfall: The
getInvoicePriceActual()method converts the invoice price to the order currency using the conversion rate as of the invoice date. If conversion rates are not maintained, the method throwsAdempiereExceptionwith the message "ErrorConvertingCurrencyToBaseCurrency". - Best Practice: Ensure currency conversion rates are up to date before completing AP invoices in foreign currencies. Review PPV amounts carefully when currencies differ between PO and invoice.
10. Summary
Three-way matching in iDempiere is a comprehensive system that ensures financial integrity throughout the procurement cycle. Here are the key takeaways:
- Two matching tables, two DocBaseTypes:
M_MatchPO(MXP) links PO lines to receipt and invoice lines;M_MatchInv(MXI) links receipt lines to invoice lines. - MMatchPO is the hub: The
MMatchPO.create()method is the central entry point called from bothMInOut.completeIt()andMInvoice.completeIt(). It follows an "update or create" pattern, first looking for existing unmatched records viaMatchPOAutoMatch.getNotMatchedMatchPOList(). - Auto-creation of MMatchInv: When an
MMatchPOrecord gains both a receipt line and an invoice line, thecreateMatchInv()method automatically creates the correspondingMMatchInvrecord within a database savepoint for safe rollback. - MInvoice AP constructors: AP invoices can be created from
MOrder(using PO-based pricing) or fromMInOut(using receipt-based references). ThesetBPartner()method uses the PayFrom location for AP and BillTo for AR. - MInvoiceLine bridges: The
setOrderLine()andsetShipLine()methods copy pricing and dimensions from PO lines and receipt lines respectively. ThegetMatchedQty()method queriesM_MatchInvto determine matching status. - PPV is calculated in beforeSave():
MMatchPO.beforeSave()computes the price difference between the PO price and the invoice price (converted to PO currency), stores it inPriceMatchDifference, and setsIsApprovedbased on the BP Group's tolerance percentage. - Quantity validation is enforced: Both
MMatchPO.afterSave()andMMatchInv.afterSave()validate that total matched quantities do not exceed the receipt's movement quantity or the invoice line's invoiced quantity, throwingIllegalStateExceptionon violation. - DateAcct uses the later date: Both MMatchPO and MMatchInv set their accounting date to the later of the invoice date and the receipt date via
getNewerDateAcct(). - Reversal creates linked records: The
reverse()methods on both classes create a new record with negated quantity, linked viaReversal_ID. The description fields provide cross-reference document numbers. - Period control uses DocBaseType: Deleting posted matching records requires the period to be open for "MXP" (Match PO) or "MXI" (Match Invoice) respectively.
- Landed costs: The
MInvoiceLine.allocateLandedCosts()method distributes freight and other charges across receipt lines based on configurable distribution rules (quantity, amount, weight, or volume).
Understanding these mechanisms at the source code level is essential for the iDempiere ERP certification exam, as matching and invoice verification questions frequently test knowledge of the exact API methods, the sequence of record creation, and the accounting implications of each matching scenario.
11. Exam Preparation: Quick Reference
Key Classes and Their Packages
| Class | Package | Purpose |
|---|---|---|
| MMatchPO | org.compiere.model | PO-to-Receipt and PO-to-Invoice matching |
| MMatchInv | org.compiere.model | Receipt-to-Invoice matching |
| MInvoice | org.compiere.model | Invoice header (AP and AR) |
| MInvoiceLine | org.compiere.model | Invoice line with PO/receipt references |
| MatchPOAutoMatch | org.compiere.model | Utility for finding unmatched MMatchPO records |
| MDocType | org.compiere.model | Document type with DocBaseType constants |
| MBPGroup | org.compiere.model | Business Partner Group with PriceMatchTolerance |
Key Constants to Remember
| Constant | Value | Where Used |
|---|---|---|
| DOCBASETYPE_MatchPO | "MXP" | Period checks for M_MatchPO deletions |
| DOCBASETYPE_MatchInvoice | "MXI" | Period checks for M_MatchInv deletions |
| DOCBASETYPE_APInvoice | "API" | AP invoice document type |
| DOCBASETYPE_APCreditMemo | "APC" | AP credit memo document type |
Critical Method Signatures to Know
// MMatchPO - Main entry point
public static MMatchPO create(MInvoiceLine iLine,
MInOutLine sLine, Timestamp dateTrx, BigDecimal qty)
// MMatchPO - Get or create pattern
public static MMatchPO getOrCreate(int C_OrderLine_ID,
BigDecimal qty, MInOutLine sLine, String trxName)
// MMatchPO - PPV helper
public BigDecimal getInvoicePriceActual()
// MMatchInv - Constructor from invoice line
public MMatchInv(MInvoiceLine iLine, Timestamp dateTrx,
BigDecimal qty)
// MMatchInv - Static queries
public static MMatchInv[] get(Properties ctx,
int M_InOutLine_ID, int C_InvoiceLine_ID, String trxName)
public static MMatchInv[] getInvoiceLine(Properties ctx,
int C_InvoiceLine_ID, String trxName)
public static MMatchInv[] getInOutLine(Properties ctx,
int M_InOutLine_ID, String trxName)
// MInvoiceLine - Matched qty from M_MatchInv
public BigDecimal getMatchedQty()
// MInvoiceLine - Copy from PO and Receipt
public void setOrderLine(MOrderLine oLine)
public void setShipLine(MInOutLine sLine)
// MInvoiceLine - Landed costs
public String allocateLandedCosts()
Common Exam Scenarios
- Scenario: PO for 100 units at $10, Receipt for 100 units, Invoice for 100 units at $12. What records are created and what is the PPV?
Answer: MMatchPO with PriceMatchDifference = (10 - 12) * 100 = -200 (unfavorable). MMatchInv auto-created. IsApproved depends on BP Group tolerance. - Scenario: PO for 100 units, Receipt for 60 units, Invoice for 100 units. What happens?
Answer: MMatchPO for 60 units (matched to receipt). Separate MMatchPO for remaining 40 units (invoice only, no receipt). MMatchInv created for 60 units only. 40 units remain as invoice-only match until 2nd receipt. - Scenario: Which date is used as DateAcct on matching records?
Answer: The later of the invoice DateAcct and the receipt DateAcct, determined bygetNewerDateAcct(). - Scenario: What happens if you try to delete a posted MMatchPO in a closed period?
Answer:beforeDelete()callsMPeriod.testPeriodOpen()with DocBaseType "MXP". If the period is closed, aPeriodClosedExceptionis thrown.