/******************************************************************************
 * 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.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import org.adempiere.exceptions.AverageCostingZeroQtyException;
import org.compiere.model.I_M_InOutLine;
import org.compiere.model.MAccount;
import org.compiere.model.MAcctSchema;
import org.compiere.model.MClientInfo;
import org.compiere.model.MConversionRate;
import org.compiere.model.MCost;
import org.compiere.model.MCostDetail;
import org.compiere.model.MCostElement;
import org.compiere.model.MCurrency;
import org.compiere.model.MFactAcct;
import org.compiere.model.MInvoice;
import org.compiere.model.MInvoiceLine;
import org.compiere.model.MLandedCostAllocation;
import org.compiere.model.MOrderLandedCostAllocation;
import org.compiere.model.MTax;
import org.compiere.model.ProductCost;
import org.compiere.model.Query;
import org.compiere.model.X_M_Cost;
import org.compiere.util.DB;
import org.compiere.util.Env;
import org.compiere.util.Trx;
/**
 *  Post Invoice Documents.
 *  
 *  Table:              C_Invoice (318)
 *  Document Types:     ARI, ARC, ARF, API, APC
 *  
 *  @author Jorg Janke
 *  @author Armen Rizal, Goodwill Consulting
 *  	BF: 2797257	Landed Cost Detail is not using allocation qty
 *
 *  @version  $Id: Doc_Invoice.java,v 1.2 2006/07/30 00:53:33 jjanke Exp $
 */
