Estimated Landed Cost (AveragePO)
accoring to
Issue with Average PO + Landed Cost in iDempiere
Background
The NF2.0 Estimated Landed Cost documentation mainly describes the process of handling landed cost under Average Invoice Costing.
That flow is officially supported and works correctly: estimated landed cost is defined on the Purchase Order, allocated during material receipt, cleared with invoices, and finally consolidated into product cost.
However, when switching the costing method to Average PO (Purchase Order Price) + Landed Cost, the current implementation in iDempiere shows inconsistent and incorrect behavior.
Problem Description
When using Average PO + Landed Cost, the system calculates product cost and landed cost separately in MCostDetail.
- Product Cost: derived directly from the purchase order line amount.
- Landed Cost: allocated via
MLandedCostAllocationand posted as additionalMCostDetail.
The issue is that the landed cost allocation does not only consider the current receipt quantity, but instead relies on the cumulative total quantity of the product.
As a result:
- First receipt works correctly:
- Example: Product A, PO = 20 units @ 10 = 400, Estimated Landed = 100 → Total 300.
- Receiving 2 units → cost is 10 + 5 = 15 each (OK).
- Second receipt for the same product misbehaves:
- Receiving another 2 units should also be 15 each.
- Instead, landed cost is recalculated using total 4 units, leading to 20 each (incorrect).
- Reversing a receipt does not fully revert landed cost allocation:
- Product cost is reversed, but landed cost remains partially applied.
- This leaves inconsistent cost history and wrong average cost.
for second recipt issue
modify these place can work for me
package org.compiere.model;
MCostHistory
protected static MCostHistory get(Properties ctx, int AD_Org_ID,
int M_CostType_ID, int C_AcctSchema_ID, String costingMethod, int M_CostElement_ID,
int M_AttributeSetInstance_ID, MCostDetail cd,
String trxName)
{
if (cd == null)
return null;
StringBuilder sql = new StringBuilder();
sql.append("SELECT c.* ");
sql.append("FROM M_CostHistory c ");
// sql.append("JOIN M_CostDetail cd ON (cd.M_CostDetail_ID = c.M_CostDetail_ID AND cd.Processed=?) ");
sql.append("JOIN M_CostDetail cd ON (cd.M_CostDetail_ID = c.M_CostDetail_ID ) ");
sql.append("LEFT JOIN M_CostDetail refcd ON (refcd.M_CostDetail_ID=cd.Ref_CostDetail_ID) ");
sql.append("LEFT OUTER JOIN M_CostElement ce ON (c.M_CostElement_ID=ce.M_CostElement_ID) ");
sql.append("WHERE c.AD_Client_ID=? AND c.AD_Org_ID=? ");
sql.append(" AND c.M_Product_ID=? ");
sql.append(" AND (c.M_AttributeSetInstance_ID=? OR c.M_AttributeSetInstance_ID=0) ");
sql.append(" AND c.M_CostType_ID=? AND cd.C_AcctSchema_ID=? ");
sql.append(" AND (ce.CostingMethod IS NULL OR ce.CostingMethod=?) ");
if (M_CostElement_ID > 0)
sql.append(" AND c.M_CostElement_ID=? ");
// sql.append(" AND c.M_CostDetail_ID IN (?,?) ");
sql.append(" AND c.M_CostDetail_ID IN (?,?,?) ");
sql.append(" AND c.DateAcct=? ");
sql.append("ORDER BY c.DateAcct DESC, ");
// sql.append("CASE WHEN COALESCE(refcd.DateAcct,cd.DateAcct) = cd.DateAcct THEN COALESCE(cd.Ref_CostDetail_ID, c.M_CostDetail_ID) ELSE c.M_CostDetail_ID END DESC, ");
sql.append("CASE WHEN COALESCE(refcd.DateAcct,cd.DateAcct) = cd.DateAcct THEN COALESCE(cd.Ref_CostDetail_ID, c.M_CostHistory_ID) END DESC NULLS LAST, ");
sql.append("c.M_CostHistory_ID DESC ");
String sqlStr = DB.getDatabase().addPagingSQL(sql.toString(), 1, 1);
List<Object> params = new ArrayList<Object>();
// params.add(cd.isDelta() ? "N" : "Y"); // cost detail is set to processed=N when it is a delta record
params.add(cd.getAD_Client_ID());
params.add(AD_Org_ID);
params.add(cd.getM_Product_ID());
params.add(M_AttributeSetInstance_ID);
params.add(M_CostType_ID);
params.add(C_AcctSchema_ID);
params.add(costingMethod);
if (M_CostElement_ID > 0)
params.add(M_CostElement_ID);
params.add(cd.getM_CostDetail_ID());
params.add(cd.getM_CostDetail_ID()-1); // 多的
params.add(cd.getRef_CostDetail_ID());
params.add(cd.getDateAcct());
MCostHistory costHistory = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try
{
pstmt = DB.prepareStatement(sqlStr, trxName);
DB.setParameters(pstmt, params);
rs = pstmt.executeQuery();
if (rs.next()) {
costHistory = new MCostHistory(ctx, rs, trxName);
costHistory.makeImmutable();
}
}
catch (SQLException e)
{
throw new DBException(e, sql.toString());
}
finally
{
DB.close(rs, pstmt);
rs = null; pstmt = null;
}
return costHistory;
}
MMatchPO.java
public static MMatchPO[] getOrderLine (Properties ctx, int C_OrderLine_ID, String trxName)
{
if (C_OrderLine_ID == 0)
return new MMatchPO[]{};
//
String sql = "SELECT * FROM M_MatchPO WHERE C_OrderLine_ID=? order by M_MatchPO_id asc";
......
MCostDetail.java
protected boolean process (MAcctSchema as, MProduct product, MCostElement ce,
int Org_ID, int M_ASI_ID)
{
........
boolean costAdjustment = false;
if (this.getM_CostElement_ID() > 0 && this.getM_CostElement_ID() != ce.getM_CostElement_ID())
{
MCostElement thisCostElement = MCostElement.get(getCtx(), getM_CostElement_ID());
if (thisCostElement.getCostingMethod() == null && ce.getCostingMethod() != null)
{
qty = BigDecimal.ZERO;
costAdjustment = true;
amt=getAmt();
}
}
........
[[File:MR1.png|thumb|Material Receipt 1[[File:ProductCost1.png|thumb|show cost detail ( after Material Receipt 1)
]]]] [[File:MR2.png|thumb|Material Receipt 2[[File:ProductCost2.png|thumb|
cost detail (after Material Receipt 2 ,incorrent)]]]]
