Vendor Management Basics
Overview
- What you’ll learn:
- The vendor master data model in iDempiere, powered by the
MBPartnerclass and its dual customer/vendor nature - How
MProductPOmaps vendor-product relationships, including pricing, delivery times, and order constraints - Payment term configuration through
MPaymentTermandMPaySchedule, including multi-installment schedules - Vendor location management via
MBPartnerLocation, with role-based address flags (PayFrom, RemitTo, ShipTo, BillTo) - How purchase orders and AP invoices resolve vendor defaults at runtime
- Complete code examples for creating vendors, linking products, and querying vendor data programmatically
- The vendor master data model in iDempiere, powered by the
- Prerequisites: Lesson 1 — Procurement Overview
- Estimated reading time: 25 minutes
1. Introduction: Vendor Master Data
In any procurement system, the vendor record is the foundational entity. Every purchase order, every goods receipt, and every accounts payable invoice ultimately traces back to a vendor. In iDempiere, vendors are not stored in a separate table — they are a subset of business partners. This unified design means a single entity can serve as both a customer (for sales) and a vendor (for purchasing), with distinct configurations for each role.
The vendor master data model in iDempiere consists of four interconnected components:
| Component | Java Class | Database Table | Purpose |
|---|---|---|---|
| Business Partner (Vendor) | MBPartner |
C_BPartner |
Core vendor record with PO-specific defaults (price list, payment term, discount schema) |
| Partner Location | MBPartnerLocation |
C_BPartner_Location |
Physical addresses with role flags: PayFrom, RemitTo, ShipTo, BillTo |
| Vendor-Product Link | MProductPO |
M_Product_PO |
Per-vendor pricing, delivery times, order constraints, and catalog info for each product |
| Payment Term | MPaymentTerm |
C_PaymentTerm |
Net days, discounts, and multi-installment payment schedules |
1.1 The Vendor Setup Workflow
Setting up a vendor in iDempiere follows a predictable sequence:
- Create the Business Partner — Set
IsVendor = trueand configure PO-specific fields (PO price list, PO payment term, PO discount schema). - Attach Locations — Create at least one
MBPartnerLocationrecord. For AP invoices, the system looks for a location flagged asIsPayFrom. - Add Contacts — Optional but important:
MUserrecords linked to the business partner for purchase order communication. - Link Products — Create
MProductPOrecords to specify which products this vendor supplies, at what price, with what lead time. - Assign Payment Terms — Configure
MPaymentTerm(optionally withMPayScheduleinstallments) and assign it to the vendor viaPO_PaymentTerm_ID.
With this foundation in place, every time a buyer selects this vendor on a purchase order, the system automatically populates the order with the correct price list, payment terms, delivery rules, and addresses. Let us examine each of these components in detail, starting with the business partner itself.
2. MBPartner: The Vendor Deep Dive
The MBPartner class (source file: org.compiere.model.MBPartner) extends X_C_BPartner and implements ImmutablePOSupport. It is the model class for the C_BPartner table, which stores every business partner in the system — customers, vendors, employees, and sales representatives alike.
2.1 The Dual Nature: IsVendor and IsCustomer
The critical design decision in iDempiere’s business partner model is that a single record can serve multiple roles simultaneously. The role flags are set in the setInitialDefaults() method, which is called when creating a new record (ID = 0):
/**
* Set the initial defaults for a new record
*/
private void setInitialDefaults() {
//
setIsCustomer (true);
setIsProspect (true);
//
setSendEMail (false);
setIsOneTime (false);
setIsVendor (false); // NOT a vendor by default
setIsSummary (false);
setIsEmployee (false);
setIsSalesRep (false);
setIsTaxExempt(false);
setIsPOTaxExempt(false); // PO-specific tax exemption
setIsDiscountPrinted(false);
//
setSO_CreditLimit (Env.ZERO);
setSO_CreditUsed (Env.ZERO);
setTotalOpenBalance (Env.ZERO);
setSOCreditStatus(SOCREDITSTATUS_NoCreditCheck);
//
setFirstSale(null);
setActualLifeTimeValue(Env.ZERO);
setPotentialLifeTimeValue(Env.ZERO);
setAcqusitionCost(Env.ZERO);
setShareOfCustomer(0);
setSalesVolume(0);
}
Notice the asymmetry: IsCustomer defaults to true, while IsVendor defaults to false. This means every new business partner is a customer by default but must be explicitly flagged as a vendor. This is a deliberate design choice — in most businesses, the set of customers is much larger than the set of vendors, so the default optimizes for the common case.
Also note IsPOTaxExempt — this is a separate tax exemption flag specifically for purchase transactions, independent of the sales-side IsTaxExempt. This allows a business partner that is both customer and vendor to have different tax treatment for each role.
2.2 PO-Specific Fields on C_BPartner
The C_BPartner table carries a parallel set of fields for purchasing, each prefixed with PO_. These fields determine default behavior when the vendor is selected on a purchase order or AP invoice:
| Field | Type | Description | Fallback |
|---|---|---|---|
PO_PriceList_ID |
int (FK) | The default purchase price list for this vendor. Determines currency and pricing for PO lines. | BP Group’s PO_PriceList_ID |
PO_PaymentTerm_ID |
int (FK) | Payment term for purchase transactions (net days, discount schedule). | BP Group’s PO_PaymentTerm_ID |
PO_DiscountSchema_ID |
int (FK) | Discount schema for calculating purchase trade discounts (volume discounts, tiered pricing). | BP Group’s PO_DiscountSchema_ID |
IsPOTaxExempt |
boolean | Whether this business partner is exempt from tax on purchases. | false |
PaymentRulePO |
String | Default payment rule for purchase orders (On Credit, Direct Debit, etc.). | Falls back to PaymentRule |
2.3 BP Group Fallback: The getBPGroup() Pattern
One of the most important patterns in iDempiere’s vendor model is the BP Group fallback. When a PO-specific field is not set directly on the business partner, the system automatically falls back to the BP Group’s setting. This is implemented through method overrides in MBPartner:
/**
* Get PO PriceList ID
* @return BP PO price list id or BP Group PO price list id
*/
public int getPO_PriceList_ID () {
int ii = super.getPO_PriceList_ID();
if (ii == 0)
ii = getBPGroup().getPO_PriceList_ID();
return ii;
}
/**
* Get PO DiscountSchema id
* @return po discount schema id
*/
public int getPO_DiscountSchema_ID () {
int ii = super.getPO_DiscountSchema_ID ();
if (ii == 0)
ii = getBPGroup().getPO_DiscountSchema_ID();
return ii;
}
The same pattern applies to the sales-side price list and discount schema. The getBPGroup() method lazily loads and caches the BP Group:
/**
* Get BP Group
* @return group
*/
public MBPGroup getBPGroup() {
if (m_group == null) {
if (getC_BP_Group_ID() == 0)
m_group = MBPGroup.getDefault(getCtx());
else
m_group = MBPGroup.getCopy(getCtx(), getC_BP_Group_ID(), get_TrxName());
}
return m_group;
}
And when a BP Group is assigned (or changed), the setBPGroup() method propagates defaults from the group to the partner:
public void setBPGroup(MBPGroup group) {
m_group = group;
if (m_group == null)
return;
setC_BP_Group_ID(m_group.getC_BP_Group_ID());
if (m_group.getC_Dunning_ID() != 0)
setC_Dunning_ID(m_group.getC_Dunning_ID());
if (m_group.getM_PriceList_ID() != 0)
setM_PriceList_ID(m_group.getM_PriceList_ID());
if (m_group.getPO_PriceList_ID() != 0)
setPO_PriceList_ID(m_group.getPO_PriceList_ID());
if (m_group.getM_DiscountSchema_ID() != 0)
setM_DiscountSchema_ID(m_group.getM_DiscountSchema_ID());
if (m_group.getPO_DiscountSchema_ID() != 0)
setPO_DiscountSchema_ID(m_group.getPO_DiscountSchema_ID());
}
This inheritance chain (Vendor → BP Group → System Default) is triggered automatically in beforeSave() for new records or when the group changes:
@Override
protected boolean beforeSave (boolean newRecord) {
if (newRecord || is_ValueChanged("C_BP_Group_ID")) {
MBPGroup grp = getBPGroup();
if (grp == null) {
log.saveError("Error", Msg.parseTranslation(getCtx(),
"@NotFound@: @C_BP_Group_ID@"));
return false;
}
setBPGroup(grp); // setDefaults
}
return true;
}
Exam tip: This means you can configure purchase defaults at the BP Group level (e.g., “All Hardware Suppliers use PO Price List X and Payment Term Net-30”) and have them automatically apply to every new vendor in that group. Individual vendors can override any of these defaults by setting the field directly.
2.4 Credit Status Constants and Logic
While credit management is primarily a sales-side concern, understanding the credit status system is important for certification because it affects the overall business partner model. The X_C_BPartner class defines five credit status constants:
| Constant | Value | Description |
|---|---|---|
SOCREDITSTATUS_NoCreditCheck |
"X" |
No credit check is performed. This is the default for new partners. |
SOCREDITSTATUS_CreditOK |
"O" |
Open balance is within acceptable limits. |
SOCREDITSTATUS_CreditWatch |
"W" |
Open balance exceeds the watch threshold (credit limit * watch ratio) but is below the credit limit. |
SOCREDITSTATUS_CreditHold |
"H" |
Open balance exceeds the credit limit. Sales orders may be blocked. |
SOCREDITSTATUS_CreditStop |
"S" |
Manually set to stop all credit sales. Cannot be overridden by the automatic calculation. |
The setSOCreditStatus() method in MBPartner implements the automatic credit evaluation:
public void setSOCreditStatus () {
BigDecimal creditLimit = getSO_CreditLimit();
// Nothing to do
if (SOCREDITSTATUS_NoCreditCheck.equals(getSOCreditStatus())
|| SOCREDITSTATUS_CreditStop.equals(getSOCreditStatus())
|| Env.ZERO.compareTo(creditLimit) == 0)
return;
// Above Credit Limit
if (creditLimit.compareTo(getTotalOpenBalance()) < 0)
setSOCreditStatus(SOCREDITSTATUS_CreditHold);
else {
// Above Watch Limit
BigDecimal watchAmt = creditLimit.multiply(getCreditWatchRatio());
if (watchAmt.compareTo(getTotalOpenBalance()) < 0)
setSOCreditStatus(SOCREDITSTATUS_CreditWatch);
else // is OK
setSOCreditStatus (SOCREDITSTATUS_CreditOK);
}
}
Key points to notice:
- If the status is
NoCreditCheckorCreditStop, the method returns immediately — these states are not automatically changed. - If the credit limit is zero, no check is performed (avoids division issues and treats zero limit as "no limit set").
- The watch ratio comes from the BP Group via
getCreditWatchRatio()(defaults to 0.9 if not set). - The helper method
isCreditStopHold()provides a convenient check:return SOCREDITSTATUS_CreditStop.equals(status) || SOCREDITSTATUS_CreditHold.equals(status);
2.5 Retrieving Vendor Locations and the Primary Location
The MBPartner class provides several methods for working with locations. The getLocations() method returns only active locations, sorted by ID:
public MBPartnerLocation[] getLocations (boolean reload) {
if (reload || m_locations == null || m_locations.length == 0)
;
else
return m_locations;
//
ArrayList<MBPartnerLocation> list = new ArrayList<MBPartnerLocation>();
final String sql = "SELECT * FROM C_BPartner_Location "
+ "WHERE C_BPartner_ID=? AND IsActive='Y'"
+ " ORDER BY C_BPartner_Location_ID";
// ... (JDBC loading code)
m_locations = new MBPartnerLocation[list.size()];
list.toArray(m_locations);
return m_locations;
}
The getPrimaryC_BPartner_Location_ID() method selects the primary location with a preference for BillTo addresses, falling back to the first location:
public int getPrimaryC_BPartner_Location_ID() {
if (m_primaryC_BPartner_Location_ID == null) {
MBPartnerLocation[] locs = getLocations(false);
for (int i = 0; m_primaryC_BPartner_Location_ID == null
&& i < locs.length; i++) {
if (locs[i].isBillTo()) {
setPrimaryC_BPartner_Location_ID(
locs[i].getC_BPartner_Location_ID());
break;
}
}
// get first
if (m_primaryC_BPartner_Location_ID == null && locs.length > 0)
setPrimaryC_BPartner_Location_ID(
locs[0].getC_BPartner_Location_ID());
}
if (m_primaryC_BPartner_Location_ID == null)
return 0;
return m_primaryC_BPartner_Location_ID.intValue();
}
2.6 MBPartner Key Methods Reference
| Method | Return Type | Description |
|---|---|---|
getTemplate(ctx, AD_Client_ID) |
MBPartner (static) |
Creates a new BP from the Cash Trx template, resetting identity fields. |
get(ctx, Value) |
MBPartner (static) |
Looks up a partner by search key (Value) within the current client. |
get(ctx, C_BPartner_ID) |
MBPartner (static) |
Looks up a partner by primary key ID within the current client. |
getFirstWithTaxID(ctx, taxID, trxName) |
MBPartner (static) |
Finds the first partner matching a tax ID, ordered by C_BPartner_ID. |
getNotInvoicedAmt(C_BPartner_ID) |
BigDecimal (static) |
Calculates the total value of shipped-but-not-invoiced SO lines for a partner. |
getLocations(reload) |
MBPartnerLocation[] |
Returns active partner locations, cached unless reload=true. |
getLocation(C_BPartner_Location_ID) |
MBPartnerLocation |
Returns a specific location, or the first BillTo, or the first location. |
getContacts(reload) |
MUser[] |
Returns active user/contact records linked to this partner. |
getContact(AD_User_ID) |
MUser |
Returns a specific contact or the first contact if ID is 0. |
getBankAccounts(requery) |
MBPBankAccount[] |
Returns active bank accounts for payment processing. |
getBPGroup() |
MBPGroup |
Returns the BP Group (cached), or the default group if none assigned. |
setBPGroup(group) |
void |
Sets the BP Group and propagates defaults (price lists, discount schemas, dunning). |
getPO_PriceList_ID() |
int |
Returns PO price list from BP, falling back to BP Group. |
getPO_DiscountSchema_ID() |
int |
Returns PO discount schema from BP, falling back to BP Group. |
setSOCreditStatus() |
void |
Recalculates credit status based on open balance vs. credit limit. |
getSOCreditStatus(additionalAmt) |
String |
Simulates credit status with an additional amount (for pre-order checks). |
isCreditStopHold() |
boolean |
Returns true if status is CreditStop or CreditHold. |
setTotalOpenBalance() |
void |
Recalculates total open balance and SO credit used from completed invoices and payments. |
getPrimaryC_BPartner_Location_ID() |
int |
Returns the primary location (first BillTo or first). |
3. MBPartnerLocation: Vendor Addresses
The MBPartnerLocation class (source file: org.compiere.model.MBPartnerLocation) extends X_C_BPartner_Location and represents a physical address associated with a business partner. Each partner can have multiple locations, each serving different roles in the procurement lifecycle.
3.1 Location Role Flags
Each MBPartnerLocation record carries four boolean flags that determine how the address is used in different transaction types. The defaults are set in setInitialDefaults():
private void setInitialDefaults() {
setName(".");
//
setIsShipTo(true); // Shipping address for goods receipt
setIsRemitTo(true); // Address where we send payments (vendor's payment address)
setIsPayFrom(true); // Address from which the vendor pays (for AP invoices)
setIsBillTo(true); // Invoice/billing address
}
All four flags default to true, meaning a new location is assumed to serve all roles. In practice, larger vendors may have separate addresses for different purposes:
| Flag | Procurement Usage | Description |
|---|---|---|
IsShipTo |
Purchase Order → Ship-To on PO | Where goods are shipped from the vendor (or where we receive goods). On POs, used for selecting the vendor's ship-from address. |
IsBillTo |
Purchase Order → Bill-To | Invoice/billing address. On sales orders, this is the customer's billing address. On POs, the MOrder.setBPartner() uses this to set the Bill_Location_ID. |
IsPayFrom |
AP Invoice → Location selection | The address used for AP (vendor) invoices. The MInvoice.setBPartner() method specifically selects this location for purchase invoices. |
IsRemitTo |
Payment → Remittance address | The address where payments are remitted (sent to the vendor). |
3.2 How AP Invoice setBPartner() Selects PayFrom Location
One of the most important things to understand is how the system selects the correct location for AP invoices. The MInvoice.setBPartner() method makes a clear distinction between sales and purchase invoices:
// From MInvoice.setBPartner():
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());
}
// set to first
if (getC_BPartner_Location_ID() == 0 && locs.length > 0)
setC_BPartner_Location_ID(locs[0].getC_BPartner_Location_ID());
}
The logic is straightforward:
- For sales invoices (
isSOTrx() = true): select a location flagged asIsBillTo. - For purchase invoices (
isSOTrx() = false): select a location flagged asIsPayFrom. - Fallback: if no matching location is found, use the first location in the list.
In contrast, the MOrder.setBPartner() method uses a different selection strategy for purchase orders:
// From MOrder.setBPartner():
MBPartnerLocation[] locs = bp.getLocations(false);
if (locs != null) {
for (int i = 0; i < locs.length; i++) {
if (locs[i].isShipTo())
super.setC_BPartner_Location_ID(
locs[i].getC_BPartner_Location_ID());
if (locs[i].isBillTo())
setBill_Location_ID(
locs[i].getC_BPartner_Location_ID());
}
// set to first
if (getC_BPartner_Location_ID() == 0 && locs.length > 0)
super.setC_BPartner_Location_ID(locs[0].getC_BPartner_Location_ID());
if (getBill_Location_ID() == 0 && locs.length > 0)
setBill_Location_ID(locs[0].getC_BPartner_Location_ID());
}
// Both are required - exceptions thrown if missing:
if (getC_BPartner_Location_ID() == 0)
throw new BPartnerNoShipToAddressException(bp);
if (getBill_Location_ID() == 0)
throw new BPartnerNoBillToAddressException(bp);
Notice that MOrder assigns two separate locations — one for shipping (IsShipTo) and one for billing (IsBillTo). If either is missing, an exception is thrown.
3.3 The getForBPartner() Static Method
The MBPartnerLocation class provides a static factory method to retrieve all locations (including inactive) for a given partner:
public static MBPartnerLocation[] getForBPartner(Properties ctx,
int C_BPartner_ID, String trxName) {
List<MBPartnerLocation> list = new Query(ctx, Table_Name,
"C_BPartner_ID=?", trxName)
.setParameters(C_BPartner_ID)
.list();
MBPartnerLocation[] retValue = new MBPartnerLocation[list.size()];
list.toArray(retValue);
return retValue;
}
Important distinction: This static method returns all locations (including inactive), while the instance method MBPartner.getLocations() filters to active only via AND IsActive='Y' in its SQL.
3.4 Automatic Name Generation
When a new location is created with the default name ".", the beforeSave() method automatically generates a human-readable name based on the physical address:
@Override
protected boolean beforeSave(boolean newRecord) {
if (getC_Location_ID() == 0)
return false;
// Set New Name
if (".".equals(getName()) && !isPreserveCustomName()) {
MLocation address = getLocation(true);
setName(getBPLocName(address));
}
return true;
}
The getBPLocName() method constructs a unique name using a cascading strategy: City → Address1 → Address2 → Region → ID. This ensures each location for a business partner has a distinct, recognizable name. The IsPreserveCustomName flag (default false) can be set to true to prevent automatic name generation after the initial save.
4. MProductPO: Vendor-Product Relationship
The MProductPO class (source file: org.compiere.model.MProductPO) extends X_M_Product_PO and represents the critical link between a product and its vendor. Each record in the M_Product_PO table defines one vendor's terms for supplying one product — including pricing, delivery times, order constraints, and catalog information.
4.1 Complete Field Reference
The M_Product_PO table contains an extensive set of fields, organized into functional groups:
| Category | Field | Type | Description |
|---|---|---|---|
| Pricing | PricePO |
BigDecimal | Standard purchase price from this vendor |
PriceList |
BigDecimal | Vendor's published list price | |
PriceLastPO |
BigDecimal | Last price paid on a PO (auto-updated, read-only via set_ValueNoCheck) |
|
PriceLastInv |
BigDecimal | Last price on a vendor invoice (auto-updated, read-only) | |
PriceEffective |
Timestamp | Date from which PricePO becomes effective | |
| Order Constraints | Order_Min |
BigDecimal | Minimum order quantity (replenishment rounds up to this) |
Order_Pack |
BigDecimal | Package/batch quantity (orders rounded to multiples of this) | |
| Delivery | DeliveryTime_Promised |
int | Vendor's promised lead time in days |
DeliveryTime_Actual |
int | Actual average delivery time in days | |
| Vendor Catalog | VendorProductNo |
String | Vendor's product/part number |
VendorCategory |
String | Vendor's product category | |
Manufacturer |
String | Original equipment manufacturer name | |
UPC |
String | Universal Product Code / barcode | |
| Costs | QualityRating |
BigDecimal | Quality rating 0-100 for vendor evaluation |
CostPerOrder |
BigDecimal | Fixed cost per order placed (shipping/handling) | |
RoyaltyAmt |
BigDecimal | Royalty or licensing fee per unit | |
| Status | IsCurrentVendor |
boolean | Whether this is the current/primary vendor for this product |
IsDiscontinued |
boolean | Product discontinued by this vendor | |
DiscontinuedAt |
Timestamp | Date the product was/will be discontinued |
4.2 The IsCurrentVendor Constraint
One of the most important business rules in MProductPO is enforced in the beforeSave() method — only one vendor per product can be marked as the current vendor:
@Override
protected boolean beforeSave(boolean newRecord) {
if (isCurrentVendor()) {
// Ensure only one current vendor per product
StringBuilder sql = new StringBuilder(
"UPDATE M_Product_PO SET IsCurrentVendor='N' "
+ "WHERE M_Product_ID=? AND C_BPartner_ID!=?");
int updated = DB.executeUpdate(sql.toString(),
new Object[]{getM_Product_ID(), getC_BPartner_ID()},
false, get_TrxName());
if (log.isLoggable(Level.FINE))
log.fine("Updated " + updated);
}
return true;
}
When you set IsCurrentVendor = true for one vendor-product record, the system automatically resets all other vendors for that same product to false. This ensures a single "preferred vendor" designation per product, which is used by the RequisitionPOCreate process for automatic vendor selection.
4.3 The getOfProduct() Static Method
The primary query method retrieves all vendor records for a given product:
public static MProductPO[] getOfProduct(Properties ctx, int M_Product_ID,
String trxName) {
List<MProductPO> list = new Query(ctx, Table_Name,
"M_Product_ID=?", trxName)
.setParameters(M_Product_ID)
.setOnlyActiveRecords(true)
.setOrderBy("IsCurrentVendor DESC, PricePO")
.list();
return list.toArray(new MProductPO[list.size()]);
}
Note the ORDER BY: the current vendor always appears first, followed by other vendors sorted by purchase price ascending. This makes it easy to find the preferred vendor or the cheapest alternative.
4.4 Read-Only Price Tracking Fields
The PriceLastPO and PriceLastInv fields are automatically updated by the system when purchase orders are completed and vendor invoices are processed. They use set_ValueNoCheck in the generated model class, which means they can only be set programmatically — not through the UI. This prevents manual tampering with historical price data:
// From X_M_Product_PO (generated model):
public void setPriceLastPO(BigDecimal PriceLastPO) {
set_ValueNoCheck(COLUMNNAME_PriceLastPO, PriceLastPO);
}
public void setPriceLastInv(BigDecimal PriceLastInv) {
set_ValueNoCheck(COLUMNNAME_PriceLastInv, PriceLastInv);
}
5. MPaymentTerm: Payment Terms Deep Dive
The MPaymentTerm class (source file: org.compiere.model.MPaymentTerm) extends X_C_PaymentTerm and defines the payment conditions for transactions — net days, early payment discounts, and multi-installment schedules.
5.1 Key Fields
| Field | Type | Description |
|---|---|---|
NetDays |
int | Number of days until payment is due (e.g., 30 for Net-30) |
DiscountDays |
int | Number of days within which early payment discount applies |
Discount |
BigDecimal | Early payment discount percentage (e.g., 2.0 for 2%) |
DiscountDays2 |
int | Second discount tier days |
Discount2 |
BigDecimal | Second discount tier percentage |
GraceDays |
int | Grace period days beyond due date |
IsNextBusinessDay |
boolean | Whether to push due date to next business day |
FixMonthCutoff |
int | Day of month for fixed-day payment terms |
FixMonthDay |
int | Day of month when payment is due |
FixMonthOffset |
int | Month offset for fixed-day terms |
5.2 The getSchedule() Method
For multi-installment payment terms (e.g., "50% in 30 days, 50% in 60 days"), the schedule is stored in child MPaySchedule records. The getSchedule() method retrieves them:
public MPaySchedule[] getSchedule(boolean requery) {
if (m_schedule != null && !requery)
return m_schedule;
String sql = "SELECT * FROM C_PaySchedule WHERE C_PaymentTerm_ID=? "
+ "AND IsActive='Y' ORDER BY NetDays";
// ... JDBC loading code ...
m_schedule = new MPaySchedule[list.size()];
list.toArray(m_schedule);
return m_schedule;
}
Each MPaySchedule record defines:
| Field | Type | Description |
|---|---|---|
Percentage |
BigDecimal | Percentage of total amount for this installment (must sum to 100%) |
NetDays |
int | Net days for this installment's due date |
DiscountDays |
int | Early discount days for this installment |
Discount |
BigDecimal | Early discount percentage for this installment |
5.3 The validate() Method
The validate() method ensures payment schedule percentages sum to 100%:
public boolean validate() {
MPaySchedule[] schedule = getSchedule(true);
if (schedule.length == 0) {
setIsValid(true);
return true;
}
BigDecimal total = Env.ZERO;
for (int i = 0; i < schedule.length; i++) {
BigDecimal percentage = schedule[i].getPercentage();
if (percentage != null)
total = total.add(percentage);
}
boolean valid = total.compareTo(Env.ONEHUNDRED) == 0;
setIsValid(valid);
return valid;
}
5.4 The apply() Method: Creating Invoice Payment Schedule
The apply() method is called when an invoice is created to generate the corresponding C_InvoicePaySchedule records:
public boolean apply(MInvoice invoice) {
if (invoice == null || invoice.getGrandTotal() == null
|| Env.ZERO.compareTo(invoice.getGrandTotal()) == 0)
return false;
MPaySchedule[] schedule = getSchedule(false);
if (schedule.length == 0) {
return applyNoSchedule(invoice); // Simple single-payment term
} else {
return applySchedule(invoice); // Multi-installment
}
}
applyNoSchedule: Creates a single MInvoicePaySchedule with 100% of the grand total, due date calculated from NetDays, and discount date from DiscountDays.
applySchedule: Creates one MInvoicePaySchedule per MPaySchedule record, with each installment's amount calculated as: GrandTotal × Percentage / 100, rounded to the currency's standard precision.
// From the MInvoicePaySchedule constructor (called by applySchedule):
public MInvoicePaySchedule(MInvoice invoice, MPaySchedule paySchedule) {
super(invoice.getCtx(), 0, invoice.get_TrxName());
m_parent = invoice;
setClientOrg(invoice);
setC_Invoice_ID(invoice.getC_Invoice_ID());
setC_PaySchedule_ID(paySchedule.getC_PaySchedule_ID());
// Amounts
int scale = MCurrency.getStdPrecision(getCtx(), invoice.getC_Currency_ID());
BigDecimal due = invoice.getGrandTotal();
if (due.compareTo(Env.ZERO) == 0) {
setDueAmt(Env.ZERO);
setDiscountAmt(Env.ZERO);
setIsValid(false);
} else {
due = due.multiply(paySchedule.getPercentage())
.divide(Env.ONEHUNDRED, scale, RoundingMode.HALF_UP);
setDueAmt(due);
BigDecimal discount = due.multiply(paySchedule.getDiscount())
.divide(Env.ONEHUNDRED, scale, RoundingMode.HALF_UP);
setDiscountAmt(discount);
setIsValid(true);
}
// Dates
Timestamp dueDate = TimeUtil.addDays(invoice.getDateInvoiced(),
paySchedule.getNetDays());
setDueDate(dueDate);
Timestamp discountDate = TimeUtil.addDays(invoice.getDateInvoiced(),
paySchedule.getDiscountDays());
setDiscountDate(discountDate);
}
6. Vendor Selection and Sourcing
When a buyer creates a purchase order and selects a vendor, the system automatically resolves a chain of defaults. Understanding this resolution chain is critical for both configuration and certification exams.
6.1 How MOrder.setBPartner() Resolves Vendor Defaults
The MOrder.setBPartner() method handles vendor selection differently from customer selection. For purchase orders (IsSOTrx = false), it resolves PO-specific defaults:
// Simplified from MOrder.setBPartner():
public void setBPartner(MBPartner bp) {
// Core fields
super.setC_BPartner_ID(bp.getC_BPartner_ID());
setAD_Language(bp.getAD_Language());
// Price List resolution
int pricelist = bp.getPO_PriceList_ID(); // BP → BP Group fallback
if (pricelist > 0 && !isSOTrx())
setM_PriceList_ID(pricelist);
// Payment Term resolution
int paymentTerm = bp.getPO_PaymentTerm_ID(); // BP → BP Group fallback
if (paymentTerm > 0 && !isSOTrx())
setC_PaymentTerm_ID(paymentTerm);
// Location resolution (ShipTo + BillTo)
MBPartnerLocation[] locs = bp.getLocations(false);
// ... selects IsShipTo for C_BPartner_Location_ID
// ... selects IsBillTo for Bill_Location_ID
}
6.2 Multiple Vendors Per Product
The M_Product_PO table supports multiple vendors for the same product. This enables:
- Price comparison: Query all vendors for a product sorted by price
- Backup sourcing: If the current vendor discontinues the product, switch to an alternative
- Regional sourcing: Different vendors for different warehouses or organizations
// Query all vendors for a product, sorted by price:
MProductPO[] vendors = MProductPO.getOfProduct(ctx, M_Product_ID, trxName);
// vendors[0] is the current vendor (or cheapest if none is current)
// Find the cheapest non-discontinued vendor:
for (MProductPO vendorProduct : vendors) {
if (!vendorProduct.isDiscontinued()) {
// Use this vendor
int vendorBP_ID = vendorProduct.getC_BPartner_ID();
BigDecimal price = vendorProduct.getPricePO();
break;
}
}
6.3 The RequisitionPOCreate Vendor Resolution
The RequisitionPOCreate process (covered in Lesson 1) resolves the vendor for each requisition line using a three-level priority:
| Priority | Source | Description |
|---|---|---|
| 1 | Requisition Line | C_BPartner_ID set directly on the line by the requester |
| 2 | Charge | If the line uses a Charge instead of a product, the charge's vendor is used |
| 3 | MProductPO | The IsCurrentVendor = true record for the product |
7. Complete Code Examples
7.1 Creating a Vendor Business Partner
// Create a new vendor with PO defaults
MBPartner vendor = new MBPartner(ctx, 0, trxName);
vendor.setValue("V-1001");
vendor.setName("Acme Supplies Ltd.");
vendor.setIsVendor(true); // Enable vendor role
vendor.setIsCustomer(false); // Optionally disable customer role
// Set PO-specific defaults
vendor.setPO_PriceList_ID(purchasePriceListId);
vendor.setPO_PaymentTerm_ID(net30TermId);
vendor.setPaymentRulePO(MBPartner.PAYMENTRULE_OnCredit);
vendor.setIsPOTaxExempt(false);
// Assign to a BP Group (propagates defaults if PO fields not set)
MBPGroup group = MBPGroup.get(ctx, supplierGroupId);
vendor.setBPGroup(group);
vendor.saveEx();
// Add a location with PayFrom flag
MBPartnerLocation loc = new MBPartnerLocation(vendor);
loc.setC_Location_ID(locationId);
loc.setIsPayFrom(true);
loc.setIsRemitTo(true);
loc.setIsShipTo(false); // This is a billing-only address
loc.setIsBillTo(true);
loc.saveEx();
7.2 Setting Up MProductPO Records
// Link a product to a vendor with pricing and delivery terms
MProductPO productVendor = new MProductPO(ctx, 0, trxName);
productVendor.setM_Product_ID(productId);
productVendor.setC_BPartner_ID(vendorId);
productVendor.setIsCurrentVendor(true); // Mark as preferred vendor
// Pricing
productVendor.setPricePO(new BigDecimal("25.50"));
productVendor.setPriceList(new BigDecimal("30.00"));
// Order constraints
productVendor.setOrder_Min(new BigDecimal("100")); // Minimum 100 units
productVendor.setOrder_Pack(new BigDecimal("25")); // Must order in multiples of 25
// Delivery
productVendor.setDeliveryTime_Promised(14); // 14 days lead time
// Vendor catalog info
productVendor.setVendorProductNo("ACME-W-500");
productVendor.setVendorCategory("Widgets");
productVendor.setManufacturer("Acme Corp");
productVendor.setUPC("012345678901");
// Quality and costs
productVendor.setQualityRating(new BigDecimal("85"));
productVendor.setCostPerOrder(new BigDecimal("15.00"));
productVendor.saveEx();
7.3 Querying Vendor-Product Relationships
// Find all vendors for a product
MProductPO[] vendors = MProductPO.getOfProduct(ctx, productId, trxName);
StringBuilder report = new StringBuilder();
report.append("Vendors for Product ID: ").append(productId).append("\n");
report.append(String.format("%-20s %-12s %-12s %-8s %-10s%n",
"Vendor", "PO Price", "Last PO", "Lead", "Current"));
for (MProductPO vp : vendors) {
MBPartner bp = MBPartner.get(ctx, vp.getC_BPartner_ID());
report.append(String.format("%-20s %-12s %-12s %-8d %-10s%n",
bp.getName(),
vp.getPricePO(),
vp.getPriceLastPO() != null ? vp.getPriceLastPO() : "N/A",
vp.getDeliveryTime_Promised(),
vp.isCurrentVendor() ? "YES" : "no"));
}
// SQL approach for cross-vendor price comparison:
// SELECT bp.Name, po.PricePO, po.PriceLastPO, po.PriceLastInv,
// po.DeliveryTime_Promised, po.QualityRating, po.IsCurrentVendor
// FROM M_Product_PO po
// JOIN C_BPartner bp ON po.C_BPartner_ID = bp.C_BPartner_ID
// WHERE po.M_Product_ID = ?
// AND po.IsActive = 'Y'
// AND po.IsDiscontinued = 'N'
// ORDER BY po.IsCurrentVendor DESC, po.PricePO ASC
8. Common Pitfalls and Best Practices
| Pitfall | Solution |
|---|---|
Forgetting to set IsVendor = true |
New BPs default to customer only. Always explicitly set IsVendor(true) for purchasing. |
| No PayFrom location for AP invoices | Ensure at least one MBPartnerLocation has IsPayFrom = true, otherwise AP invoices fall back to the first location. |
| PO price list not set | Set PO_PriceList_ID on the BP or BP Group. Without it, PO lines cannot resolve prices. |
| Multiple current vendors | The beforeSave() constraint handles this automatically, but be aware that setting one vendor as current clears all others. |
| Payment schedule not validated | Call MPaymentTerm.validate() after adding MPaySchedule records to ensure percentages sum to 100%. |
| Ignoring BP Group inheritance | Configure defaults at the BP Group level first, then override per vendor only when needed. This reduces configuration effort. |
| Discontinued products not detected | Always check isDiscontinued() and getDiscontinuedAt() before relying on a vendor-product record for new orders. |
9. Summary
- MBPartner serves as both customer and vendor, differentiated by
IsVendor/IsCustomerflags. New partners default to customer-only — vendor role must be explicitly enabled. - PO-specific fields (
PO_PriceList_ID,PO_PaymentTerm_ID,PO_DiscountSchema_ID) onC_BPartnerfall back to the BP Group via thegetBPGroup()pattern. setBPGroup()propagates group defaults to the partner and is called automatically inbeforeSave()for new records.- Credit status (
SOCreditStatus) is managed by five constants (NoCreditCheck, CreditOK, CreditWatch, CreditHold, CreditStop) with automatic recalculation insetSOCreditStatus(). - MBPartnerLocation uses four role flags:
IsShipTo,IsBillTo,IsPayFrom,IsRemitTo. AP invoices select theIsPayFromlocation; POs need bothIsShipToandIsBillTo. - MProductPO links products to vendors with comprehensive data: pricing (5 price fields), order constraints (
Order_Min,Order_Pack), delivery times, vendor catalog info, quality rating, and discontinuation tracking. - The
IsCurrentVendorconstraint inbeforeSave()ensures only one current vendor per product — used byRequisitionPOCreatefor automatic vendor selection. PriceLastPOandPriceLastInvare read-only fields (viaset_ValueNoCheck) automatically maintained by the system for price trend tracking.- MPaymentTerm supports both simple (single due date) and complex (multi-installment via
MPaySchedule) payment configurations, applied to invoices via theapply()method. - The vendor default resolution chain is: Vendor BP → BP Group → System Default, ensuring minimal per-vendor configuration when group-level defaults are properly set up.