public class Doc_Invoice extends Doc
{
	/**
	 *  Constructor
	 * 	@param as accounting schemata
	 * 	@param rs record
	 * 	@param trxName trx
	 */
	public Doc_Invoice(MAcctSchema as, ResultSet rs, String trxName)
	{
		super (as, MInvoice.class, rs, null, trxName);
	}	//	Doc_Invoice
	/** Contained Optional Tax Lines    */
	protected DocTax[]        m_taxes = null;
	/** Contained Optional Tax Lines Distributed to Line Item */
	@SuppressWarnings("unused")
	private DocTax[]        m_addToLineTaxes = null;
	/** Currency Precision				*/
	protected int				m_precision = -1;
	/** All lines are Service			*/
	protected boolean			m_allLinesService = true;
	/** All lines are product item		*/
	protected boolean			m_allLinesItem = true;
	/**
	 *  Load Specific Document Details
	 *  @return error message or null
	 */
	@Override
	protected String loadDocumentDetails ()
	{
		MInvoice invoice = (MInvoice)getPO();
		setDateDoc(invoice.getDateInvoiced());
		setIsTaxIncluded(invoice.isTaxIncluded());
		//	Amounts
		setAmount(Doc.AMTTYPE_Gross, invoice.getGrandTotal());
		setAmount(Doc.AMTTYPE_Net, invoice.getTotalLines());
		setAmount(Doc.AMTTYPE_Charge, invoice.getChargeAmt());
		//	Contained Objects
		m_taxes = loadTaxes();
		p_lines = loadLines(invoice);
		if (log.isLoggable(Level.FINE)) log.fine("Lines=" + p_lines.length + ", Taxes=" + m_taxes.length);
		return null;
	}   //  loadDocumentDetails
	/**
	 *	Load Invoice Taxes
	 *  @return DocTax Array
	 */
	private DocTax[] loadTaxes()
	{
		ArrayList list = new ArrayList();
		ArrayList distributeList = new ArrayList();
		String sql = "SELECT it.C_Tax_ID, t.Name, t.Rate, it.TaxBaseAmt, it.TaxAmt, t.IsSalesTax "
				+ "FROM C_Tax t, C_InvoiceTax it "
				+ "WHERE t.C_Tax_ID=it.C_Tax_ID AND it.C_Invoice_ID=?";
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try
		{
			pstmt = DB.prepareStatement(sql, getTrxName());
			pstmt.setInt(1, get_ID());
			rs = pstmt.executeQuery();
			//
			while (rs.next())
			{
				int C_Tax_ID = rs.getInt(1);
				String name = rs.getString(2);
				BigDecimal rate = rs.getBigDecimal(3);
				BigDecimal taxBaseAmt = rs.getBigDecimal(4);
				BigDecimal amount = rs.getBigDecimal(5);
				boolean salesTax = "Y".equals(rs.getString(6));
				//
				MTax tax = MTax.get(getCtx(), C_Tax_ID);
				DocTax taxLine = new DocTax(C_Tax_ID, name, rate,
					taxBaseAmt, amount, salesTax);
				if (log.isLoggable(Level.FINE)) log.fine(taxLine.toString());
				if (!tax.isDistributeTaxWithLineItem())
				{					
					list.add(taxLine);
				}
				else
				{
					distributeList.add(taxLine);
				}
			}
		}
		catch (SQLException e)
		{
			log.log(Level.SEVERE, sql, e);
			return null;
		}
		finally {
			DB.close(rs, pstmt);
			rs = null; pstmt = null;
		}
		//	Return Array
		DocTax[] tl = new DocTax[list.size()];
		list.toArray(tl);
		//	Distribute list
		m_addToLineTaxes = distributeList.toArray(new DocTax[0]);
		
		return tl;
	}	//	loadTaxes
	/**
	 *	Load Invoice Line
	 *	@param invoice invoice
	 *  @return DocLine Array
	 */
	private DocLine[] loadLines (MInvoice invoice)
	{
		ArrayList list = new ArrayList();
		//
		MInvoiceLine[] lines = invoice.getLines(false);
		for (int i = 0; i < lines.length; i++)
		{
			MInvoiceLine line = lines[i];
			if (line.isDescription())
				continue;
			DocLine docLine = new DocLine(line, this);
			//	Qty
			BigDecimal Qty = line.getQtyInvoiced();
			boolean cm = getDocumentType().equals(DOCTYPE_ARCredit)
				|| getDocumentType().equals(DOCTYPE_APCredit);
			docLine.setQty(cm ? Qty.negate() : Qty, invoice.isSOTrx());
			//
			BigDecimal LineNetAmt = line.getLineNetAmt();
			BigDecimal PriceList = line.getPriceList();
			int C_Tax_ID = docLine.getC_Tax_ID();
			//	Correct included Tax
			if (isTaxIncluded() && C_Tax_ID != 0)
			{
				MTax tax = MTax.get(getCtx(), C_Tax_ID);
				if (!tax.isZeroTax())
				{
					BigDecimal LineNetAmtTax = tax.calculateTax(LineNetAmt, true, getStdPrecision());
					if (log.isLoggable(Level.FINE)) log.fine("LineNetAmt=" + LineNetAmt + " - Tax=" + LineNetAmtTax);
					if (tax.isSummary()) {
						LineNetAmt = LineNetAmt.subtract(LineNetAmtTax);
						BigDecimal base = LineNetAmt;
						BigDecimal sumChildLineNetAmtTax = Env.ZERO;
						DocTax taxToApplyDiff = null;
						for (MTax childTax : tax.getChildTaxes(false)) {
							if (!childTax.isZeroTax())
							{
								BigDecimal childLineNetAmtTax = childTax.calculateTax(base, false, getStdPrecision());
								if (log.isLoggable(Level.FINE)) log.fine("LineNetAmt=" + base + " - Child Tax=" + childLineNetAmtTax);
								if (childTax.isDistributeTaxWithLineItem())
								{
									LineNetAmt = LineNetAmt.add(childLineNetAmtTax);
									LineNetAmtTax = LineNetAmtTax.subtract(childLineNetAmtTax);
								}
								else
								{
									for (int t = 0; t < m_taxes.length; t++)
									{
										if (m_taxes[t].getC_Tax_ID() == childTax.getC_Tax_ID())
										{
											m_taxes[t].addIncludedTax(childLineNetAmtTax);
											taxToApplyDiff = m_taxes[t];
											sumChildLineNetAmtTax = sumChildLineNetAmtTax.add(childLineNetAmtTax);
											break;
										}
									}
								}
							}
						}
						BigDecimal diffChildVsSummary = LineNetAmtTax.subtract(sumChildLineNetAmtTax);
						if (diffChildVsSummary.signum() != 0 && taxToApplyDiff != null) {
							taxToApplyDiff.addIncludedTax(diffChildVsSummary);
						}
					} else {
						if (!tax.isDistributeTaxWithLineItem())
						{
							LineNetAmt = LineNetAmt.subtract(LineNetAmtTax);
							for (int t = 0; t < m_taxes.length; t++)
							{
								if (m_taxes[t].getC_Tax_ID() == C_Tax_ID)
								{
									m_taxes[t].addIncludedTax(LineNetAmtTax);
									break;
								}
							}
						}
					}
					
					BigDecimal PriceListTax = tax.calculateTax(PriceList, true, getStdPrecision());
					PriceList = PriceList.subtract(PriceListTax);
				}
			}	//	correct included Tax
			else
			{
				int stdPrecision = MCurrency.getStdPrecision(getCtx(), invoice.getC_Currency_ID());
				MTax tax = MTax.get(getCtx(), C_Tax_ID);
				if (tax.isSummary())
				{
					MTax[] cTaxes = tax.getChildTaxes(false);
					BigDecimal base = LineNetAmt;
					for(MTax cTax : cTaxes)
					{
						if (cTax.isDistributeTaxWithLineItem())
						{
							BigDecimal taxAmt = cTax.calculateTax(base, false, stdPrecision);
							LineNetAmt = LineNetAmt.add(taxAmt);
						}
					}
				}
				else if (tax.isDistributeTaxWithLineItem())
				{
					BigDecimal taxAmt = tax.calculateTax(LineNetAmt, false, stdPrecision);
					LineNetAmt = LineNetAmt.add(taxAmt);
				}
			}
			docLine.setAmount (LineNetAmt, PriceList, Qty);	//	qty for discount calc
			if (docLine.isItem())
				m_allLinesService = false;
			else
				m_allLinesItem = false;
			//
			if (log.isLoggable(Level.FINE)) log.fine(docLine.toString());
			list.add(docLine);
		}
		//	Convert to Array
		DocLine[] dls = new DocLine[list.size()];
		list.toArray(dls);
		//	Included Tax - make sure that no difference
		if (isTaxIncluded())
		{
			for (int i = 0; i < m_taxes.length; i++)
			{
				if (m_taxes[i].isIncludedTaxDifference())
				{
					BigDecimal diff = m_taxes[i].getIncludedTaxDifference();
					for (int j = 0; j < dls.length; j++)
					{
						MTax lineTax = MTax.get(getCtx(), dls[j].getC_Tax_ID());
						MTax[] composingTaxes = null;
						if (lineTax.isSummary()) {
							composingTaxes = lineTax.getChildTaxes(false);
						} else {
							composingTaxes = new MTax[1];
							composingTaxes[0] = lineTax;
						}
						for (MTax mTax : composingTaxes) {
							if (mTax.getC_Tax_ID() == m_taxes[i].getC_Tax_ID())
							{
								dls[j].setLineNetAmtDifference(diff);
								m_taxes[i].addIncludedTax(diff.negate());
								diff = Env.ZERO;
								break;
							}
						}
						if (diff.signum() == 0) {
							break;
						}
					}	//	for all lines
				}	//	tax difference
			}	//	for all taxes
		}	//	Included Tax difference
		//	Return Array
		return dls;
	}	//	loadLines
	/**
	 * 	Get Currency Precision
	 *	@return precision
	 */
	private int getStdPrecision()
	{
		if (m_precision == -1)
			m_precision = MCurrency.getStdPrecision(getCtx(), getC_Currency_ID());
		return m_precision;
	}	//	getPrecision
	/**
	 *  Get Source Currency Balance - subtracts line and tax amounts from total - no rounding
	 *  @return positive amount, if total invoice is bigger than lines
	 */
	@Override
	public BigDecimal getBalance()
	{
		BigDecimal retValue = Env.ZERO;
		StringBuilder sb = new StringBuilder (" [");
		//  Total
		retValue = retValue.add(getAmount(Doc.AMTTYPE_Gross));
		sb.append(getAmount(Doc.AMTTYPE_Gross));
		//  - Header Charge
		retValue = retValue.subtract(getAmount(Doc.AMTTYPE_Charge));
		sb.append("-").append(getAmount(Doc.AMTTYPE_Charge));
		//  - Tax
		for (int i = 0; i < m_taxes.length; i++)
		{
			retValue = retValue.subtract(m_taxes[i].getAmount());
			sb.append("-").append(m_taxes[i].getAmount());
		}
		//  - Lines
		for (int i = 0; i < p_lines.length; i++)
		{
			retValue = retValue.subtract(p_lines[i].getAmtSource());
			sb.append("-").append(p_lines[i].getAmtSource());
		}
		sb.append("]");
		//
		if (log.isLoggable(Level.FINE)) log.fine(toString() + " Balance=" + retValue + sb.toString());
		return retValue;
	}   //  getBalance
	/**
	 *  Create Facts (the accounting logic) for
	 *  ARI, ARC, ARF, API, APC.
	 *  
	 *  ARI, ARF
	 *      Receivables     DR
	 *      Charge                  CR
	 *      TaxDue                  CR
	 *      Revenue                 CR
	 *
	 *  ARC
	 *      Receivables             CR
	 *      Charge          DR
	 *      TaxDue          DR
	 *      Revenue         RR
	 *
	 *  API
	 *      Payables                CR
	 *      Charge          DR
	 *      TaxCredit       DR
	 *      Expense         DR
	 *
	 *  APC
	 *      Payables        DR
	 *      Charge                  CR
	 *      TaxCredit               CR
	 *      Expense                 CR
	 *  
	 *  @param as accounting schema
	 *  @return Fact
	 */
	@Override
	public ArrayList createFacts (MAcctSchema as)
	{
		//
		ArrayList facts = new ArrayList();
		//  create Fact Header
		Fact fact = new Fact(this, as, Fact.POST_Actual);
		//  Cash based accounting
		if (!as.isAccrual())
			return facts;
		//  ** ARI, ARF
		if (getDocumentType().equals(DOCTYPE_ARInvoice)
			|| getDocumentType().equals(DOCTYPE_ARProForma))
		{			
			BigDecimal grossAmt = getAmount(Doc.AMTTYPE_Gross);
			BigDecimal serviceAmt = Env.ZERO;
			//  Header Charge           CR
			BigDecimal amt = getAmount(Doc.AMTTYPE_Charge);
			if (amt != null && amt.signum() != 0)
				fact.createLine(null, getAccount(Doc.ACCTTYPE_Charge, as),
					getC_Currency_ID(), null, amt);
			//  TaxDue                  CR
			for (int i = 0; i < m_taxes.length; i++)
			{
				amt = m_taxes[i].getAmount();
				if (amt != null && amt.signum() != 0)
				{
					FactLine tl = fact.createLine(null, m_taxes[i].getAccount(DocTax.ACCTTYPE_TaxDue, as),
						getC_Currency_ID(), null, amt);
					if (tl != null)
						tl.setC_Tax_ID(m_taxes[i].getC_Tax_ID());					
				}
			}
			//  Revenue                 CR
			for (int i = 0; i < p_lines.length; i++)
			{
				amt = p_lines[i].getAmtSource();
				BigDecimal dAmt = null;
				if (as.isTradeDiscountPosted())
				{
					BigDecimal discount = p_lines[i].getDiscount();
					if (discount != null && discount.signum() != 0)
					{
						amt = amt.add(discount);
						dAmt = discount;
						fact.createLine (p_lines[i],
								p_lines[i].getAccount(ProductCost.ACCTTYPE_P_TDiscountGrant, as),
								getC_Currency_ID(), dAmt, null);
					}
				}
				fact.createLine (p_lines[i],
					p_lines[i].getAccount(ProductCost.ACCTTYPE_P_Revenue, as),
					getC_Currency_ID(), null, amt);
				if (!p_lines[i].isItem())
				{
					grossAmt = grossAmt.subtract(amt);
					serviceAmt = serviceAmt.add(amt);
				}
			}
			//  Receivables     DR
			int receivables_ID = getValidCombination_ID(Doc.ACCTTYPE_C_Receivable, as);
			int receivablesServices_ID = receivables_ID; // Receivable Services account Deprecated IDEMPIERE-362
			if (m_allLinesItem || !as.isPostServices()
				|| receivables_ID == receivablesServices_ID)
			{
				grossAmt = getAmount(Doc.AMTTYPE_Gross);
				serviceAmt = Env.ZERO;
			}
			else if (m_allLinesService)
			{
				serviceAmt = getAmount(Doc.AMTTYPE_Gross);
				grossAmt = Env.ZERO;
			}
			if (grossAmt.signum() != 0)
				fact.createLine(null, MAccount.get(getCtx(), receivables_ID),
					getC_Currency_ID(), grossAmt, null);
			if (serviceAmt.signum() != 0)
				fact.createLine(null, MAccount.get(getCtx(), receivablesServices_ID),
					getC_Currency_ID(), serviceAmt, null);
			//  Set Locations
			FactLine[] fLines = fact.getLines();
			for (int i = 0; i < fLines.length; i++)
			{
				if (fLines[i] != null)
				{
					fLines[i].setLocationFromOrg(fLines[i].getAD_Org_ID(), true);      //  from Loc
					fLines[i].setLocationFromBPartner(getC_BPartner_Location_ID(), false);  //  to Loc
				}
			}
		}
		//  ARC
		else if (getDocumentType().equals(DOCTYPE_ARCredit))
		{
			BigDecimal grossAmt = getAmount(Doc.AMTTYPE_Gross);
			BigDecimal serviceAmt = Env.ZERO;
			//  Header Charge   DR
			BigDecimal amt = getAmount(Doc.AMTTYPE_Charge);
			if (amt != null && amt.signum() != 0)
				fact.createLine(null, getAccount(Doc.ACCTTYPE_Charge, as),
					getC_Currency_ID(), amt, null);
			//  TaxDue          DR
			for (int i = 0; i < m_taxes.length; i++)
			{
				amt = m_taxes[i].getAmount();
				if (amt != null && amt.signum() != 0)
				{
					FactLine tl = fact.createLine(null, m_taxes[i].getAccount(DocTax.ACCTTYPE_TaxDue, as),
						getC_Currency_ID(), amt, null);
					if (tl != null)
						tl.setC_Tax_ID(m_taxes[i].getC_Tax_ID());
				}
			}
			//  Revenue         CR
			for (int i = 0; i < p_lines.length; i++)
			{
				amt = p_lines[i].getAmtSource();
				BigDecimal dAmt = null;
				if (as.isTradeDiscountPosted())
				{
					BigDecimal discount = p_lines[i].getDiscount();
					if (discount != null && discount.signum() != 0)
					{
						amt = amt.add(discount);
						dAmt = discount;
						fact.createLine (p_lines[i],
								p_lines[i].getAccount (ProductCost.ACCTTYPE_P_TDiscountGrant, as),
								getC_Currency_ID(), null, dAmt);
					}
				}
				fact.createLine (p_lines[i],
					p_lines[i].getAccount (ProductCost.ACCTTYPE_P_Revenue, as),
					getC_Currency_ID(), amt, null);
				if (!p_lines[i].isItem())
				{
					grossAmt = grossAmt.subtract(amt);
					serviceAmt = serviceAmt.add(amt);
				}
			}
			//  Receivables             CR
			int receivables_ID = getValidCombination_ID (Doc.ACCTTYPE_C_Receivable, as);
			int receivablesServices_ID = receivables_ID; // Receivable Services account Deprecated IDEMPIERE-362
			if (m_allLinesItem || !as.isPostServices()
				|| receivables_ID == receivablesServices_ID)
			{
				grossAmt = getAmount(Doc.AMTTYPE_Gross);
				serviceAmt = Env.ZERO;
			}
			else if (m_allLinesService)
			{
				serviceAmt = getAmount(Doc.AMTTYPE_Gross);
				grossAmt = Env.ZERO;
			}
			if (grossAmt.signum() != 0)
				fact.createLine(null, MAccount.get(getCtx(), receivables_ID),
					getC_Currency_ID(), null, grossAmt);
			if (serviceAmt.signum() != 0)
				fact.createLine(null, MAccount.get(getCtx(), receivablesServices_ID),
					getC_Currency_ID(), null, serviceAmt);
			//  Set Locations
			FactLine[] fLines = fact.getLines();
			for (int i = 0; i < fLines.length; i++)
			{
				if (fLines[i] != null)
				{
					fLines[i].setLocationFromOrg(fLines[i].getAD_Org_ID(), true);      //  from Loc
					fLines[i].setLocationFromBPartner(getC_BPartner_Location_ID(), false);  //  to Loc
				}
			}
		}
		//  ** API
		else if (getDocumentType().equals(DOCTYPE_APInvoice))
		{
			MInvoice invoice = (MInvoice)getPO();
			MInvoice originalInvoice = null;
			if (invoice.getReversal_ID() > 0 && invoice.getReversal_ID() < invoice.getC_Invoice_ID())
			{
				originalInvoice = new MInvoice(Env.getCtx(), invoice.getReversal_ID(), invoice.get_TrxName());
			}
			BigDecimal grossAmt = getAmount(Doc.AMTTYPE_Gross);
			BigDecimal serviceAmt = Env.ZERO;
			//  Charge          DR
			fact.createLine(null, getAccount(Doc.ACCTTYPE_Charge, as),
				getC_Currency_ID(), getAmount(Doc.AMTTYPE_Charge), null);
			//  TaxCredit       DR
			for (int i = 0; i < m_taxes.length; i++)
			{
				FactLine tl = fact.createLine(null, m_taxes[i].getAccount(m_taxes[i].getAPTaxType(), as),
					getC_Currency_ID(), m_taxes[i].getAmount(), null);
				if (tl != null)
					tl.setC_Tax_ID(m_taxes[i].getC_Tax_ID());
				if (tl != null && invoice.getReversal_ID() > 0 && invoice.getReversal_ID() < invoice.getC_Invoice_ID())
				{
					tl.updateReverseLine(MInvoice.Table_ID, invoice.getReversal_ID(), 0, BigDecimal.ONE);
				}
			}
			//  Expense         DR
			for (int i = 0; i < p_lines.length; i++)
			{
				DocLine line = p_lines[i];
				boolean landedCost = landedCost(as, fact, line, true);
				if (landedCost && as.isExplicitCostAdjustment())
				{
					fact.createLine (line, line.getAccount(ProductCost.ACCTTYPE_P_Expense, as),
						getC_Currency_ID(), line.getAmtSource(), null);
					//
					FactLine fl = fact.createLine (line, line.getAccount(ProductCost.ACCTTYPE_P_Expense, as),
						getC_Currency_ID(), null, line.getAmtSource());
					String desc = line.getDescription();
					if (desc == null)
						desc = "100%";
					else
						desc += " 100%";
					fl.setDescription(desc);
					if (invoice.getReversal_ID() > 0 && invoice.getReversal_ID() < invoice.getC_Invoice_ID())
					{
						int lineId = 0;
						if (originalInvoice != null)
						{
							MInvoiceLine[] lines = originalInvoice.getLines();
							if (lines.length > i)
								lineId = lines[i].getC_InvoiceLine_ID();
						}
						fl.updateReverseLine(MInvoice.Table_ID, invoice.getReversal_ID(), lineId, BigDecimal.ONE);
					}
				}
				if (!landedCost)
				{
					MAccount expense = line.getAccount(ProductCost.ACCTTYPE_P_Expense, as);
					if (line.isItem())
						expense = line.getAccount (ProductCost.ACCTTYPE_P_InventoryClearing, as);
					BigDecimal amt = line.getAmtSource();
					BigDecimal dAmt = null;
					if (as.isTradeDiscountPosted() && !line.isItem())
					{
						BigDecimal discount = line.getDiscount();
						if (discount != null && discount.signum() != 0)
						{
							amt = amt.add(discount);
							dAmt = discount;
							MAccount tradeDiscountReceived = line.getAccount(ProductCost.ACCTTYPE_P_TDiscountRec, as);
							FactLine fl = fact.createLine (line, tradeDiscountReceived,
									getC_Currency_ID(), null, dAmt);
							if (fl != null && invoice.getReversal_ID() > 0 && invoice.getReversal_ID() < invoice.getC_Invoice_ID())
							{
								int lineId = 0;
								if (originalInvoice != null)
								{
									MInvoiceLine[] lines = originalInvoice.getLines();
									if (lines.length > i)
										lineId = lines[i].getC_InvoiceLine_ID();
								}
								fl.updateReverseLine(MInvoice.Table_ID, invoice.getReversal_ID(), lineId, BigDecimal.ONE);
							}
						}
					}
					FactLine fl = fact.createLine (line, expense,
						getC_Currency_ID(), amt, null);
					if (fl != null && invoice.getReversal_ID() > 0 && invoice.getReversal_ID() < invoice.getC_Invoice_ID())
					{
						int lineId = 0;
						if (originalInvoice != null)
						{
							MInvoiceLine[] lines = originalInvoice.getLines();
							if (lines.length > i)
								lineId = lines[i].getC_InvoiceLine_ID();
						}
						fl.updateReverseLine(MInvoice.Table_ID, invoice.getReversal_ID(), lineId, BigDecimal.ONE);
					}
					if (!line.isItem())
					{
						grossAmt = grossAmt.subtract(amt);
						serviceAmt = serviceAmt.add(amt);
					}
					//
					if (line.getM_Product_ID() != 0
						&& line.getProduct().isService())	//	otherwise Inv Matching
						MCostDetail.createInvoice(as, line.getAD_Org_ID(),
							line.getM_Product_ID(), line.getM_AttributeSetInstance_ID(),
							line.get_ID(), 0,		//	No Cost Element
							line.getAmtSource(), line.getQty(),
							line.getDescription(), getTrxName());
				}
			}
			//  Liability               CR
			int payables_ID = getValidCombination_ID (Doc.ACCTTYPE_V_Liability, as);
			int payablesServices_ID = payables_ID; // Liability Services account Deprecated IDEMPIERE-362
			if (m_allLinesItem || !as.isPostServices()
				|| payables_ID == payablesServices_ID)
			{
				grossAmt = getAmount(Doc.AMTTYPE_Gross);
				serviceAmt = Env.ZERO;
			}
			else if (m_allLinesService)
			{
				serviceAmt = getAmount(Doc.AMTTYPE_Gross);
				grossAmt = Env.ZERO;
			}
			FactLine fl = null;
			if (grossAmt.signum() > 0)
				fl = fact.createLine(null, MAccount.get(getCtx(), payables_ID),
					getC_Currency_ID(), null, grossAmt);
			else if (grossAmt.signum() < 0)
				fl = fact.createLine(null, MAccount.get(getCtx(), payables_ID),
						getC_Currency_ID(), grossAmt.negate(), null);
			if (serviceAmt.signum() > 0)
				fl = fact.createLine(null, MAccount.get(getCtx(), payablesServices_ID),
					getC_Currency_ID(), null, serviceAmt);
			else if (serviceAmt.signum() < 0)
				fl = fact.createLine(null, MAccount.get(getCtx(), payablesServices_ID),
						getC_Currency_ID(), serviceAmt.negate(), null);
			if (fl != null && invoice.getReversal_ID() > 0 && invoice.getReversal_ID() < invoice.getC_Invoice_ID())
			{
				fl.updateReverseLine(MInvoice.Table_ID, invoice.getReversal_ID(), 0, BigDecimal.ONE);
			}
			//  Set Locations
			FactLine[] fLines = fact.getLines();
			for (int i = 0; i < fLines.length; i++)
			{
				if (fLines[i] != null)
				{
					fLines[i].setLocationFromBPartner(getC_BPartner_Location_ID(), true);  //  from Loc
					fLines[i].setLocationFromOrg(fLines[i].getAD_Org_ID(), false);    //  to Loc
				}
			}
			//
			updateProductPO(as);	//	Only API
		}
		//  APC
		else if (getDocumentType().equals(DOCTYPE_APCredit))
		{
			BigDecimal grossAmt = getAmount(Doc.AMTTYPE_Gross);
			BigDecimal serviceAmt = Env.ZERO;
			//  Charge                  CR
			fact.createLine (null, getAccount(Doc.ACCTTYPE_Charge, as),
				getC_Currency_ID(), null, getAmount(Doc.AMTTYPE_Charge));
			//  TaxCredit               CR
			for (int i = 0; i < m_taxes.length; i++)
			{
				FactLine tl = fact.createLine (null, m_taxes[i].getAccount(m_taxes[i].getAPTaxType(), as),
					getC_Currency_ID(), null, m_taxes[i].getAmount());
				if (tl != null)
					tl.setC_Tax_ID(m_taxes[i].getC_Tax_ID());
			}
			//  Expense                 CR
			for (int i = 0; i < p_lines.length; i++)
			{
				DocLine line = p_lines[i];
				boolean landedCost = landedCost(as, fact, line, false);
				if (landedCost && as.isExplicitCostAdjustment())
				{
					fact.createLine (line, line.getAccount(ProductCost.ACCTTYPE_P_Expense, as),
						getC_Currency_ID(), null, line.getAmtSource());
					//
					FactLine fl = fact.createLine (line, line.getAccount(ProductCost.ACCTTYPE_P_Expense, as),
						getC_Currency_ID(), line.getAmtSource(), null);
					String desc = line.getDescription();
					if (desc == null)
						desc = "100%";
					else
						desc += " 100%";
					fl.setDescription(desc);
				}
				if (!landedCost)
				{
					MAccount expense = line.getAccount(ProductCost.ACCTTYPE_P_Expense, as);
					if (line.isItem())
						expense = line.getAccount (ProductCost.ACCTTYPE_P_InventoryClearing, as);
					BigDecimal amt = line.getAmtSource();
					BigDecimal dAmt = null;
					if (as.isTradeDiscountPosted() && !line.isItem())
					{
						BigDecimal discount = line.getDiscount();
						if (discount != null && discount.signum() != 0)
						{
							amt = amt.add(discount);
							dAmt = discount;
							MAccount tradeDiscountReceived = line.getAccount(ProductCost.ACCTTYPE_P_TDiscountRec, as);
							fact.createLine (line, tradeDiscountReceived,
									getC_Currency_ID(), dAmt, null);
						}
					}
					fact.createLine (line, expense,
						getC_Currency_ID(), null, amt);
					if (!line.isItem())
					{
						grossAmt = grossAmt.subtract(amt);
						serviceAmt = serviceAmt.add(amt);
					}
					//
					if (line.getM_Product_ID() != 0
						&& line.getProduct().isService())	//	otherwise Inv Matching
						MCostDetail.createInvoice(as, line.getAD_Org_ID(),
							line.getM_Product_ID(), line.getM_AttributeSetInstance_ID(),
							line.get_ID(), 0,		//	No Cost Element
							line.getAmtSource().negate(), line.getQty(),
							line.getDescription(), getTrxName());
				}
			}
			//  Liability       DR
			int payables_ID = getValidCombination_ID (Doc.ACCTTYPE_V_Liability, as);
			int payablesServices_ID = payables_ID; // Liability Services account Deprecated IDEMPIERE-362
			if (m_allLinesItem || !as.isPostServices()
				|| payables_ID == payablesServices_ID)
			{
				grossAmt = getAmount(Doc.AMTTYPE_Gross);
				serviceAmt = Env.ZERO;
			}
			else if (m_allLinesService)
			{
				serviceAmt = getAmount(Doc.AMTTYPE_Gross);
				grossAmt = Env.ZERO;
			}
			if (grossAmt.signum() != 0)
				fact.createLine(null, MAccount.get(getCtx(), payables_ID),
					getC_Currency_ID(), grossAmt, null);
			if (serviceAmt.signum() != 0)
				fact.createLine(null, MAccount.get(getCtx(), payablesServices_ID),
					getC_Currency_ID(), serviceAmt, null);
			//  Set Locations
			FactLine[] fLines = fact.getLines();
			for (int i = 0; i < fLines.length; i++)
			{
				if (fLines[i] != null)
				{
					fLines[i].setLocationFromBPartner(getC_BPartner_Location_ID(), true);  //  from Loc
					fLines[i].setLocationFromOrg(fLines[i].getAD_Org_ID(), false);    //  to Loc
				}
			}
		}
		else
		{
			p_Error = "DocumentType unknown: " + getDocumentType();
			log.log(Level.SEVERE, p_Error);
			fact = null;
		}
		//
		facts.add(fact);
		return facts;
	}   //  createFact
	/**
	 * 	Create Fact for Cash Based accounting (i.e. only revenue/expense)
	 *	@param as accounting schema
	 *	@param fact fact to add lines to
	 *	@param multiplier source amount multiplier
	 *	@return accounted amount
	 */
	public BigDecimal createFactCash (MAcctSchema as, Fact fact, BigDecimal multiplier)
	{
		boolean creditMemo = getDocumentType().equals(DOCTYPE_ARCredit)
			|| getDocumentType().equals(DOCTYPE_APCredit);
		boolean payables = getDocumentType().equals(DOCTYPE_APInvoice)
			|| getDocumentType().equals(DOCTYPE_APCredit);
		BigDecimal acctAmt = Env.ZERO;
		FactLine fl = null;
		//	Revenue/Cost
		for (int i = 0; i < p_lines.length; i++)
		{
			DocLine line = p_lines[i];
			boolean landedCost = false;
			if  (payables)
				landedCost = landedCost(as, fact, line, false);
			if (landedCost && as.isExplicitCostAdjustment())
			{
				fact.createLine (line, line.getAccount(ProductCost.ACCTTYPE_P_Expense, as),
					getC_Currency_ID(), null, line.getAmtSource());
				//
				fl = fact.createLine (line, line.getAccount(ProductCost.ACCTTYPE_P_Expense, as),
					getC_Currency_ID(), line.getAmtSource(), null);
				String desc = line.getDescription();
				if (desc == null)
					desc = "100%";
				else
					desc += " 100%";
				fl.setDescription(desc);
			}
			if (!landedCost)
			{
				MAccount acct = line.getAccount(
					payables ? ProductCost.ACCTTYPE_P_Expense : ProductCost.ACCTTYPE_P_Revenue, as);
				if (payables)
				{
					//	if Fixed Asset
					if (line.isItem())
						acct = line.getAccount (ProductCost.ACCTTYPE_P_InventoryClearing, as);
				}
				BigDecimal amt = line.getAmtSource().multiply(multiplier);
				BigDecimal amt2 = null;
				if (creditMemo)
				{
					amt2 = amt;
					amt = null;
				}
				if (payables)	//	Vendor = DR
					fl = fact.createLine (line, acct,
						getC_Currency_ID(), amt, amt2);
				else			//	Customer = CR
					fl = fact.createLine (line, acct,
						getC_Currency_ID(), amt2, amt);
				if (fl != null)
					acctAmt = acctAmt.add(fl.getAcctBalance());
			}
		}
		//  Tax
		for (int i = 0; i < m_taxes.length; i++)
		{
			BigDecimal amt = m_taxes[i].getAmount();
			BigDecimal amt2 = null;
			if (creditMemo)
			{
				amt2 = amt;
				amt = null;
			}
			FactLine tl = null;
			if (payables)
				tl = fact.createLine (null, m_taxes[i].getAccount(m_taxes[i].getAPTaxType(), as),
					getC_Currency_ID(), amt, amt2);
			else
				tl = fact.createLine (null, m_taxes[i].getAccount(DocTax.ACCTTYPE_TaxDue, as),
					getC_Currency_ID(), amt2, amt);
			if (tl != null)
				tl.setC_Tax_ID(m_taxes[i].getC_Tax_ID());
		}
		//  Set Locations
		FactLine[] fLines = fact.getLines();
		for (int i = 0; i < fLines.length; i++)
		{
			if (fLines[i] != null)
			{
				if (payables)
				{
					fLines[i].setLocationFromBPartner(getC_BPartner_Location_ID(), true);  //  from Loc
					fLines[i].setLocationFromOrg(fLines[i].getAD_Org_ID(), false);    //  to Loc
				}
				else
				{
					fLines[i].setLocationFromOrg(fLines[i].getAD_Org_ID(), true);    //  from Loc
					fLines[i].setLocationFromBPartner(getC_BPartner_Location_ID(), false);  //  to Loc
				}
			}
		}
		return acctAmt;
	}	//	createFactCash
	/**
	 * 	Create Landed Cost accounting and Cost lines
	 *	@param as accounting schema
	 *	@param fact fact
	 *	@param line document line
	 *	@param dr true for DR side, false otherwise
	 *	@return true if landed costs were created
	 */
	protected boolean landedCost (MAcctSchema as, Fact fact, DocLine line, boolean dr)
	{
		int C_InvoiceLine_ID = line.get_ID();
		MLandedCostAllocation[] lcas = MLandedCostAllocation.getOfInvoiceLine(
			getCtx(), C_InvoiceLine_ID, getTrxName());
		if (lcas.length == 0)
			return false;
		//	Calculate Total Base
		double totalBase = 0;
		for (int i = 0; i < lcas.length; i++)
			totalBase += lcas[i].getBase().doubleValue();
		Map costDetailAmtMap = new HashMap<>();
		Map mcostQtyMap = new HashMap<>();
		
		//	Create New
		MInvoiceLine il = new MInvoiceLine (getCtx(), C_InvoiceLine_ID, getTrxName());
		for (int i = 0; i < lcas.length; i++)
		{
			MLandedCostAllocation lca = lcas[i];
			if (lca.getBase().signum() == 0)
				continue;
			double percent = lca.getBase().doubleValue() / totalBase;
			String desc = il.getDescription();
			if (desc == null)
				desc = percent + "%";
			else
				desc += " - " + percent + "%";
			if (line.getDescription() != null)
				desc += " - " + line.getDescription();
			// Accounting			
			BigDecimal drAmt = null;
			BigDecimal crAmt = null;
			MAccount account = null;
			ProductCost pc = new ProductCost (Env.getCtx(),
					lca.getM_Product_ID(), lca.getM_AttributeSetInstance_ID(), getTrxName());
			String costingMethod = pc.getProduct().getCostingMethod(as);
			if (X_M_Cost.COSTINGMETHOD_AverageInvoice.equals(costingMethod) || X_M_Cost.COSTINGMETHOD_AveragePO.equals(costingMethod))
			{
			
				BigDecimal allocationAmt =  lca.getAmt();
				boolean reversal = false;
				if (allocationAmt.signum() < 0) //reversal
				{
					allocationAmt = allocationAmt.negate();
					reversal = true;
				}
				
				BigDecimal estimatedAmt = BigDecimal.ZERO;
				BigDecimal costAdjustmentAmt = BigDecimal.ZERO;
				boolean usesSchemaCurrency = false;
				MInvoiceLine reversalLine = null;
				if (reversal)
				{
					MInvoice invoice = (MInvoice)getPO();
					MInvoice reversalInvoice = new MInvoice(getCtx(), invoice.getReversal_ID(), getTrxName());
					MInvoiceLine[] lines = invoice.getLines();
					MInvoiceLine[] reversalLines = reversalInvoice.getLines();
					for(int j = 0; j < lines.length; j++) {
						if (lines[j].get_ID() == il.get_ID()) {
							reversalLine = reversalLines[j];
							break;
						}
					}
				}
				else
				{
					int oCurrencyId = 0;					
					Timestamp oDateAcct = getDateAcct();
					if (lca.getM_InOutLine_ID() > 0)
					{
						I_M_InOutLine iol = lca.getM_InOutLine();
						if (iol.getC_OrderLine_ID() > 0)
						{
							oCurrencyId =  iol.getC_OrderLine().getC_Currency_ID();
							oDateAcct = iol.getC_OrderLine().getC_Order().getDateAcct();
							MOrderLandedCostAllocation[] allocations = MOrderLandedCostAllocation.getOfOrderLine(iol.getC_OrderLine_ID(), getTrxName());
							for(MOrderLandedCostAllocation allocation : allocations)
							{
								if (allocation.getC_OrderLandedCost().getM_CostElement_ID() != lca.getM_CostElement_ID())
									continue;
								
								BigDecimal amt = allocation.getAmt();
								BigDecimal qty = allocation.getQty();
								if (qty.compareTo(iol.getMovementQty()) != 0)
								{
									amt = amt.multiply(iol.getMovementQty()).divide(qty, 12, RoundingMode.HALF_UP);
								}
								estimatedAmt = estimatedAmt.add(amt); 
							}
						}
					}
					
					if (estimatedAmt.scale() > as.getCostingPrecision())
					{
						estimatedAmt = estimatedAmt.setScale(as.getCostingPrecision(), RoundingMode.HALF_UP);
					}
					costAdjustmentAmt = allocationAmt;
					if (estimatedAmt.signum() > 0)
					{					
						//get other allocation amt
						StringBuilder sql = new StringBuilder("SELECT Sum(Amt) FROM C_LandedCostAllocation WHERE M_InOutLine_ID=? ")
							.append("AND C_LandedCostAllocation_ID<>? ")
							.append("AND M_CostElement_ID=? ")
							.append("AND AD_Client_ID=? ");
						BigDecimal otherAmt = DB.getSQLValueBD(getTrxName(), sql.toString(), lca.getM_InOutLine_ID(), lca.getC_LandedCostAllocation_ID(), 
								lca.getM_CostElement_ID(), lca.getAD_Client_ID());
						if (otherAmt != null) 
						{
							estimatedAmt = estimatedAmt.subtract(otherAmt);
						}	
						//added for IDEMPIERE-3014
						//convert to accounting schema currency
						if (estimatedAmt.signum() > 0 && oCurrencyId != getC_Currency_ID())
						{
							estimatedAmt = MConversionRate.convert(getCtx(), estimatedAmt,
									oCurrencyId, as.getC_Currency_ID(),
									oDateAcct, getC_ConversionType_ID(),
									getAD_Client_ID(), getAD_Org_ID());
	
							allocationAmt = MConversionRate.convert(getCtx(), allocationAmt,
									getC_Currency_ID(), as.getC_Currency_ID(),
									getDateAcct(), getC_ConversionType_ID(),
									getAD_Client_ID(), getAD_Org_ID());
							setC_Currency_ID(as.getC_Currency_ID());
							usesSchemaCurrency = true;
						}
	
						if (estimatedAmt.signum() > 0)
						{						
							costAdjustmentAmt = allocationAmt.subtract(estimatedAmt);
						}
					}
					
					if (!dr)
						costAdjustmentAmt = costAdjustmentAmt.negate();
				}
	
				BigDecimal amtAsset = Env.ZERO;
				BigDecimal amtVariance = Env.ZERO;
				BigDecimal costDetailQty = lca.getQty();
				if (costAdjustmentAmt.signum() != 0 && !reversal)
				{
					Trx trx = Trx.get(getTrxName(), false);
					Savepoint savepoint = null;					
					try {
						savepoint = trx.setSavepoint(null);
						
						amtVariance = Env.ZERO;
						amtAsset = costAdjustmentAmt;
						
						if(X_M_Cost.COSTINGMETHOD_AveragePO.equals(costingMethod))
						{
							int AD_Org_ID = lca.getAD_Org_ID();
							int M_AttributeSetInstance_ID = lca.getM_AttributeSetInstance_ID();
							if (MAcctSchema.COSTINGLEVEL_Client.equals(as.getCostingLevel()))
							{
								AD_Org_ID = 0;
								M_AttributeSetInstance_ID = 0;
							}
							else if (MAcctSchema.COSTINGLEVEL_Organization.equals(as.getCostingLevel()))
								M_AttributeSetInstance_ID = 0;
							else if (MAcctSchema.COSTINGLEVEL_BatchLot.equals(as.getCostingLevel()))
								AD_Org_ID = 0;
							
							MCostElement ce = MCostElement.getMaterialCostElement(getCtx(), as.getCostingMethod(),
									AD_Org_ID);
							MCost c = MCost.get(getCtx(), getAD_Client_ID(), AD_Org_ID, lca.getM_Product_ID(),
									as.getM_CostType_ID(), as.getC_AcctSchema_ID(), ce.getM_CostElement_ID(),
									M_AttributeSetInstance_ID, getTrxName());
							if (c != null)
							{
								BigDecimal mcostQty = c.getCurrentQty();
								if (mcostQtyMap.containsKey(c.get_UUID())) {
									mcostQty = mcostQty.subtract(mcostQtyMap.get(c.get_UUID()));
									if (mcostQty.signum() < 0)
										mcostQty = new BigDecimal("0.00");
								}
								if (mcostQty.compareTo(lca.getQty()) < 0) {
									amtAsset = mcostQty.multiply(costAdjustmentAmt.divide(lca.getQty(), as.getCostingPrecision(), RoundingMode.HALF_UP));
									amtVariance = costAdjustmentAmt.subtract(amtAsset);
									costDetailQty = mcostQty;									
								}
								if (mcostQtyMap.containsKey(c.get_UUID())) {
									mcostQtyMap.put(c.get_UUID(), mcostQtyMap.get(c.get_UUID()).add(costDetailQty));
								} else {
									mcostQtyMap.put(c.get_UUID(), costDetailQty);
								}
							}
						}
						
						BigDecimal costDetailAmt = amtAsset;
						//convert to accounting schema currency
						if (getC_Currency_ID() != as.getC_Currency_ID())
							costDetailAmt = MConversionRate.convert(getCtx(), costDetailAmt,
								getC_Currency_ID(), as.getC_Currency_ID(),
								getDateAcct(), getC_ConversionType_ID(),
								getAD_Client_ID(), getAD_Org_ID());
						if (costDetailAmt.scale() > as.getCostingPrecision())
							costDetailAmt = costDetailAmt.setScale(as.getCostingPrecision(), RoundingMode.HALF_UP);
						
						String key = lca.getM_Product_ID()+"_"+lca.getM_AttributeSetInstance_ID();
						BigDecimal prevAmt = costDetailAmtMap.remove(key);
						if (prevAmt != null) {
							costDetailAmt = costDetailAmt.add(prevAmt);
						}
						costDetailAmtMap.put(key, costDetailAmt);
						if (costDetailAmt.signum() != 0 && 
							!MCostDetail.createInvoice(as, lca.getAD_Org_ID(),
								lca.getM_Product_ID(), lca.getM_AttributeSetInstance_ID(),
								C_InvoiceLine_ID, lca.getM_CostElement_ID(),
								costDetailAmt, costDetailQty,
								desc, getTrxName())) {
							throw new RuntimeException("Failed to create cost detail record.");
						}				
					} catch (SQLException e) {
						throw new RuntimeException(e.getLocalizedMessage(), e);
					} catch (AverageCostingZeroQtyException e) {
						try { 
							amtAsset = BigDecimal.ZERO;
							amtVariance = costAdjustmentAmt;
							trx.rollback(savepoint);
							savepoint = null;
						} catch (SQLException e1) {
							throw new RuntimeException(e1.getLocalizedMessage(), e1);
						}
					} finally {
						if (savepoint != null) {
							try {
								trx.releaseSavepoint(savepoint);
							} catch (SQLException e) {}
						}
					}
				} else if (reversal) {
					costDetailQty = BigDecimal.ZERO;
					int AD_Org_ID = lca.getAD_Org_ID();
					int M_AttributeSetInstance_ID = lca.getM_AttributeSetInstance_ID();
					if (MAcctSchema.COSTINGLEVEL_Client.equals(as.getCostingLevel()))
					{
						AD_Org_ID = 0;
						M_AttributeSetInstance_ID = 0;
					}
					else if (MAcctSchema.COSTINGLEVEL_Organization.equals(as.getCostingLevel()))
						M_AttributeSetInstance_ID = 0;
					else if (MAcctSchema.COSTINGLEVEL_BatchLot.equals(as.getCostingLevel()))
						AD_Org_ID = 0;
					String key = lca.getM_Product_ID()+"_"+M_AttributeSetInstance_ID;
					if (!costDetailAmtMap.containsKey(key)) {
						costDetailAmtMap.put(key, BigDecimal.ZERO);
						amtAsset = BigDecimal.ZERO;
						amtVariance = BigDecimal.ZERO;
						MAccount varianceAccount = pc.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as);
						MAccount assetAccount = pc.getAccount(ProductCost.ACCTTYPE_P_Asset, as);
						Query query = MFactAcct.createRecordIdQuery(MInvoice.Table_ID, reversalLine.getC_Invoice_ID(), as.getC_AcctSchema_ID(), getTrxName());
						List factAccts = query.list();
						for(MFactAcct factAcct : factAccts) {
							if (factAcct.getM_Product_ID() != lca.getM_Product_ID())
								continue;
							if (factAcct.getLine_ID() != reversalLine.get_ID())
								continue;
							if (factAcct.getAccount_ID() == assetAccount.getAccount_ID()) {
								if (factAcct.getAmtAcctDr().signum() != 0)
									amtAsset = amtAsset.add(factAcct.getAmtAcctDr());
								else if (factAcct.getAmtAcctCr().signum() != 0)
									amtAsset = amtAsset.subtract(factAcct.getAmtAcctCr());
							} else if (factAcct.getAccount_ID() == varianceAccount.getAccount_ID()) {
								if (factAcct.getAmtAcctDr().signum() != 0)
									amtVariance = amtVariance.add(factAcct.getAmtAcctDr());
								else if (factAcct.getAmtAcctCr().signum() != 0)
									amtVariance = amtVariance.subtract(factAcct.getAmtAcctCr());
							}
						}
						if (lca.getM_AttributeSetInstance_ID() > 0 && M_AttributeSetInstance_ID == 0) {
							String sql = 
									"""
										SELECT SUM(Qty)
										FROM M_CostDetail
										WHERE C_InvoiceLine_ID=? AND Coalesce(M_CostElement_ID,0)=?
										AND M_Product_ID=? AND C_AcctSchema_ID=? 										
									""";
							costDetailQty = DB.getSQLValueBDEx(getTrxName(), sql, reversalLine.get_ID(), lca.getM_CostElement_ID(), lca.getM_Product_ID(), as.getC_AcctSchema_ID());
							if (costDetailQty == null)
								costDetailQty = BigDecimal.ZERO;
						} else if (lca.getM_AttributeSetInstance_ID() > 0 && M_AttributeSetInstance_ID > 0) {
							MCostDetail cd = MCostDetail.get (as.getCtx(), "C_InvoiceLine_ID=? AND Coalesce(M_CostElement_ID,0)="+lca.getM_CostElement_ID()+" AND M_Product_ID="+lca.getM_Product_ID(), 
									reversalLine.get_ID(), lca.getM_AttributeSetInstance_ID(), as.getC_AcctSchema_ID(), getTrxName());
							costDetailQty = cd != null ? cd.getQty() : BigDecimal.ZERO;
							if (cd != null) {
								amtAsset = cd.getAmt();
							}
							if (i > 0) {
								for(int j = 0; j < i; j++) {
									if (lcas[j].getM_Product_ID() == lca.getM_Product_ID()) {
										//variance have been posted by product
										amtVariance = BigDecimal.ZERO;
									}
								}
							}
						} else {
							MCostDetail cd = MCostDetail.get (as.getCtx(), "C_InvoiceLine_ID=? AND Coalesce(M_CostElement_ID,0)="+lca.getM_CostElement_ID()+" AND M_Product_ID="+lca.getM_Product_ID(), 
									reversalLine.get_ID(), lca.getM_AttributeSetInstance_ID(), as.getC_AcctSchema_ID(), getTrxName());
							costDetailQty = cd != null ? cd.getQty() : BigDecimal.ZERO;
						}
						if (costDetailQty.signum() != 0)
						{
							MCostElement ce = MCostElement.getMaterialCostElement(getCtx(), as.getCostingMethod(),
									AD_Org_ID);
							MCost c = MCost.get(getCtx(), getAD_Client_ID(), AD_Org_ID, lca.getM_Product_ID(),
									as.getM_CostType_ID(), as.getC_AcctSchema_ID(), ce.getM_CostElement_ID(),
									M_AttributeSetInstance_ID, getTrxName());
							if (c != null) {
								if (c.getCurrentQty().signum() == 0) {
									amtVariance = amtVariance.add(amtAsset);
									amtAsset = BigDecimal.ZERO;
								} else if (c.getCurrentQty().compareTo(costDetailQty) < 0) {
									BigDecimal currentAmtAsset = amtAsset;
									amtAsset = amtAsset.divide(costDetailQty, RoundingMode.HALF_UP).multiply(c.getCurrentQty());
									amtVariance = amtVariance.add(currentAmtAsset.subtract(amtAsset));
									costDetailQty = c.getCurrentQty();
								}
							}
						}
						if (amtAsset.signum() != 0) {
							if (!MCostDetail.createInvoice(as, lca.getAD_Org_ID(),
									lca.getM_Product_ID(), lca.getM_AttributeSetInstance_ID(),
									C_InvoiceLine_ID, lca.getM_CostElement_ID(),
									amtAsset.negate(), costDetailQty,
									desc, getTrxName())) {
								throw new RuntimeException("Failed to create cost detail record.");
							}
						}
						if (getC_Currency_ID() != as.getC_Currency_ID()) {
							usesSchemaCurrency = true;
							setC_Currency_ID(as.getC_Currency_ID());
						}
					}
				}
								
				if (allocationAmt.signum() > 0 && !reversal)
				{
					if (allocationAmt.scale() > as.getStdPrecision())
					{
						allocationAmt = allocationAmt.setScale(as.getStdPrecision(), RoundingMode.HALF_UP);
					}
					if (estimatedAmt.scale() > as.getStdPrecision())
					{
						estimatedAmt = estimatedAmt.setScale(as.getStdPrecision(), RoundingMode.HALF_UP);
					}
					if (allocationAmt.compareTo(estimatedAmt)!=0)
					{
						if (estimatedAmt.signum() != 0)
						{
							drAmt = dr ? (reversal ? null : estimatedAmt): (reversal ? estimatedAmt : null);
							crAmt = dr ? (reversal ? estimatedAmt : null): (reversal ? null : estimatedAmt);						
							account = pc.getAccount(ProductCost.ACCTTYPE_P_LandedCostClearing, as);
							FactLine fl = fact.createLine (line, account, getC_Currency_ID(), drAmt, crAmt);
							fl.setDescription(desc);
							fl.setM_Product_ID(lca.getM_Product_ID());
							fl.setQty(line.getQty());
						}
						
						if (amtVariance.signum() != 0) {
							if (amtVariance.signum() > 0) {
								drAmt = dr ? amtVariance : null;
								crAmt = dr ? null : amtVariance;
							} else {
								BigDecimal underAmt = amtVariance.negate();
								drAmt = dr ? null : underAmt;
								crAmt = dr ? underAmt : null;
							}
							account = pc.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as);
							FactLine fl = fact.createLine(line, account, getC_Currency_ID(), drAmt, crAmt);
							fl.setDescription(desc);
							fl.setM_Product_ID(lca.getM_Product_ID());
							fl.setQty(line.getQty());
						}
						if (amtAsset.signum() != 0) {
							if (amtAsset.signum() > 0) {
								drAmt = dr ? amtAsset : null;
								crAmt = dr ? null : amtAsset;
							} else {
								BigDecimal underAmt = amtAsset.negate();
								drAmt = dr ? null : underAmt;
								crAmt = dr ? underAmt : null;
							}
							account = pc.getAccount(ProductCost.ACCTTYPE_P_Asset, as);
							FactLine  fl = fact.createLine(line, account, getC_Currency_ID(), drAmt, crAmt);
							fl.setDescription(desc);
							fl.setM_Product_ID(lca.getM_Product_ID());
							fl.setQty(line.getQty());
						}
					}
					else if (allocationAmt.signum() != 0)
					{
						drAmt = dr ? (reversal ? null : allocationAmt) : (reversal ? allocationAmt : null);
						crAmt = dr ? (reversal ? allocationAmt : null) : (reversal ? null : allocationAmt);
						account = pc.getAccount(ProductCost.ACCTTYPE_P_LandedCostClearing, as);
						FactLine fl = fact.createLine (line, account, getC_Currency_ID(), drAmt, crAmt);
						fl.setDescription(desc);
						fl.setM_Product_ID(lca.getM_Product_ID());
						fl.setQty(line.getQty());
					}
				} else if (reversal) {
					account = pc.getAccount(ProductCost.ACCTTYPE_P_LandedCostClearing, as);
					FactLine fl = fact.createLine (line, account, getC_Currency_ID(), BigDecimal.ZERO, BigDecimal.ZERO);
					fl.updateReverseLine(MInvoice.Table_ID, reversalLine.getC_Invoice_ID(), reversalLine.get_ID(), BigDecimal.ONE);
					if (fl.getAmtAcctCr().signum() == 0 && fl.getAmtAcctDr().signum() == 0)
						fact.remove(fl);
					
					if (amtVariance.signum() != 0) {
						if (amtVariance.signum() > 0) {
							drAmt = dr ? null : amtVariance;
							crAmt = dr ? amtVariance : null;
						} else {
							BigDecimal underAmt = amtVariance.negate();
							drAmt = dr ? underAmt : null;
							crAmt = dr ? null : underAmt;
						}
						account = pc.getAccount(ProductCost.ACCTTYPE_P_AverageCostVariance, as);
						fl = fact.createLine(line, account, getC_Currency_ID(), drAmt, crAmt);
						fl.setDescription(desc);
						fl.setM_Product_ID(lca.getM_Product_ID());
						fl.setQty(line.getQty());
					}
					if (amtAsset.signum() != 0) {
						if (amtAsset.signum() > 0) {
							drAmt = dr ? null : amtAsset;
							crAmt = dr ? amtAsset : null;
						} else {
							BigDecimal underAmt = amtAsset.negate();
							drAmt = dr ? underAmt : null;
							crAmt = dr ? null : underAmt;
						}
						account = pc.getAccount(ProductCost.ACCTTYPE_P_Asset, as);
						fl = fact.createLine(line, account, getC_Currency_ID(), drAmt, crAmt);
						fl.setDescription(desc);
						fl.setM_Product_ID(lca.getM_Product_ID());
						fl.setQty(line.getQty());
					}
				}
				if (usesSchemaCurrency)
					setC_Currency_ID(line.getC_Currency_ID());
			} 
			else 
			{
				if (dr)
					drAmt = lca.getAmt();
				else
					crAmt = lca.getAmt();
				account = pc.getAccount(ProductCost.ACCTTYPE_P_CostAdjustment, as);
				FactLine fl = fact.createLine (line, account, getC_Currency_ID(), drAmt, crAmt);
				fl.setDescription(desc);
				fl.setM_Product_ID(lca.getM_Product_ID());
				fl.setQty(line.getQty());
			}
		}
		if (log.isLoggable(Level.CONFIG)) log.config("Created #" + lcas.length);
		return true;
	}	//	landedCosts
	/**
	 * 	Update ProductPO PriceLastInv
	 *	@param as accounting schema
	 */
	protected void updateProductPO (MAcctSchema as)
	{
		MClientInfo ci = MClientInfo.get(getCtx(), as.getAD_Client_ID());
		if (ci.getC_AcctSchema1_ID() != as.getC_AcctSchema_ID())
			return;
		StringBuilder sql = new StringBuilder (
			"UPDATE M_Product_PO po ")
			 .append("SET PriceLastInv = ")
			//	select
			.append("(SELECT currencyConvertInvoice(i.C_Invoice_ID,po.C_Currency_ID,il.PriceActual,i.DateInvoiced) ")
			.append("FROM C_Invoice i, C_InvoiceLine il ")
			.append("WHERE i.C_Invoice_ID=il.C_Invoice_ID")
			.append(" AND po.M_Product_ID=il.M_Product_ID AND po.C_BPartner_ID=i.C_BPartner_ID");
			if (DB.isOracle()) //jz
			{
				sql.append(" AND ROWNUM=1 ");
			}
			else
			{
				sql.append(" AND il.C_InvoiceLine_ID = (SELECT MIN(il1.C_InvoiceLine_ID) ")
						.append("FROM C_Invoice i1, C_InvoiceLine il1 ")
						.append("WHERE i1.C_Invoice_ID=il1.C_Invoice_ID")
						.append(" AND po.M_Product_ID=il1.M_Product_ID AND po.C_BPartner_ID=i1.C_BPartner_ID")
						.append("  AND i1.C_Invoice_ID=").append(get_ID()).append(") ");
			}
			sql.append("  AND i.C_Invoice_ID=").append(get_ID()).append(") ")
			//	update
			.append("WHERE EXISTS (SELECT * ")
			.append("FROM C_Invoice i, C_InvoiceLine il ")
			.append("WHERE i.C_Invoice_ID=il.C_Invoice_ID")
			.append(" AND po.M_Product_ID=il.M_Product_ID AND po.C_BPartner_ID=i.C_BPartner_ID")
			.append(" AND i.C_Invoice_ID=").append(get_ID()).append(")");
		int no = DB.executeUpdate(sql.toString(), getTrxName());
		if (log.isLoggable(Level.FINE)) log.fine("Updated=" + no);
	}	//	updateProductPO
	@Override
	public BigDecimal getCurrencyRate() {
		if (getC_Currency_ID() == getAcctSchema().getC_Currency_ID())
			return null;
		
		MInvoice inv = (MInvoice)getPO();
		int baseCurrencyId = MClientInfo.get(getCtx(), inv.getAD_Client_ID()).getC_Currency_ID();
		if (baseCurrencyId != getAcctSchema().getC_Currency_ID())
			return null;
		
		if (inv.isOverrideCurrencyRate()) {
			return inv.getCurrencyRate();
		} else {
			return null;
		}		
	}	
	
	@Override
	public boolean isConvertible (MAcctSchema acctSchema) {
		MInvoice inv = (MInvoice)getPO();
		if (inv.getC_Currency_ID() != acctSchema.getC_Currency_ID()) {
			int baseCurrencyId = MClientInfo.get(getCtx(), inv.getAD_Client_ID()).getC_Currency_ID();
			if (baseCurrencyId == acctSchema.getC_Currency_ID() && inv.isOverrideCurrencyRate()) {
				return true;
			}
		}
		
		return super.isConvertible(acctSchema);
	}
	
}   //  Doc_Invoice