In Compiere, you use a Java Callout for "data entry consequences" - example - you select a business partner, which then populates business partner location and user as well as other specific default values like payment terms. Without a Callout, the system would just set the dependent values (location, user) to null. Consequently, Callouts are essential when you want to provide easy-to-use, interactive windows.
Callouts are available for Compiere for a long time and one of the reason for Compiere's success. The ultimate objective is to have a single controller for Swing and the WebUI with one Callout API. At this momemnt, you need to use platform specific code to create a callout.
As a example, I will use the situation to automatocally select a revenue recognition method after you selected a product - i.e. after you enter the product, the system checks if revenue recognition applies and selects the appropriate mathod, which you the could overwrite. Revenue Recognition e.g. distributes the revenue you cannot recognize immediate, but over time.
Data Dictionary
After logging in as System Administrator, open window "Table and Column" - navigate to the table and column you want to enable - here you find the Callout flag and the Callout code.
Callout for Swing UI
The steps to create a Callout for the Swing UI are:
- Create a class which
extends CalloutEngine
- Create a method with the following signature:
public String justAnExample (Ctx ctx, int WindowNo, GridTab mTab, GridField mField, Object value, Object oldValue)
- Enter the fully qualified class.method name in the Data Dictionary Callout code for the column
The business logic you provide here should be repeated on the model level to have the same experience/behavior if you were to enter/modify a record manually via UI or via API.
There are many examples for callouts in Swing - check classes like CalloutInvoice for a bit more complex logic or the class CalloutUser for basic information.
Callout for WebUI
The objective was to have all functionality in the model class, so you would just add a special setter method - example from MConversionRate:
@UICallout public void setMultiplyRate (String multiplyRateOld, String multiplyRateNew, int windowNo) throws Exception
{
setMultiplyRate(convertToBigDecimal(multiplyRateNew));
}
You would then just convert the String to the correct data type using a static method and call a standard method. This way, there is no need to replicate functionality on the model level as this is executed on the model level.
To enable the callout, you just select the Callout flag for the Column in the dictionary.
This approach is easy, if you are the owner of the object. If you want to extend an existing class, you have two options:
- If you create a component, you just enter your model package in your entity type and create in there your extension in your package - example:
public class MOrder extends org.compiere.model.MOrder
- Use an external class (details below). You may want to select this option, if you don't want to create your own extension, e.g. because another component is alreay doing that - or becuase the change is simple
To create an external [external to the model class] WebUI callout, you just create a method in a class with a parameter-less constructor and add the fully qualified class.method name in the Data Dictionary Callout code for the column - example:
public void justAnExample (PO po, UIField field, String oldValue, String newValue)
Note that you can use the same class.method name for SwingUI and WebUI - here compiere.model.CalloutUser.justAnExample
- as the parameters are different, it is easy to differentiate and allows to have the code in the same class. Note that you should use the ModelValidator to replicate the logic for the model level to achieve the same behavior via API.
Here the complete example:
/**
* Product Callout - Swing.
* Sets Revenue Rec Method based on BPartner & Product
* Could be called from Invoice and Order Line
* @param ctx context
* @param windowNo window no
* @param mTab swing tab
* @param mField swing field
* @param newValue new value
* @param oldValue old value
* @return "" or error message
*/
public String product (Ctx ctx, int windowNo, GridTab mTab, GridField mField, Object newValue, Object oldValue)
{
if (!ctx.isSOTrx(windowNo)) // Sales Only
return "";
int M_Product_ID = 0; // Product parameter
if (newValue != null) // No Product is valid for Rule
M_Product_ID = ((Integer)newValue).intValue(); // get Directly
int AREV_RevRecMethod_ID = 0;
// Get Revenue Recognition Method from Product
if (M_Product_ID > 0)
{
MProduct product = MProduct.get(ctx, M_Product_ID);
AREV_RevRecMethod_ID = product.get_ValueAsInt("AREV_RevRecMethod_ID");
}
// Get Revenue Recognition Method from Rule
if (AREV_RevRecMethod_ID == 0)
{
int C_BPartner_ID = ctx.getContextAsInt(windowNo, "Bill_BPartner_ID");
if (C_BPartner_ID == 0) // Invoice has no Bill_
C_BPartner_ID = ctx.getContextAsInt(windowNo, "C_BPartner_ID");
if (C_BPartner_ID == 0) // There should be one
return "No BPartner found"; // Error message to User
int AD_Org_ID = ctx.getContextAsInt(windowNo, "AD_Org_ID");
AREV_RevRecMethod_ID = MAEVRevRecMethodRule.getAREV_RevRecMethod_ID (ctx, ctx.getAD_Client_ID(), AD_Org_ID, C_BPartner_ID, M_Product_ID);
}
//
Integer ii = new Integer(AREV_RevRecMethod_ID);
if (AREV_RevRecMethod_ID == 0) // Explicit NULL handling
ii = null;
mTab.setValue("AREV_RevRecMethod_ID", ii);
log.config("AREV_RevRecMethod_ID=" + AREV_RevRecMethod_ID);
return "";
} // product (Swing)
/**
* Product Callout - WebUI.
* Sets Revenue Rec Method based on BPartner & Product
* Could be called from Invoice and Order Line
* @param po persistent object
* @param field changed field
* @param oldValue old value
* @param newValue new value
*/
public void product (PO po, UIField field, String oldValue, String newValue)
{
int M_Product_ID = 0; // better use: PO.convertToID(newValue);
if (!Util.isEmpty(newValue)) // null or ""
M_Product_ID = PO.convertToInt(newValue);
// Get BPartner and check that it's a sales trx
int C_BPartner_ID = 0;
if (po instanceof MOrderLine)
{
MOrderLine ol = (MOrderLine)po;
MOrder order = ol.getParent();
if (!order.isSOTrx())
return;
C_BPartner_ID = order.getBill_BPartner_ID();
}
else if (po instanceof MInvoiceLine)
{
MInvoiceLine il = (MInvoiceLine)po;
MInvoice invoice = il.getParent();
if (!invoice.isSOTrx())
return;
C_BPartner_ID = invoice.getC_BPartner_ID();
}
else
{
log.warning("Invalid PO=" + po);
return;
}
int AREV_RevRecMethod_ID = 0;
// Get Method from Product
if (M_Product_ID > 0)
{
MProduct product = MProduct.get(po.getCtx(), M_Product_ID);
AREV_RevRecMethod_ID = product.get_ValueAsInt("AREV_RevRecMethod_ID");
}
// Get Method from Rule
if (AREV_RevRecMethod_ID == 0)
{
if (C_BPartner_ID == 0)
{
if (po.get_ChangeVO() != null) // Error message to User
po.get_ChangeVO().addWarning("No BPartner found");
return;
}
AREV_RevRecMethod_ID = MAREVRevRecMethodRule.getAREV_RevRecMethod_ID (po.getCtx(), po.getAD_Client_ID(), po.getAD_Org_ID(), C_BPartner_ID, M_Product_ID);
}
// Set value via generic method for custom column
Integer ii = new Integer(AREV_RevRecMethod_ID);
if (AREV_RevRecMethod_ID == 0) // Explicit NULL handling
ii = null;
po.set_Value("AREV_RevRecMethod_ID", ii); // Optional
log.config("AREV_RevRecMethod_ID=" + AREV_RevRecMethod_ID);
} // product (WebUI)
One of the remaining challenges is to automatically change the core system for Application Components. Example: After installing the component Revenue Recognition, all the fieds and code is included, but the callout needs to enabled manually (i.e. the callout class/method "...arev.CalloutRevRec.product" needs to be added to the existing column M_Product_ID in the table C_InvoiceLine and C_OrderLine). In your install routine, you could just add e.g. MColumn.checkCallout(MOrderLine.Table_ID, "M_Product_ID", "...arev.CalloutRevRec.product")
to ensure that the callout link is added to the dictionary.
The functionality discussed here is available with release 3.2.1. For questions, just contact us.