/****************************************************************************** * Product: Adempiere ERP & CRM Smart Business Solution * * Copyright (C) 1999-2006 ComPiere, Inc. All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * * under the terms version 2 of the GNU General Public License as published * * by the Free Software Foundation. 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., * * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. * * For the text or an alternative of this public license, you may reach us * * ComPiere, Inc., 2620 Augustine Dr. #245, Santa Clara, CA 95054, USA * * or via info@compiere.org or http://www.compiere.org/license.html * *****************************************************************************/ package org.compiere.acct; import java.math.BigDecimal; import java.math.RoundingMode; import java.sql.ResultSet; import java.sql.Timestamp; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import org.compiere.model.MAccount; import org.compiere.model.MAcctSchema; import org.compiere.model.MAcctSchemaElement; import org.compiere.model.MConversionRate; import org.compiere.model.MCostDetail; import org.compiere.model.MCurrency; import org.compiere.model.MInOut; import org.compiere.model.MInOutLine; import org.compiere.model.MMatchPO; import org.compiere.model.MOrder; import org.compiere.model.MOrderLandedCostAllocation; import org.compiere.model.MOrderLine; import org.compiere.model.MProduct; import org.compiere.model.MTax; import org.compiere.model.MatchPOAutoMatch; import org.compiere.model.ProductCost; import org.compiere.model.X_M_InOut; import org.compiere.process.DocAction; import org.compiere.util.DB; import org.compiere.util.Env; import org.compiere.util.Msg; import org.compiere.util.Util; /** * Post MatchPO Documents. *
 *  Table:              C_MatchPO (473)
 *  Document Types:     MXP
 *  
* @author Jorg Janke * @version $Id: Doc_MatchPO.java,v 1.3 2006/07/30 00:53:33 jjanke Exp $ */ public class Doc_MatchPO extends Doc { /** * Constructor * @param as accounting schemata * @param rs record * @param trxName trx */ public Doc_MatchPO (MAcctSchema as, ResultSet rs, String trxName) { super(as, MMatchPO.class, rs, DOCTYPE_MatMatchPO, trxName); } // Doc_MatchPO private int m_C_OrderLine_ID = 0; private MOrderLine m_oLine = null; // private int m_M_InOutLine_ID = 0; private MInOutLine m_ioLine = null; @SuppressWarnings("unused") private int m_C_InvoiceLine_ID = 0; private ProductCost m_pc; private int m_M_AttributeSetInstance_ID = 0; private MMatchPO m_matchPO; private boolean m_deferPosting = false; /** * Load Specific Document Details * @return error message or null */ protected String loadDocumentDetails () { setC_Currency_ID (Doc.NO_CURRENCY); m_matchPO = (MMatchPO)getPO(); setDateDoc(m_matchPO.getDateTrx()); // m_M_AttributeSetInstance_ID = m_matchPO.getM_AttributeSetInstance_ID(); setQty (m_matchPO.getQty()); // m_C_OrderLine_ID = m_matchPO.getC_OrderLine_ID(); m_oLine = new MOrderLine (getCtx(), m_C_OrderLine_ID, getTrxName()); // m_M_InOutLine_ID = m_matchPO.getM_InOutLine_ID(); m_ioLine = new MInOutLine (getCtx(), m_M_InOutLine_ID, getTrxName()); m_C_InvoiceLine_ID = m_matchPO.getC_InvoiceLine_ID(); // m_pc = new ProductCost (Env.getCtx(), getM_Product_ID(), m_M_AttributeSetInstance_ID, getTrxName()); m_pc.setQty(getQty()); if (m_M_InOutLine_ID == 0) { List noInvoiceLines = new ArrayList(); Map noShipmentLines = new HashMap<>(); Map postedNoShipmentLines = new HashMap<>(); List matchPOs = MatchPOAutoMatch.getNotMatchedMatchPOList(getCtx(), m_oLine.getC_OrderLine_ID(), getTrxName()); for (MMatchPO matchPO : matchPOs) { if (matchPO.getM_InOutLine_ID() > 0 && matchPO.getC_InvoiceLine_ID() == 0 && matchPO.getReversal_ID()==0) { String docStatus = matchPO.getM_InOutLine().getM_InOut().getDocStatus(); if (docStatus.equals(DocAction.STATUS_Completed) || docStatus.equals(DocAction.STATUS_Closed)) { noInvoiceLines.add(matchPO); } } else if (matchPO.getM_InOutLine_ID() == 0 && matchPO.getReversal_ID()==0) { String docStatus = matchPO.getC_InvoiceLine().getC_Invoice().getDocStatus(); if (docStatus.equals(DocAction.STATUS_Completed) || docStatus.equals(DocAction.STATUS_Closed)) { if (matchPO.isPosted()) postedNoShipmentLines.put(matchPO.getM_MatchPO_ID(), new BigDecimal[]{matchPO.getQty()}); else noShipmentLines.put(matchPO.getM_MatchPO_ID(), new BigDecimal[]{matchPO.getQty()}); } } } for (MMatchPO matchPO : noInvoiceLines) { BigDecimal qty = matchPO.getQty(); for (Integer matchPOId : postedNoShipmentLines.keySet()) { BigDecimal[] qtyHolder = postedNoShipmentLines.get(matchPOId); if (qtyHolder[0].compareTo(qty) >= 0) { qtyHolder[0] = qtyHolder[0].subtract(qty); qty = BigDecimal.ZERO; } else if (qtyHolder[0].signum() > 0) { qty = qty.subtract(qtyHolder[0]); qtyHolder[0] = BigDecimal.ZERO; } if (qty.signum() == 0) break; } if (qty.signum() == 0) continue; for (Integer matchPOId : noShipmentLines.keySet()) { BigDecimal[] qtyHolder = noShipmentLines.get(matchPOId); if (qtyHolder[0].compareTo(qty) >= 0) { qtyHolder[0] = qtyHolder[0].subtract(qty); qty = BigDecimal.ZERO; } else if (qtyHolder[0].signum() > 0) { qty = qty.subtract(qtyHolder[0]); qtyHolder[0] = BigDecimal.ZERO; } if (qtyHolder[0].signum() == 0) { if (matchPOId == m_matchPO.getM_MatchPO_ID()) { m_M_InOutLine_ID = matchPO.getM_InOutLine_ID(); break; } } if (qty.signum() == 0) break; } if (m_M_InOutLine_ID > 0) break; } } if (m_M_InOutLine_ID == 0) // Defer posting if not matched to Shipment { if (m_matchPO.getRef_MatchPO_ID() == 0) m_deferPosting = true; } else { String posted = DB.getSQLValueStringEx(getTrxName(), "SELECT Posted FROM M_MatchPO WHERE M_MatchPO_ID=?", m_matchPO.getM_MatchPO_ID()); if (STATUS_Deferred.equals(posted)) { int M_InOut_ID = DB.getSQLValueEx(getTrxName(), "SELECT M_InOut_ID FROM M_InOutLine WHERE M_InOutLine_ID=?", m_M_InOutLine_ID); MInOut inout = new MInOut(getCtx(), M_InOut_ID, getTrxName()); if (inout.getDateAcct().after(m_matchPO.getDateAcct())) { m_matchPO.setDateAcct(inout.getDateAcct()); m_matchPO.setDateTrx(inout.getDateAcct()); setDateAcct(inout.getDateAcct()); setDateDoc(inout.getDateAcct()); m_matchPO.saveEx(); } } } return null; } // loadDocumentDetails /** * Get Source Currency Balance - subtracts line and tax amounts from total - no rounding * @return Zero - always balanced */ @Override public BigDecimal getBalance() { return Env.ZERO; } // getBalance /** * Create Facts (the accounting logic) for * MXP. *
	 *      Product PPV     
	 *      PPV_Offset                  
	 *  
* @param as accounting schema * @return Fact */ @Override public ArrayList createFacts (MAcctSchema as) { ArrayList facts = new ArrayList(); // if (getM_Product_ID() == 0 // Nothing to do if no Product || getQty().signum() == 0) { if (log.isLoggable(Level.FINE)) log.fine("No Product/Qty - M_Product_ID=" + getM_Product_ID() + ",Qty=" + getQty()); return facts; } if (m_M_InOutLine_ID == 0) { MMatchPO[] matchPOs = MMatchPO.getOrderLine(getCtx(), m_oLine.getC_OrderLine_ID(), getTrxName()); for (MMatchPO matchPO : matchPOs) { if (matchPO.getM_InOutLine_ID() > 0 && matchPO.getC_InvoiceLine_ID() == 0) { String docStatus = matchPO.getM_InOutLine().getM_InOut().getDocStatus(); if (docStatus.equals(DocAction.STATUS_Completed) || docStatus.equals(DocAction.STATUS_Closed)) { if (matchPO.getQty().compareTo(getQty()) <= 0) { m_M_InOutLine_ID = matchPO.getM_InOutLine_ID(); break; } } } } } if (m_M_InOutLine_ID == 0) // No posting if not matched to Shipment { if (m_matchPO.getRef_MatchPO_ID() > 0) return facts; p_Error = Msg.getMsg(Env.getCtx(), "NoPostingIfNotMatchedToShipment"); return null; } // create Fact Header Fact fact = new Fact(this, as, Fact.POST_Actual); setC_Currency_ID(as.getC_Currency_ID()); boolean isInterOrg = isInterOrg(as); // Purchase Order Line BigDecimal poCost = m_oLine.getPriceCost(); if (poCost == null || poCost.signum() == 0) { poCost = m_oLine.getPriceActual(); // Goodwill: Correct included Tax int C_Tax_ID = m_oLine.getC_Tax_ID(); MTax tax = MTax.get(getCtx(), C_Tax_ID); int stdPrecision = MCurrency.getStdPrecision(getCtx(), m_oLine.getC_Currency_ID()); if (m_oLine.isTaxIncluded() && C_Tax_ID != 0) { if (!tax.isZeroTax()) { BigDecimal costTax = tax.calculateTax(poCost, true, stdPrecision); if (log.isLoggable(Level.FINE)) log.fine("Costs=" + poCost + " - Tax=" + costTax); if (tax.isSummary()) { poCost = poCost.subtract(costTax); BigDecimal base = poCost; for (MTax childTax : tax.getChildTaxes(false)) { if (!childTax.isZeroTax() && childTax.isDistributeTaxWithLineItem()) { BigDecimal taxAmt = childTax.calculateTax(base, false, stdPrecision); poCost = poCost.add(taxAmt); } } } else if (!tax.isDistributeTaxWithLineItem()) { poCost = poCost.subtract(costTax); } } } // correct included Tax else { if (tax.isSummary()) { BigDecimal base = poCost; for (MTax childTax : tax.getChildTaxes(false)) { if (childTax.isDistributeTaxWithLineItem()) { BigDecimal taxAmt = childTax.calculateTax(base, false, stdPrecision); poCost = poCost.add(taxAmt); } } } else if (tax.isDistributeTaxWithLineItem()) { BigDecimal taxAmt = tax.calculateTax(poCost, false, stdPrecision); poCost = poCost.add(taxAmt); } } } MInOutLine receiptLine = new MInOutLine (getCtx(), m_M_InOutLine_ID, getTrxName()); MInOut inOut = receiptLine.getParent(); boolean isReturnTrx = inOut.getMovementType().equals(X_M_InOut.MOVEMENTTYPE_VendorReturns); Map landedCostMap = new LinkedHashMap(); BigDecimal landedCost = BigDecimal.ZERO; int C_OrderLine_ID = m_oLine.getC_OrderLine_ID(); MOrderLandedCostAllocation[] allocations = MOrderLandedCostAllocation.getOfOrderLine(C_OrderLine_ID, getTrxName()); for(MOrderLandedCostAllocation allocation : allocations) { BigDecimal totalAmt = allocation.getAmt(); BigDecimal totalQty = allocation.getQty(); BigDecimal amt = totalAmt.multiply(m_ioLine.getMovementQty()).divide(totalQty, 12, RoundingMode.HALF_UP); if (m_oLine.getC_Currency_ID() != as.getC_Currency_ID()) { MOrder order = m_oLine.getParent(); Timestamp dateAcct = inOut.getDateAcct(); BigDecimal rate = MConversionRate.getRate( order.getC_Currency_ID(), as.getC_Currency_ID(), dateAcct, order.getC_ConversionType_ID(), m_oLine.getAD_Client_ID(), m_oLine.getAD_Org_ID()); if (rate == null) { p_Error = Msg.getMsg(Env.getCtx(), "PurchaseOrderNotConvertible", new String[] {as.getName()}); return null; } amt = amt.multiply(rate); } amt = amt.divide(getQty(), 12, RoundingMode.HALF_UP); landedCost = landedCost.add(amt); if (landedCost.scale() > as.getCostingPrecision()) landedCost = landedCost.setScale(as.getCostingPrecision(), RoundingMode.HALF_UP); int elementId = allocation.getC_OrderLandedCost().getM_CostElement_ID(); BigDecimal elementAmt = landedCostMap.get(elementId); if (elementAmt == null) { elementAmt = amt; } else { elementAmt = elementAmt.add(amt); } landedCostMap.put(elementId, elementAmt); } // Different currency if (m_oLine.getC_Currency_ID() != as.getC_Currency_ID()) { MOrder order = m_oLine.getParent(); Timestamp dateAcct = inOut.getDateAcct(); BigDecimal rate = MConversionRate.getRate( order.getC_Currency_ID(), as.getC_Currency_ID(), dateAcct, order.getC_ConversionType_ID(), m_oLine.getAD_Client_ID(), m_oLine.getAD_Org_ID()); if (rate == null) { p_Error = Msg.getMsg(Env.getCtx(), "PurchaseOrderNotConvertible", new String[] {as.getName()}); return null; } poCost = poCost.multiply(rate); if (poCost.scale() > as.getCostingPrecision()) poCost = poCost.setScale(as.getCostingPrecision(), RoundingMode.HALF_UP); } String costingError = createMatchPOCostDetail(as, poCost, landedCostMap); if (costingError != null && costingError.trim().length() > 0) { p_Error = costingError; return null; } // calculate po cost BigDecimal deliveredCost = poCost.multiply(getQty()); // Delivered so far BigDecimal totalCost = deliveredCost.add(landedCost); // Calculate PPV for standard costing MProduct product = MProduct.get(getCtx(), getM_Product_ID()); String costingMethod = product.getCostingMethod(as); //get standard cost and also make sure cost for other costing method is updated BigDecimal costs = m_pc.getProductCosts(as, getAD_Org_ID(), MAcctSchema.COSTINGMETHOD_StandardCosting, m_C_OrderLine_ID, false); // non-zero costs if (MAcctSchema.COSTINGMETHOD_StandardCosting.equals(costingMethod)) { if (m_matchPO.isReversal()) { // Product PPV FactLine cr = fact.createLine(null, m_pc.getAccount(ProductCost.ACCTTYPE_P_PPV, as), as.getC_Currency_ID(), Env.ONE); if (!cr.updateReverseLine(MMatchPO.Table_ID, m_matchPO.getM_MatchPO_ID(), 0, Env.ONE)) { fact.remove(cr); cr = null; } if (cr != null) { // PPV Offset FactLine dr = fact.createLine(null, getAccount(Doc.ACCTTYPE_PPVOffset, as), as.getC_Currency_ID(), Env.ONE); if (!dr.updateReverseLine(MMatchPO.Table_ID, m_matchPO.getM_MatchPO_ID(), 0, Env.ONE, cr)) { p_Error = Msg.getMsg(Env.getCtx(), "FailedToCreateReversalEntryForACCTTYPE_PPVOffset"); return null; } } } else { // No Costs yet - no PPV if (costs == null || costs.signum() == 0) { //ok if purchase price is zero too if (m_oLine.getPriceActual().signum() == 0) { costs = BigDecimal.ZERO; } else { p_Error = Msg.getMsg(Env.getCtx(), "Resubmit - No Costs for") + product.getName(); log.log(Level.SEVERE, p_Error); return null; } } // Difference BigDecimal difference = totalCost.subtract(costs); // Nothing to post if (difference.signum() == 0) { if (log.isLoggable(Level.FINE))log.log(Level.FINE, "No Cost Difference for M_Product_ID=" + getM_Product_ID()); return facts; } // Product PPV FactLine cr = fact.createLine(null, m_pc.getAccount(ProductCost.ACCTTYPE_P_PPV, as), as.getC_Currency_ID(), isReturnTrx ? difference.negate() : difference); MAccount acct_cr = null; if (cr != null) { cr.setQty(isReturnTrx ? getQty().negate() : getQty()); cr.setC_BPartner_ID(m_oLine.getC_BPartner_ID()); cr.setC_Activity_ID(m_oLine.getC_Activity_ID()); cr.setC_Campaign_ID(m_oLine.getC_Campaign_ID()); cr.setC_Project_ID(m_oLine.getC_Project_ID()); cr.setC_ProjectPhase_ID(m_oLine.getC_ProjectPhase_ID()); cr.setC_ProjectTask_ID(m_oLine.getC_ProjectTask_ID()); cr.setC_UOM_ID(m_oLine.getC_UOM_ID()); cr.setUser1_ID(m_oLine.getUser1_ID()); cr.setUser2_ID(m_oLine.getUser2_ID()); acct_cr = cr.getAccount(); // PPV Offset } // PPV Offset FactLine dr = fact.createLine(null, getAccount(Doc.ACCTTYPE_PPVOffset, as), as.getC_Currency_ID(), isReturnTrx ? difference : difference.negate()); MAccount acct_db = null; if (dr != null) { dr.setQty(isReturnTrx ? getQty() : getQty().negate()); dr.setC_BPartner_ID(m_oLine.getC_BPartner_ID()); dr.setC_Activity_ID(m_oLine.getC_Activity_ID()); dr.setC_Campaign_ID(m_oLine.getC_Campaign_ID()); dr.setC_Project_ID(m_oLine.getC_Project_ID()); dr.setC_ProjectPhase_ID(m_oLine.getC_ProjectPhase_ID()); dr.setC_ProjectTask_ID(m_oLine.getC_ProjectTask_ID()); dr.setC_UOM_ID(m_oLine.getC_UOM_ID()); dr.setUser1_ID(m_oLine.getUser1_ID()); dr.setUser2_ID(m_oLine.getUser2_ID()); acct_db = dr.getAccount(); // PPV } // Avoid usage of clearing accounts // If both accounts Purchase Price Variance and Purchase Price Variance Offset are equal // then remove the posting if ((!as.isPostIfClearingEqual()) && acct_db!=null && acct_db.equals(acct_cr) && (!isInterOrg)) { BigDecimal debit = dr.getAmtSourceDr(); BigDecimal credit = cr.getAmtSourceCr(); if (debit.compareTo(credit) == 0) { fact.remove(dr); fact.remove(cr); } } // End Avoid usage of clearing accounts } // facts.add(fact); return facts; } else { return facts; } } // createFact /** * Verify if the posting involves two or more organizations * @return true if there are more than one org involved on the posting */ private boolean isInterOrg(MAcctSchema as) { MAcctSchemaElement elementorg = as.getAcctSchemaElement(MAcctSchemaElement.ELEMENTTYPE_Organization); if (elementorg == null || !elementorg.isBalanced()) { // no org element or not need to be balanced return false; } // verify if org of receipt line is different from org of order line // ignoring invoice line org as not used in posting if (m_ioLine != null && m_oLine != null && m_ioLine.getAD_Org_ID() != m_oLine.getAD_Org_ID()) return true; return false; } /** * Create cost detail for MatchPO * @param as * @param poCost * @param landedCostMap * @return error message or empty string */ private String createMatchPOCostDetail(MAcctSchema as, BigDecimal poCost, Map landedCostMap) { if (m_ioLine != null && m_ioLine.getM_InOutLine_ID() > 0 && m_oLine != null && m_oLine.getC_OrderLine_ID() > 0) { MMatchPO mMatchPO = (MMatchPO) getPO(); // Source from Doc_MatchPO.createFacts(MAcctSchema) MInOut inOut = m_ioLine.getParent(); boolean isReturnTrx = inOut.getMovementType().equals(X_M_InOut.MOVEMENTTYPE_VendorReturns); // Create Cost Detail Matched PO using Total Amount and Total Qty based on OrderLine MMatchPO[] mPO = MMatchPO.getOrderLine(getCtx(), m_oLine.getC_OrderLine_ID(), getTrxName()); BigDecimal tQty = Env.ZERO; BigDecimal tAmt = Env.ZERO; for (int i = 0 ; i < mPO.length ; i++) { if (mPO[i].getM_AttributeSetInstance_ID() == mMatchPO.getM_AttributeSetInstance_ID() && mPO[i].getM_MatchPO_ID() != mMatchPO.getM_MatchPO_ID()) { BigDecimal qty = (isReturnTrx ? mPO[i].getQty().negate() : mPO[i].getQty()); BigDecimal orderCost = BigDecimal.ZERO; if (mPO[i].getM_InOutLine_ID() > 0) { tQty = tQty.add(qty); //IDEMPIERE-3742 Wrong product cost for partial MR if (m_oLine.getC_Currency_ID() != as.getC_Currency_ID()) { MOrder order = m_oLine.getParent(); MProduct product = new MProduct(getCtx(), m_oLine.getM_Product_ID(), getTrxName()); if(MAcctSchema.COSTINGMETHOD_AveragePO.equals(product.getCostingMethod(as))) { orderCost = mPO[i].getM_InOutLine().getC_OrderLine().getPriceActual(); Timestamp dateAcct = mPO[i].getM_InOutLine().getM_InOut().getDateAcct(); BigDecimal rate = MConversionRate.getRate( order.getC_Currency_ID(), as.getC_Currency_ID(), dateAcct, order.getC_ConversionType_ID(), m_oLine.getAD_Client_ID(), m_oLine.getAD_Org_ID()); if (rate == null) { p_Error = "Purchase Order not convertible - " + as.getName(); return null; } orderCost = orderCost.multiply(rate); tAmt = tAmt.add(orderCost.multiply(qty)); } else { tAmt = tAmt.add(poCost.multiply(qty)); } } //IDEMPIERE-3742 Wrong product cost for partial MR else { tAmt = tAmt.add(poCost.multiply(qty)); } } } } poCost = poCost.multiply(getQty()); // Delivered so far tAmt = tAmt.add(isReturnTrx ? poCost.negate() : poCost); tQty = tQty.add(isReturnTrx ? getQty().negate() : getQty()); if (mMatchPO.isReversal()) { String error = createLandedCostAdjustments(as, landedCostMap, mMatchPO, tQty); if (!Util.isEmpty(error)) return error; } if (tAmt.scale() > as.getCostingPrecision()) tAmt = tAmt.setScale(as.getCostingPrecision(), RoundingMode.HALF_UP); // Set Total Amount and Total Quantity from Matched PO if (!MCostDetail.createOrder(as, m_oLine.getAD_Org_ID(), getM_Product_ID(), mMatchPO.getM_AttributeSetInstance_ID(), m_oLine.getC_OrderLine_ID(), 0, // no cost element tAmt, tQty, // Delivered m_oLine.getDescription(), getTrxName())) { return "SaveError"; } if (!mMatchPO.isReversal()) { String error = createLandedCostAdjustments(as, landedCostMap, mMatchPO, tQty); if (!Util.isEmpty(error)) return error; } // end MZ } return ""; } /** * Create cost detail for landed cost adjustment * @param as * @param landedCostMap * @param mMatchPO * @param tQty * @return error message or empty string */ private String createLandedCostAdjustments(MAcctSchema as, Map landedCostMap, MMatchPO mMatchPO, BigDecimal tQty) { for(Integer elementId : landedCostMap.keySet()) { BigDecimal amt = landedCostMap.get(elementId); amt = amt.multiply(tQty); if (amt.scale() > as.getCostingPrecision()) amt = amt.setScale(as.getCostingPrecision(), RoundingMode.HALF_UP); if (!MCostDetail.createOrder(as, m_oLine.getAD_Org_ID(), getM_Product_ID(), mMatchPO.getM_AttributeSetInstance_ID(), m_oLine.getC_OrderLine_ID(), elementId, amt, tQty, // Delivered m_oLine.getDescription(), getTrxName())) { return "SaveError"; } } return null; } @Override public boolean isDeferPosting() { return m_deferPosting; } } // Doc_MatchPO