/*********************************************************************** * This file is part of iDempiere ERP Open Source * * http://www.idempiere.org * * * * Copyright (C) Contributors * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 2 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * * MA 02110-1301, USA. * * * * Contributors: * * - hengsin * **********************************************************************/ package org.compiere.process; import java.math.BigDecimal; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.Savepoint; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import javax.sql.RowSet; import org.compiere.model.MClient; import org.compiere.model.MCost; import org.compiere.model.MCostElement; import org.compiere.model.MDocType; import org.compiere.model.MInventory; import org.compiere.model.MInventoryLine; import org.compiere.model.MProcessPara; import org.compiere.model.MProduct; import org.compiere.util.DB; import org.compiere.util.Env; import org.compiere.util.Msg; import org.compiere.util.Trx; import org.compiere.util.Util; import org.compiere.wf.MWorkflow; /** * * @author hengsin * */ @org.adempiere.base.annotation.Process public class RollUpCosts extends SvrProcess { private int category = 0; private int product_id = 0; private int costelement_id = 0; private int charge_id = 0; private HashSet processed; private Map inventoryDocs = new HashMap<>(); private Map inventoryLines = new HashMap<>(); private MDocType adjustmentDocType = null; @Override protected void prepare() { int chosen_id = 0; ProcessInfoParameter[] para = getParameter(); for (int i = 0; i < para.length; i++) { String name = para[i].getParameterName(); if (para[i].getParameter() == null) ; else if (name.equals("M_Product_Category_ID")) category = para[i].getParameterAsInt(); else if (name.equals("M_Product_ID")) chosen_id = para[i].getParameterAsInt(); else if (name.equals("M_CostElement_ID")) costelement_id = para[i].getParameterAsInt(); else if (name.equals("C_Charge_ID")) charge_id = para[i].getParameterAsInt(); else MProcessPara.validateUnknownParameter(getProcessInfo().getAD_Process_ID(), para[i]); } if (getTable_ID() == MProduct.Table_ID) { product_id = getRecord_ID(); if (product_id == 0) { product_id = chosen_id; } } else { product_id = chosen_id; } } @Override protected String doIt() throws Exception { processed = new HashSet(); MDocType[] doctypes = MDocType.getOfDocBaseType(getCtx(), MDocType.DOCBASETYPE_MaterialPhysicalInventory); for(MDocType dt : doctypes) { if (MDocType.DOCSUBTYPEINV_CostAdjustment.equals(dt.getDocSubTypeInv())) { adjustmentDocType = dt; break; } } if (adjustmentDocType == null) throw new IllegalStateException("Failed to find cost adjustment document type"); String result = rollUp(); if (inventoryDocs.size() > 0) { for(MInventory costingDoc : inventoryDocs.values()) { ProcessInfo info = MWorkflow.runDocumentActionWorkflow(costingDoc, DocAction.ACTION_Complete); if (info.isError()) { StringBuilder msg = new StringBuilder(); msg.append(Msg.getMsg(getCtx(), "ProcessFailed")).append(": "); msg.append(info.getSummary()); addLog(getAD_PInstance_ID(), null, null, msg.toString()); } else { costingDoc.saveEx(); addBufferLog(getAD_PInstance_ID(), null, null, costingDoc.toString(), MInventory.Table_ID, costingDoc.get_ID()); } } } return result; } /** * do rollup * @return number of records updated * @throws Exception */ protected String rollUp() throws Exception { int count = 0; if (product_id != 0) //only for the product { String error = rollUpCosts(product_id); if (!Util.isEmpty(error)) { addLog(getAD_PInstance_ID(), null, null, "Rollup BOM Cost is not applicable for the product " + MProduct.get(getCtx(), product_id).getName() + ". Details: " + error, MProduct.Table_ID, product_id); } else { count++; } } else if (category != 0) //roll up for all categories { String sql = "SELECT M_PRODUCT_ID FROM M_PRODUCT WHERE M_PRODUCT_CATEGORY_ID = " + category + " AND AD_CLIENT_ID = " + getAD_Client_ID() + " AND M_PRODUCT_ID IN (SELECT b.M_PRODUCT_ID FROM PP_PRODUCT_BOM b " + " JOIN PP_PRODUCT_BOMLINE bl ON b.PP_PRODUCT_BOM_ID = bl.PP_PRODUCT_BOM_ID" + " WHERE b.AD_Client_ID=" + getAD_Client_ID() +" AND b.IsActive='Y' AND bl.IsActive='Y' AND b.BOMType='A' AND b.BOMUse='A')"; Trx trx = Trx.get(get_TrxName(), false); RowSet results = null; try { results = DB.getRowSet(sql); while (results.next()) { Savepoint savepoint = trx.setSavepoint(null); int id= results.getInt(1); String error = rollUpCosts(id); if (!Util.isEmpty(error)) { addLog(getAD_PInstance_ID(), null, null, "Rollup BOM Cost is not applicable for the product " + MProduct.get(getCtx(), id).getName() + ". Details: " + error, MProduct.Table_ID, product_id); trx.rollback(savepoint); } else { trx.releaseSavepoint(savepoint); count++; } } } finally { DB.close(results); } } else //do it for all products { String sql = "SELECT M_PRODUCT_ID FROM M_PRODUCT WHERE AD_CLIENT_ID = " + getAD_Client_ID() + " AND M_PRODUCT_ID IN (SELECT b.M_PRODUCT_ID FROM PP_PRODUCT_BOM b " + " JOIN PP_PRODUCT_BOMLINE bl ON b.PP_PRODUCT_BOM_ID = bl.PP_PRODUCT_BOM_ID" + " WHERE b.AD_Client_ID=" + getAD_Client_ID() +" AND b.IsActive='Y' AND bl.IsActive='Y' AND b.BOMType='A' AND b.BOMUse='A')"; Trx trx = Trx.get(get_TrxName(), false); RowSet results = null; try { results = DB.getRowSet(sql); while (results.next()) { Savepoint savepoint = trx.setSavepoint(null); int id= results.getInt(1); String error = rollUpCosts(id); if (!Util.isEmpty(error)) { addLog(getAD_PInstance_ID(), null, null, "Rollup BOM Cost is not applicable for the product " + MProduct.get(getCtx(), id).getName() + ". Details: " + error, MProduct.Table_ID, product_id); trx.rollback(savepoint); } else { trx.releaseSavepoint(savepoint); count++; } } } finally { DB.close(results); } } return count + " Product Cost Updated."; } /** * * @param productId * @return error message (if any) * @throws Exception */ protected String rollUpCosts(int productId) throws Exception { String sql = "SELECT bl.M_Product_ID FROM PP_Product_BOMLine bl " + " JOIN PP_PRODUCT_BOM b " + " ON ( b.PP_PRODUCT_BOM_ID = bl.PP_PRODUCT_BOM_ID ) WHERE b.M_Product_ID = ?" + " AND b.AD_Client_ID = " + getAD_Client_ID() + " AND b.BOMUse='A' AND b.BOMType='A' AND b.IsActive='Y' AND bl.IsActive='Y' "; int[] prodbomids = DB.getIDsEx(get_TrxName(), sql.toString(), productId); for (int prodbomid : prodbomids) { if ( !processed.contains(prodbomid)) { String error = rollUpCosts(prodbomid); if (!Util.isEmpty(error)) return error; } } MProduct product = new MProduct(getCtx(), productId, get_TrxName()); PreparedStatement pstmt = null; ResultSet rs = null; try { //once the subproducts costs are accurate, calculate the costs for this product StringBuilder productCostSql = new StringBuilder("SELECT * FROM M_Cost ") .append(" WHERE M_Product_ID = ").append(productId).append(" AND AD_Client_ID = ").append(getAD_Client_ID()) .append(" AND M_CostElement_ID = ").append(costelement_id) .append(" AND C_AcctSchema_ID = ").append(MClient.get(getAD_Client_ID()).getAcctSchema().getC_AcctSchema_ID()) .append(" AND M_PRODUCT_ID IN (SELECT b.M_PRODUCT_ID FROM PP_PRODUCT_BOM b ") .append(" JOIN PP_PRODUCT_BOMLINE bl ON b.PP_PRODUCT_BOM_ID = bl.PP_PRODUCT_BOM_ID") .append(" WHERE b.BOMType='A' AND b.BOMUse='A' AND b.IsActive='Y' ") .append(" AND b.AD_Client_ID=").append(getAD_Client_ID()).append(")"); pstmt = DB.prepareStatement(productCostSql.toString(), get_TrxName()); rs = pstmt.executeQuery(); while (rs.next()) { MCost cost = new MCost(getCtx(), rs, get_TrxName()); StringBuilder newCurrentCostPriceSql = new StringBuilder("SELECT SUM(b.QtyBOM * c.CurrentCostPrice)") .append(" FROM PP_Product_BOMLine b ") .append(" INNER JOIN M_Cost c ON (b.M_PRODUCT_ID = c.M_Product_ID) ") .append(" JOIN PP_Product_BOM bom ON (bom.PP_PRODUCT_BOM_ID = b.PP_PRODUCT_BOM_ID AND bom.IsActive='Y') ") .append(" WHERE bom.M_Product_ID = ").append(productId).append(" AND bom.BOMType='A' AND bom.BOMUse='A' AND M_CostElement_ID = ").append(costelement_id) .append(" AND c.C_AcctSchema_ID = ").append(cost.getC_AcctSchema_ID()); BigDecimal newCurrentCostPrice = DB.getSQLValueBD(get_TrxName(), newCurrentCostPriceSql.toString()); if (newCurrentCostPrice == null) newCurrentCostPrice = BigDecimal.ZERO; StringBuilder newFutureCostPriceSql = new StringBuilder("SELECT SUM(b.QtyBOM * c.FutureCostPrice)") .append(" FROM PP_Product_BOMLine b ") .append(" INNER JOIN M_Cost c ON (b.M_PRODUCT_ID = c.M_Product_ID) ") .append(" JOIN PP_Product_BOM bom ON (bom.PP_PRODUCT_BOM_ID = b.PP_PRODUCT_BOM_ID AND bom.IsActive='Y') ") .append(" WHERE bom.M_Product_ID = ").append(productId).append(" AND M_CostElement_ID = ").append(costelement_id) .append(" AND c.C_AcctSchema_ID = ").append(cost.getC_AcctSchema_ID()); BigDecimal newFutureCostPrice = DB.getSQLValueBD(get_TrxName(), newFutureCostPriceSql.toString()); if (newFutureCostPrice == null) newFutureCostPrice = BigDecimal.ZERO; StringBuilder qtyOnHandSql = new StringBuilder("SELECT SUM(QtyOnHand)") .append(" FROM M_StorageOnHand oh ") .append(" JOIN M_Locator loc ON (oh.M_Locator_ID=loc.M_Locator_ID)") .append(" JOIN M_Warehouse w ON (w.M_Warehouse_ID=loc.M_Warehouse_ID)") .append(" WHERE oh.M_Product_ID=").append(productId) .append(" AND w.IsActive='Y'") .append(" AND w.AD_Client_ID=").append(getAD_Client_ID()); if (cost.getM_AttributeSetInstance_ID() != 0) qtyOnHandSql.append(" AND oh.M_AttributeSetInstance_ID=").append(cost.getM_AttributeSetInstance_ID()); BigDecimal qtyOnHand = DB.getSQLValueBD(get_TrxName(), qtyOnHandSql.toString()); if (qtyOnHand == null) qtyOnHand = BigDecimal.ZERO; if (cost.getCurrentCostPrice().compareTo(newCurrentCostPrice) != 0) { MInventory costingDoc = createCostingDoc(product); String key = costingDoc.getM_Inventory_ID() + "_" + cost.getM_Product_ID() + "_" + cost.getM_AttributeSetInstance_ID(); MInventoryLine costingLine = inventoryLines.get(key); if (costingLine == null) { costingLine = new MInventoryLine(getCtx(), 0, get_TrxName()); costingLine.setAD_Org_ID(costingDoc.getAD_Org_ID()); costingLine.setM_Inventory_ID(costingDoc.getM_Inventory_ID()); costingLine.setM_Product_ID(cost.getM_Product_ID()); costingLine.setM_AttributeSetInstance_ID(cost.getM_AttributeSetInstance_ID()); costingLine.setC_Charge_ID(charge_id); costingLine.setCurrentCostPrice(cost.getCurrentCostPrice()); costingLine.setM_Locator_ID(0); } costingLine.setNewCostPrice(newCurrentCostPrice); costingLine.saveEx(); inventoryLines.put(key, costingLine); } if (cost.getFutureCostPrice().compareTo(newFutureCostPrice) != 0) { cost.setFutureCostPrice(newFutureCostPrice); } if (cost.is_Changed()) cost.saveEx(); } } finally { DB.close(rs, pstmt); } processed.add(productId); return null; } private MInventory createCostingDoc(MProduct product) { int AD_Org_ID = product.getAD_Org_ID() > 0 ? product.getAD_Org_ID() : Env.getAD_Org_ID(getCtx()); MInventory costingDoc = inventoryDocs.get(AD_Org_ID); if (costingDoc == null) { costingDoc = new MInventory(getCtx(), 0, get_TrxName()); costingDoc.setAD_Org_ID(AD_Org_ID); costingDoc.setDescription("Created due to rollup BOM cost process ID " + getAD_PInstance_ID()); costingDoc.setC_DocType_ID(adjustmentDocType.getC_DocType_ID()); costingDoc.setCostingMethod(MCostElement.get(getCtx(), costelement_id).getCostingMethod()); costingDoc.setDocAction(DocAction.ACTION_Complete); costingDoc.saveEx(); inventoryDocs.put(AD_Org_ID, costingDoc); } return costingDoc; } }