From cb6c8b636de44452eabb2cdd1ce86b434aad098c Mon Sep 17 00:00:00 2001 From: Deepak Pansheriya Date: Fri, 29 Sep 2023 20:24:23 +0530 Subject: [PATCH] =?UTF-8?q?IDEMPIERE-5836:=20Adding=20unit=20test=20cases?= =?UTF-8?q?=20for=20Non=20stocked=20expense=20curren=E2=80=A6=20(#2024)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * IDEMPIERE-5836: Adding unit test cases for Non stocked expense current bhavior * IDEMPIERE-5836: Refactoring as per code review comments. * IDEMPIERE-5836: Corrected code comments as per peer review --- .../NonStockedExpTypeAvgPOCostingTest.java | 770 +++++++++++++++++ .../NonStockedExpTypeStdCostingTest.java | 774 ++++++++++++++++++ 2 files changed, 1544 insertions(+) create mode 100644 org.idempiere.test/src/org/idempiere/test/costing/NonStockedExpTypeAvgPOCostingTest.java create mode 100644 org.idempiere.test/src/org/idempiere/test/costing/NonStockedExpTypeStdCostingTest.java diff --git a/org.idempiere.test/src/org/idempiere/test/costing/NonStockedExpTypeAvgPOCostingTest.java b/org.idempiere.test/src/org/idempiere/test/costing/NonStockedExpTypeAvgPOCostingTest.java new file mode 100644 index 0000000000..97058707b8 --- /dev/null +++ b/org.idempiere.test/src/org/idempiere/test/costing/NonStockedExpTypeAvgPOCostingTest.java @@ -0,0 +1,770 @@ +/****************************************************************************** + * Copyright (C) 2016 Logilite Technologies LLP * + * 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. * + *****************************************************************************/ +package org.idempiere.test.costing; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.Timestamp; +import java.util.List; + +import org.compiere.acct.Doc; +import org.compiere.acct.DocManager; +import org.compiere.model.MAccount; +import org.compiere.model.MAcctSchema; +import org.compiere.model.MBPartner; +import org.compiere.model.MClient; +import org.compiere.model.MCost; +import org.compiere.model.MFactAcct; +import org.compiere.model.MInOut; +import org.compiere.model.MInOutConfirm; +import org.compiere.model.MInOutLine; +import org.compiere.model.MInvoice; +import org.compiere.model.MInvoiceLine; +import org.compiere.model.MMatchInv; +import org.compiere.model.MMatchPO; +import org.compiere.model.MOrder; +import org.compiere.model.MOrderLine; +import org.compiere.model.MPriceList; +import org.compiere.model.MPriceListVersion; +import org.compiere.model.MProcess; +import org.compiere.model.MProduct; +import org.compiere.model.MProductCategory; +import org.compiere.model.MProductCategoryAcct; +import org.compiere.model.MProductPrice; +import org.compiere.model.MProduction; +import org.compiere.model.MProductionLine; +import org.compiere.model.MRMA; +import org.compiere.model.MRMALine; +import org.compiere.model.MStorageOnHand; +import org.compiere.model.ProductCost; +import org.compiere.model.Query; +import org.compiere.model.SystemIDs; +import org.compiere.process.DocAction; +import org.compiere.process.DocumentEngine; +import org.compiere.process.ProcessInfo; +import org.compiere.process.ServerProcessCtl; +import org.compiere.util.Env; +import org.compiere.util.TimeUtil; +import org.compiere.util.Util; +import org.compiere.wf.MWorkflow; +import org.eevolution.model.MPPProductBOM; +import org.eevolution.model.MPPProductBOMLine; +import org.idempiere.test.AbstractTestCase; +import org.idempiere.test.DictionaryIDs; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Isolated; + +/** + * Test cases Stocked Expense type product with Average PO Costing Method. + * + * @author Logilite Technologies + * @since September 15, 2023 + */ +@Isolated +public class NonStockedExpTypeAvgPOCostingTest extends AbstractTestCase +{ + private static final BigDecimal BD_2 = new BigDecimal("2.00"); + private static final BigDecimal BD_5 = new BigDecimal("5.00"); + private static final BigDecimal BD_20 = new BigDecimal("20.00"); + + public NonStockedExpTypeAvgPOCostingTest() + { + } + + @Test + public void testMaterialReceipt() + { + //need to create category and product for the every test case because after the first test completed it's rollback + //and also need to delete category and product at the end of the each test case otherwise next test case failed + MProductCategory category = createProductCategory(); + MProduct product = createProduct(category.get_ID()); + + try + { + MAcctSchema as = getAccountingSchema(); + MCost cost = getCost(as, product); + + // Create Material Receipt + MInOutLine rLine = createPOAndMRForProduct(product); + + MMatchPO[] matchPOs = MMatchPO.get(Env.getCtx(), rLine.getM_InOutLine_ID(), getTrxName()); + assertEquals(1, matchPOs.length); + assertEquals(rLine.getC_OrderLine_ID(), matchPOs[0].getC_OrderLine_ID()); + assertEquals(rLine.getM_InOutLine_ID(), matchPOs[0].getM_InOutLine_ID()); + assertTrue(matchPOs[0].getQty().compareTo(BigDecimal.TEN) == 0); + + cost.load(getTrxName()); + + //Validate that averagePO costing method should not be created in the cost for the expense type product + assertNoAveragePOCost(product.get_ID()); + + + // Testing cost and stock of the product after completing MR + assertEquals(BD_2, cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + + assertEquals(BigDecimal.TEN.setScale(2, RoundingMode.HALF_UP), cost.getCurrentQty().setScale(2, RoundingMode.HALF_UP), "Unexpected current Qty"); + //Assert no storage records created for expense type product + MStorageOnHand[] storages = MStorageOnHand.getAll( Env.getCtx(), product.get_ID(), + DictionaryIDs.M_Locator.HQ.id, getTrxName(), false, 0); + assertEquals(0,storages.length,"No storage records should be created for expense type product"); + + // Testing Accounting For MR + Doc doc = DocManager.getDocument(as, MInOut.Table_ID, rLine.getM_InOut_ID(), getTrxName()); + doc.setC_BPartner_ID(rLine.getM_InOut().getC_BPartner_ID()); + MAccount acctNIR = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + + // get ProductExpense Of the created MR + ProductCost pc = new ProductCost(Env.getCtx(), rLine.getM_Product_ID(), 0, getTrxName()); + MAccount productExpense = pc.getAccount(ProductCost.ACCTTYPE_P_Expense, as); + + StringBuilder whereClause = new StringBuilder(); + whereClause + .append(MFactAcct.COLUMNNAME_AD_Table_ID) + .append("=") + .append(MInOut.Table_ID) + .append(" AND ") + .append(MFactAcct.COLUMNNAME_Record_ID) + .append("=") + .append(rLine.getM_InOut_ID()) + .append(" AND ") + .append(MFactAcct.COLUMNNAME_C_AcctSchema_ID) + .append("=") + .append(as.getC_AcctSchema_ID()); + + int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause.toString(), getTrxName()); + + for (int id : ids) + { + // Test Accounting for MR + MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); + if (fa.getAccount_ID() == acctNIR.getAccount_ID()) + { + assertEquals(BD_20, fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), ""); + assertEquals(new BigDecimal("-10.00"), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); + } + else if (fa.getAccount_ID() == productExpense.getAccount_ID()) + { + assertEquals(BD_20, fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), ""); + assertEquals(BigDecimal.TEN.setScale(2, RoundingMode.HALF_UP), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); + } + } + + } + finally + { + rollback(); + if (product != null) + product.deleteEx(true); + if (category != null) + category.deleteEx(true); + } + } + + @Test + public void testSOAndShipment() + { + MProductCategory category = createProductCategory(); + MProduct product = createProduct(category.get_ID()); + + try + { + MAcctSchema as = getAccountingSchema(); + MCost cost = getCost(as, product); + + createPOAndMRForProduct(product); + cost.load(getTrxName()); + assertNoAveragePOCost(product.get_ID()); + + createSOAndShipmentForProduct(product); + cost.load(getTrxName()); + + // testing cost and stock of the product after completing Shipment + assertEquals(BD_2, cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + assertEquals(BigDecimal.TEN.setScale(2, RoundingMode.HALF_UP), cost.getCurrentQty().setScale(2, RoundingMode.HALF_UP), "Unexpected current Qty"); + // Assert no storage records created for expense type product + MStorageOnHand[] storages = MStorageOnHand.getAll( Env.getCtx(), product.get_ID(), + DictionaryIDs.M_Locator.HQ.id, getTrxName(), false, 0); + assertEquals(0,storages.length,"No storage records should be created for expense type product"); + } + finally + { + rollback(); + if (product != null) + product.deleteEx(true); + if (category != null) + category.deleteEx(true); + } + + } + + @Test + public void testCustomerRMA() + { + MProductCategory category = createProductCategory(); + MProduct product = createProduct(category.get_ID()); + + try + { + MAcctSchema as = getAccountingSchema(); + MCost cost = getCost(as, product); + + createPOAndMRForProduct(product); + cost.load(getTrxName()); + assertNoAveragePOCost(product.get_ID()); + + + // Create SO And Shipment + MInOutLine sLine = createSOAndShipmentForProduct(product); + cost.load(getTrxName()); + + // Create MRA + MRMA rma = new MRMA(Env.getCtx(), 0, getTrxName()); + rma.setName("testCustomerReturnMaterial"); + rma.setC_DocType_ID(DictionaryIDs.C_DocType.CUSTOMER_RETURN_MATERIAL.id); + rma.setM_RMAType_ID(DictionaryIDs.M_RMAType.DAMAGE_ON_ARRIVAL.id); + rma.setM_InOut_ID(sLine.getM_InOut_ID()); + rma.setIsSOTrx(true); + rma.setSalesRep_ID(SystemIDs.USER_SUPERUSER); // SuperUser + rma.saveEx(); + + // Create MRALine + MRMALine rmaLine = new MRMALine(Env.getCtx(), 0, getTrxName()); + rmaLine.setLine(10); + rmaLine.setM_RMA_ID(rma.get_ID()); + rmaLine.setQty(new BigDecimal("3")); + rmaLine.setM_InOutLine_ID(sLine.get_ID()); + rmaLine.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(rma, DocAction.ACTION_Complete); + rma.load(getTrxName()); + + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, rma.getDocStatus()); + + // Create delivery for Customer Return Material + MInOut delivery = new MInOut(Env.getCtx(), 0, getTrxName()); + delivery.setM_RMA_ID(rma.get_ID()); + delivery.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id)); + delivery.setC_DocType_ID(DictionaryIDs.C_DocType.MM_CUSTOMER_RETURN.id); + delivery.setMovementType(MInOut.MOVEMENTTYPE_CustomerReturns); + delivery.setIsSOTrx(true); + delivery.setDocStatus(DocAction.STATUS_Drafted); + delivery.setDocAction(DocAction.ACTION_Complete); + delivery.setM_Warehouse_ID(DictionaryIDs.M_Warehouse.HQ.id); + delivery.saveEx(); + + MInOutLine dLine = new MInOutLine(delivery); + dLine.setM_RMALine_ID(rmaLine.get_ID()); + dLine.setLine(10); + dLine.setProduct(new MProduct(Env.getCtx(), product.getM_Product_ID(), getTrxName())); + dLine.setM_Locator_ID(sLine.getM_Locator_ID()); + dLine.setQty(new BigDecimal("3")); + dLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(delivery, DocAction.ACTION_Complete); + delivery.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + + // Complete the MInOutConfirm to complete the delivery + MInOutConfirm[] confirmations = delivery.getConfirmations(true); + assertEquals(1, confirmations.length, "Expected 1 Shipment Confirmation Document"); + + assertTrue(confirmations[0].getM_InOutConfirm_ID() != delivery.getM_InOut_ID(), "Test should run with M_InOut_ID != M_InOutConfirm_ID"); + + info = MWorkflow.runDocumentActionWorkflow(confirmations[0], DocAction.ACTION_Complete); + if (info.isError()) + { + fail("Failed to complete shipment confirmation" + (Util.isEmpty(info.getSummary()) ? "" : " : " + info.getSummary())); + return; + } + confirmations[0].load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, confirmations[0].getDocStatus(), "Expected Completed Status for Shipment Confirmation"); + + // Complete the delivery + info = MWorkflow.runDocumentActionWorkflow(delivery, DocAction.ACTION_Complete); + delivery.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, delivery.getDocStatus(), "Expected Completed Status for Shipment"); + + if (!delivery.isPosted()) + { + String error = DocumentEngine.postImmediate( Env.getCtx(), delivery.getAD_Client_ID(), MInOut.Table_ID, + delivery.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + // testing cost and stock of the product after Customer return + cost.load(getTrxName()); + + assertEquals(BD_2, cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + assertEquals(BigDecimal.TEN.setScale(2, RoundingMode.HALF_UP), cost.getCurrentQty().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost Qty"); + // Assert no storage records created for expense type product + MStorageOnHand[] storages = MStorageOnHand.getAll( Env.getCtx(), product.get_ID(), + DictionaryIDs.M_Locator.HQ.id, getTrxName(), false, 0); + assertEquals(0,storages.length,"No storage records should be created for expense type product"); + + } + finally + { + rollback(); + if (product != null) + product.deleteEx(true); + if (category != null) + category.deleteEx(true); + } + } + + @Test + public void testInvoice() + { + MProductCategory category = createProductCategory(); + MProduct product = createProduct(category.get_ID()); + + try + { + MAcctSchema as = getAccountingSchema(); + MCost cost = getCost(as, product); + + // Create PO And MR + MInOutLine rLine = createPOAndMRForProduct(product); + cost.load(getTrxName()); + assertNoAveragePOCost(product.get_ID()); + + // Create Invoice from the MR + MInOut mInOut = (MInOut) rLine.getM_InOut(); + MInvoice invoice = new MInvoice(mInOut, rLine.getM_InOut().getMovementDate()); + invoice.setDocStatus(DocAction.STATUS_Drafted); + invoice.setDocAction(DocAction.ACTION_Complete); + invoice.saveEx(); + + MOrderLine oLine = (MOrderLine) rLine.getC_OrderLine(); + MInvoiceLine iLine = new MInvoiceLine(invoice); + iLine.setOrderLine(oLine); + iLine.setLine(10); + iLine.setProduct(new MProduct(Env.getCtx(), product.getM_Product_ID(), getTrxName())); + iLine.setQty(BigDecimal.TEN); + iLine.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(invoice, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + invoice.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, invoice.getDocStatus()); + invoice.getDocsPostProcess().clear(); + if (!invoice.isPosted()) + { + String error = DocumentEngine.postImmediate( Env.getCtx(), invoice.getAD_Client_ID(), + invoice.get_Table_ID(), invoice.get_ID(), false, getTrxName()); + assertNull(error, error); + } + // Testing Accounting for Invoice + // get ProductExpense Of the Invoice + ProductCost pc = new ProductCost(Env.getCtx(), iLine.getM_Product_ID(), 0, getTrxName()); + MAccount productExpense = pc.getAccount(ProductCost.ACCTTYPE_P_Expense, as); + + // get AccPayable of the Invoice + MAccount acctPT = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getV_Liability_Acct(), getTrxName()); + + String whereClause = "AD_Table_ID = " + MInvoice.Table_ID; + whereClause += " AND Record_ID = " + iLine.getC_Invoice_ID(); + whereClause += " AND C_AcctSchema_ID = " + as.getC_AcctSchema_ID(); + + int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause.toString(), getTrxName()); + + for (int id : ids) + { + // Test Accounting for Invoice + MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); + if (fa.getAccount_ID() == acctPT.getAccount_ID()) + { + assertEquals(BD_20, fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), ""); + assertEquals(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); + } + else if (fa.getAccount_ID() == productExpense.getAccount_ID()) + { + assertEquals(BD_20, fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), ""); + assertEquals(BigDecimal.TEN.setScale(2, RoundingMode.HALF_UP), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); + } + } + + // Testing Accounting For MatchInv + MMatchInv[] matchInvoices = MMatchInv.getInOut(Env.getCtx(), rLine.getM_InOut_ID(), getTrxName()); + assertEquals(1, matchInvoices.length); + assertEquals(rLine.getM_InOutLine_ID(), matchInvoices[0].getM_InOutLine_ID()); + assertTrue( matchInvoices[0].getQty().compareTo(BigDecimal.TEN) == 0, + "Expected 5, get " + matchInvoices[0].getQty()); + + // get NotInvoicedReceipts for the MatchInv + Doc doc = DocManager.getDocument(as, MMatchInv.Table_ID, matchInvoices[0].get_ID(), getTrxName()); + doc.setC_BPartner_ID(matchInvoices[0].getC_InvoiceLine().getC_Invoice().getC_BPartner_ID()); + MAccount acctNIR = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + + // get Product Expense for the MatchInv + pc = new ProductCost(Env.getCtx(), matchInvoices[0].getM_Product_ID(), 0, getTrxName()); + productExpense = pc.getAccount(ProductCost.ACCTTYPE_P_Expense, as); + whereClause = null; + whereClause = "AD_Table_ID = " + MMatchInv.Table_ID; + whereClause += " AND Record_ID = " + matchInvoices[0].get_ID(); + whereClause += " AND C_AcctSchema_ID = " + as.getC_AcctSchema_ID(); + + int[] idsMatchInv = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause.toString(), getTrxName()); + + for (int id : idsMatchInv) + { + // Test Accounting for MatchINV + MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); + if (fa.getAccount_ID() == acctNIR.getAccount_ID()) + { + assertEquals(BD_20, fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), ""); + assertEquals(BigDecimal.TEN.setScale(2, RoundingMode.HALF_UP), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); + } + else if (fa.getAccount_ID() == productExpense.getAccount_ID()) + { + assertEquals(BD_20, fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), ""); + assertEquals(new BigDecimal("-10.00"), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); + } + } + + } + finally + { + rollback(); + if (product != null) + product.deleteEx(true); + if (category != null) + category.deleteEx(true); + } + + } + + @Test + public void testExpenseProduction() + { + MProductCategory category = createProductCategory(); + MProduct product = createProduct(category.get_ID()); + MProduct fgProdcut = null; + + try + { + MAcctSchema as = getAccountingSchema(); + MCost cost = getCost(as, product); + + // create PO And MR for the expense type product + createPOAndMRForProduct(product); + cost.load(getTrxName()); + assertNoAveragePOCost(product.get_ID()); + + // Create Product in which isBOM Flag true + fgProdcut = new MProduct(Env.getCtx(), 0, null); + fgProdcut.setName("Test_BOM"); + fgProdcut.setIsBOM(true); + fgProdcut.setIsStocked(true); + fgProdcut.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + fgProdcut.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + fgProdcut.setProductType(MProduct.PRODUCTTYPE_Item); + fgProdcut.setM_Product_Category_ID(category.get_ID()); + fgProdcut.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + fgProdcut.saveEx(); + + MCost fgCost = MCost.get(fgProdcut, 0, as, 0, DictionaryIDs.M_CostElement.MATERIAL.id, getTrxName()); + + // create Product BOM + MPPProductBOM bom = new MPPProductBOM(Env.getCtx(), 0, getTrxName()); + bom.setM_Product_ID(fgProdcut.get_ID()); + bom.setBOMType(MPPProductBOM.BOMTYPE_CurrentActive); + bom.setBOMUse(MPPProductBOM.BOMUSE_Master); + bom.setName(fgProdcut.getName()); + bom.saveEx(); + + // Add expense type product in the ProductBOMLine + MPPProductBOMLine bomLine = new MPPProductBOMLine(bom); + bomLine.setM_Product_ID(product.getM_Product_ID()); + bomLine.setQtyBOM(BD_2); + bomLine.saveEx(); + + // Create production for the BOM product + MProduction production = new MProduction(Env.getCtx(), 0, getTrxName()); + production.setM_Product_ID(fgProdcut.get_ID()); + production.setM_Locator_ID(DictionaryIDs.M_Locator.HQ.id); + production.setIsUseProductionPlan(false); + production.setMovementDate(getLoginDate()); + production.setDocAction(DocAction.ACTION_Complete); + production.setDocStatus(DocAction.STATUS_Drafted); + production.setIsComplete(false); + production.setProductionQty(BD_2); + production.setPP_Product_BOM_ID(bom.getPP_Product_BOM_ID()); + production.saveEx(); + + // Create ProductionLine for the BOM Product + MProcess process = MProcess.get(Env.getCtx(), 53226); // CreateProductionAndCreateLines + ProcessInfo pi = new ProcessInfo(process.getName(), process.get_ID()); + pi.setAD_Client_ID(getAD_Client_ID()); + pi.setAD_User_ID(getAD_User_ID()); + pi.setRecord_ID(production.get_ID()); + pi.setTransactionName(getTrxName()); + ServerProcessCtl.process(pi, getTrx(), false); + assertFalse(pi.isError(), pi.getSummary()); + + production.load(getTrxName()); + MProductionLine[] plines = production.getLines(); + assertEquals("Y", production.getIsCreated(), "MProduction.IsCreated != Y"); + assertTrue(plines.length > 0, "No Production Lines"); + assertEquals(2, plines.length, "Unexpected number of production lines"); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(production, DocAction.ACTION_Complete); + production.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals( DocAction.STATUS_Completed, production.getDocStatus(), + "Production Status=" + production.getDocStatus()); + + fgCost.load(getTrxName()); + + cost.load(getTrxName()); + + // Test the Cost and stock for the Expense type's Product + assertEquals(new BigDecimal("6.00"), cost.getCurrentQty().setScale(2, RoundingMode.HALF_UP), ""); + assertEquals(BD_2, cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), ""); + // Assert no storage records created for expense type product + MStorageOnHand[] storages = MStorageOnHand.getAll( Env.getCtx(), product.get_ID(), + DictionaryIDs.M_Locator.HQ.id, getTrxName(), false, 0); + assertEquals(0,storages.length,"No stocked should be used for expense type product Usage in Poroduction"); + + + // test Stock and cost of the Product in which BOM is created + assertEquals(BD_2, fgCost.getCurrentQty().setScale(2, RoundingMode.HALF_UP), ""); + assertEquals(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP), fgCost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), ""); + // Assert that OnHand Qty for the Finished goods of item type is created + BigDecimal onHand = MStorageOnHand.getQtyOnHand(fgProdcut.get_ID(),DictionaryIDs.M_Warehouse.HQ.id, 0, getTrxName()); + assertEquals(BD_2, onHand.setScale(2, RoundingMode.HALF_UP), "Storge for the Expense Type Product Should not be Created"); + + + } + finally + { + rollback(); + if (product != null) + product.deleteEx(true); + if (fgProdcut != null) + fgProdcut.deleteEx(true); + if (category != null) + category.deleteEx(true); + } + } + + /** + * Create Expense Type Product + * + * @param category_ID Standard Costing + * @return + */ + private MProduct createProduct(int category_ID) + { + MProduct product = new MProduct(Env.getCtx(), 0, null); + product.setName("testStandardCosting"); + product.setValue("testStandardCosting"); + product.setProductType(MProduct.PRODUCTTYPE_ExpenseType); + product.setIsStocked(false); + product.setIsSold(true); + product.setIsPurchased(true); + product.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + product.setM_Product_Category_ID(category_ID); + product.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + product.saveEx(); + + MPriceListVersion plv = MPriceList.get(DictionaryIDs.M_PriceList.PURCHASE.id).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(product.get_ID()); + pp.saveEx(); + + MPriceListVersion plvs = MPriceList.get(DictionaryIDs.M_PriceList.STANDARD.id).getPriceListVersion(null); + MProductPrice pps = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pps.setM_PriceList_Version_ID(plvs.getM_PriceList_Version_ID()); + pps.setM_Product_ID(product.get_ID()); + pps.saveEx(); + + return product; + + } + + /** + * Create PO And MR for the Expense Type Product + * + * @param product + * @return + */ + private MInOutLine createPOAndMRForProduct(MProduct product) + { + MOrder order = new MOrder(Env.getCtx(), 0, getTrxName()); + order.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id)); + order.setC_DocTypeTarget_ID(DictionaryIDs.C_DocType.PURCHASE_ORDER.id); + order.setIsSOTrx(false); + order.setSalesRep_ID(DictionaryIDs.AD_User.GARDEN_ADMIN.id); + order.setDocStatus(DocAction.STATUS_Drafted); + order.setDocAction(DocAction.ACTION_Complete); + Timestamp today = TimeUtil.getDay(System.currentTimeMillis()); + order.setDateOrdered(today); + order.setDatePromised(today); + order.saveEx(); + + MOrderLine oLine = new MOrderLine(order); + oLine.setLine(10); + oLine.setProduct(product); + oLine.setQty(BigDecimal.TEN); + oLine.setDatePromised(today); + oLine.setPrice(BD_2); + oLine.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(order, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + order.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, order.getDocStatus()); + + MInOut receipt = new MInOut(order, DictionaryIDs.C_DocType.MM_RECEIPT.id, order.getDateOrdered()); + receipt.setDocStatus(DocAction.STATUS_Drafted); + receipt.setDocAction(DocAction.ACTION_Complete); + receipt.saveEx(); + + MInOutLine rLine = new MInOutLine(receipt); + rLine.setOrderLine(oLine, 0, BigDecimal.TEN); + rLine.setQty(BigDecimal.TEN); + rLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, receipt.getDocStatus()); + if (!receipt.isPosted()) + { + String error = DocumentEngine.postImmediate( Env.getCtx(), receipt.getAD_Client_ID(), receipt.get_Table_ID(), + receipt.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + return rLine; + } + + /** + * Create SO And Shipment for the Expense Type Product + * + * @param product + * @return + */ + private MInOutLine createSOAndShipmentForProduct(MProduct product) + { + MOrder order = new MOrder(Env.getCtx(), 0, getTrxName()); + order.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id)); + order.setC_DocTypeTarget_ID(DictionaryIDs.C_DocType.STANDARD_ORDER.id); + order.setIsSOTrx(true); + order.setSalesRep_ID(DictionaryIDs.AD_User.GARDEN_ADMIN.id); + order.setDocStatus(DocAction.STATUS_Drafted); + order.setDocAction(DocAction.ACTION_Complete); + Timestamp today = TimeUtil.getDay(System.currentTimeMillis()); + order.setDateOrdered(today); + order.setDatePromised(today); + order.saveEx(); + + MOrderLine oLine = new MOrderLine(order); + oLine.setLine(10); + oLine.setProduct(product); + oLine.setQty(BD_5); + oLine.setDatePromised(today); + oLine.setPrice(BD_2); + oLine.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(order, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + order.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, order.getDocStatus()); + + MInOut receipt = new MInOut(order, DictionaryIDs.C_DocType.MM_SHIPMENT.id, order.getDateOrdered()); + receipt.setDocStatus(DocAction.STATUS_Drafted); + receipt.setDocAction(DocAction.ACTION_Complete); + receipt.setIsSOTrx(true); + receipt.saveEx(); + + MInOutLine rLine = new MInOutLine(receipt); + rLine.setOrderLine(oLine, 0, BD_5); + rLine.setQty(BD_5); + rLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, receipt.getDocStatus()); + + if (!receipt.isPosted()) + { + String error = DocumentEngine.postImmediate( Env.getCtx(), receipt.getAD_Client_ID(), receipt.get_Table_ID(), + receipt.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + return rLine; + } + + /** + * Get Accounting Schema + * + * @return + */ + private MAcctSchema getAccountingSchema() + { + MClient client = MClient.get(Env.getCtx()); + MAcctSchema as = client.getAcctSchema(); + return as; + } + + private MCost getCost(MAcctSchema as, MProduct product) + { + return MCost.get(product, 0, as, 0, DictionaryIDs.M_CostElement.MATERIAL.id, getTrxName()); + } + + /** + * Create Product Category "Standard Costing" + * + * @return + */ + private MProductCategory createProductCategory() + { + MProductCategory category = new MProductCategory(Env.getCtx(), 0, null); + category.setName("Average PO"); + category.saveEx(); + String whereClause = "M_Product_Category_ID=?"; + List categoryAccts = new Query(Env.getCtx(), MProductCategoryAcct.Table_Name, whereClause, null) + .setParameters(category.get_ID()) + .list(); + for (MProductCategoryAcct categoryAcct : categoryAccts) + { + categoryAcct.setCostingMethod(MAcctSchema.COSTINGMETHOD_AveragePO); + categoryAcct.saveEx(); + } + return category; + } + + private void assertNoAveragePOCost(int productID) + { + List costs = new Query(Env.getCtx(), MCost.Table_Name, " M_Product_ID = ? ", null) + .setParameters(productID) + .list(); + for (MCost cost : costs) + { + assertFalse(DictionaryIDs.M_CostElement.AVERAGE_PO.id == cost.getM_CostElement_ID(),"Average PO cost should not be created for Expense type product"); + } + + } +} diff --git a/org.idempiere.test/src/org/idempiere/test/costing/NonStockedExpTypeStdCostingTest.java b/org.idempiere.test/src/org/idempiere/test/costing/NonStockedExpTypeStdCostingTest.java new file mode 100644 index 0000000000..db276c8c80 --- /dev/null +++ b/org.idempiere.test/src/org/idempiere/test/costing/NonStockedExpTypeStdCostingTest.java @@ -0,0 +1,774 @@ +/****************************************************************************** + * Copyright (C) 2016 Logilite Technologies LLP * + * 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. * + *****************************************************************************/ +package org.idempiere.test.costing; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.Timestamp; +import java.util.List; + +import org.compiere.acct.Doc; +import org.compiere.acct.DocManager; +import org.compiere.model.MAccount; +import org.compiere.model.MAcctSchema; +import org.compiere.model.MBPartner; +import org.compiere.model.MClient; +import org.compiere.model.MCost; +import org.compiere.model.MFactAcct; +import org.compiere.model.MInOut; +import org.compiere.model.MInOutConfirm; +import org.compiere.model.MInOutLine; +import org.compiere.model.MInvoice; +import org.compiere.model.MInvoiceLine; +import org.compiere.model.MMatchInv; +import org.compiere.model.MMatchPO; +import org.compiere.model.MOrder; +import org.compiere.model.MOrderLine; +import org.compiere.model.MPriceList; +import org.compiere.model.MPriceListVersion; +import org.compiere.model.MProcess; +import org.compiere.model.MProduct; +import org.compiere.model.MProductCategory; +import org.compiere.model.MProductCategoryAcct; +import org.compiere.model.MProductPrice; +import org.compiere.model.MProduction; +import org.compiere.model.MProductionLine; +import org.compiere.model.MRMA; +import org.compiere.model.MRMALine; +import org.compiere.model.MStorageOnHand; +import org.compiere.model.ProductCost; +import org.compiere.model.Query; +import org.compiere.model.SystemIDs; +import org.compiere.process.DocAction; +import org.compiere.process.DocumentEngine; +import org.compiere.process.ProcessInfo; +import org.compiere.process.ServerProcessCtl; +import org.compiere.util.Env; +import org.compiere.util.TimeUtil; +import org.compiere.util.Util; +import org.compiere.wf.MWorkflow; +import org.eevolution.model.MPPProductBOM; +import org.eevolution.model.MPPProductBOMLine; +import org.idempiere.test.AbstractTestCase; +import org.idempiere.test.DictionaryIDs; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Isolated; + +/** + * Test cases Stocked Expense type product with Standard PO Costing Method. + * + * @author Logilite Technologies + * @since September 15, 2023 + */ +@Isolated +public class NonStockedExpTypeStdCostingTest extends AbstractTestCase +{ + private static final BigDecimal BD_2 = new BigDecimal("2.00"); + private static final BigDecimal BD_5 = new BigDecimal("5.00"); + private static final BigDecimal BD_20 = new BigDecimal("20.00"); + + public NonStockedExpTypeStdCostingTest() + { + } + + @Test + public void testMaterialReceipt() + { + MProductCategory category = createProductCategory(); + MProduct product = createProduct(category.get_ID()); + try + { + MAcctSchema as = getAccountingSchema(); + MCost cost = getCost(as, product); + assertNotNull(cost, "No MCost record found"); + + // Create Material Receipt + MInOutLine rLine = createPOAndMRForProduct(product); + //MatchPO + MMatchPO[] matchPOs = MMatchPO.get(Env.getCtx(), rLine.getM_InOutLine_ID(), getTrxName()); + assertEquals(1, matchPOs.length); + assertEquals(rLine.getC_OrderLine_ID(), matchPOs[0].getC_OrderLine_ID()); + assertEquals(rLine.getM_InOutLine_ID(), matchPOs[0].getM_InOutLine_ID()); + assertTrue(matchPOs[0].getQty().compareTo(BigDecimal.TEN) == 0); + + cost.load(getTrxName()); + + + // testing cost and stock of the product after completing MR + assertEquals(BD_2, cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + assertEquals(BigDecimal.TEN.setScale(2, RoundingMode.HALF_UP), cost.getCurrentQty().setScale(2, RoundingMode.HALF_UP), "Unexpected current Qty"); + // Assert no storage records created for expense type product + MStorageOnHand[] storages = MStorageOnHand.getAll( Env.getCtx(), product.get_ID(), + DictionaryIDs.M_Locator.HQ.id, getTrxName(), false, 0); + assertEquals(0,storages.length,"No storage records should be created for expense type product"); + + // Test Acc. For MR + // get NotInvoicedReceipts Of the created MR + Doc doc = DocManager.getDocument(as, MInOut.Table_ID, rLine.getM_InOut_ID(), getTrxName()); + doc.setC_BPartner_ID(rLine.getM_InOut().getC_BPartner_ID()); + MAccount acctNIR = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + + // get ProductExpense Of the created MR + ProductCost pc = new ProductCost(Env.getCtx(), rLine.getM_Product_ID(), 0, getTrxName()); + MAccount productExpense = pc.getAccount(ProductCost.ACCTTYPE_P_Expense, as); + + StringBuilder whereClause = new StringBuilder(); + whereClause + .append(MFactAcct.COLUMNNAME_AD_Table_ID) + .append("=") + .append(MInOut.Table_ID) + .append(" AND ") + .append(MFactAcct.COLUMNNAME_Record_ID) + .append("=") + .append(rLine.getM_InOut_ID()) + .append(" AND ") + .append(MFactAcct.COLUMNNAME_C_AcctSchema_ID) + .append("=") + .append(as.getC_AcctSchema_ID()); + + int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause.toString(), getTrxName()); + + for (int id : ids) + { + // Test Accounting for MR + MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); + if (fa.getAccount_ID() == acctNIR.getAccount_ID()) + { + assertEquals(BD_20, fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), ""); + assertEquals(new BigDecimal("-10.00"), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); + } + else if (fa.getAccount_ID() == productExpense.getAccount_ID()) + { + assertEquals(BD_20, fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), ""); + assertEquals(BigDecimal.TEN.setScale(2, RoundingMode.HALF_UP), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); + } + } + + } + finally + { + rollback(); + if (product != null) + product.deleteEx(true); + if (category != null) + category.deleteEx(true); + } + } + + + @Test + public void testSOAndShipment() + { + MProductCategory category = createProductCategory(); + MProduct product = createProduct(category.get_ID()); + + try + { + MAcctSchema as = getAccountingSchema(); + MCost cost = getCost(as, product); + assertNotNull(cost, "No MCost record found"); + + // Create PO And MR + createPOAndMRForProduct(product); + cost.load(getTrxName()); + + // Create SO And Shipment + createSOAndShipmentForProduct(product); + cost.load(getTrxName()); + + // testing cost and stock of the product after completing Shipment + assertEquals(BD_2, cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + assertEquals(BD_5, cost.getCurrentQty().setScale(2, RoundingMode.HALF_UP), "Unexpected current Qty"); + // Assert no storage records created for expense type product + MStorageOnHand[] storages = MStorageOnHand.getAll( Env.getCtx(), product.get_ID(), + DictionaryIDs.M_Locator.HQ.id, getTrxName(), false, 0); + assertEquals(0,storages.length,"No storage records should be created for expense type product"); + + } + finally + { + rollback(); + if (product != null) + product.deleteEx(true); + if (category != null) + category.deleteEx(true); + } + + } + + @Test + public void testCustomerRMA() + { + MProductCategory category = createProductCategory(); + MProduct product = createProduct(category.get_ID()); + + try + { + MAcctSchema as = getAccountingSchema(); + MCost cost = getCost(as, product); + assertNotNull(cost, "No MCost record found"); + // Create PO And MR + createPOAndMRForProduct(product); + cost.load(getTrxName()); + + // Create SO And Shipment + MInOutLine sLine = createSOAndShipmentForProduct(product); + cost.load(getTrxName()); + + // Create MRA + MRMA rma = new MRMA(Env.getCtx(), 0, getTrxName()); + rma.setName("testCustomerReturnMaterial"); + rma.setC_DocType_ID(DictionaryIDs.C_DocType.CUSTOMER_RETURN_MATERIAL.id); + rma.setM_RMAType_ID(DictionaryIDs.M_RMAType.DAMAGE_ON_ARRIVAL.id); + rma.setM_InOut_ID(sLine.getM_InOut_ID()); + rma.setIsSOTrx(true); + rma.setSalesRep_ID(SystemIDs.USER_SUPERUSER); + rma.saveEx(); + + // Create MRALine + MRMALine rmaLine = new MRMALine(Env.getCtx(), 0, getTrxName()); + rmaLine.setLine(10); + rmaLine.setM_RMA_ID(rma.get_ID()); + rmaLine.setQty(new BigDecimal("3")); + rmaLine.setM_InOutLine_ID(sLine.get_ID()); + rmaLine.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(rma, DocAction.ACTION_Complete); + rma.load(getTrxName()); + + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, rma.getDocStatus()); + + // Create shipment for Customer Return Material + MInOut delivery = new MInOut(Env.getCtx(), 0, getTrxName()); + delivery.setM_RMA_ID(rma.get_ID()); + delivery.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id)); + delivery.setC_DocType_ID(DictionaryIDs.C_DocType.MM_CUSTOMER_RETURN.id); + delivery.setMovementType(MInOut.MOVEMENTTYPE_CustomerReturns); + delivery.setIsSOTrx(true); + delivery.setDocStatus(DocAction.STATUS_Drafted); + delivery.setDocAction(DocAction.ACTION_Complete); + delivery.setM_Warehouse_ID(DictionaryIDs.M_Warehouse.HQ.id); + delivery.saveEx(); + + MInOutLine dLine = new MInOutLine(delivery); + dLine.setM_RMALine_ID(rmaLine.get_ID()); + dLine.setLine(10); + dLine.setProduct(new MProduct(Env.getCtx(), product.getM_Product_ID(), getTrxName())); + dLine.setM_Locator_ID(sLine.getM_Locator_ID()); + dLine.setQty(new BigDecimal("3")); + dLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(delivery, DocAction.ACTION_Complete); + delivery.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + + // Need to complete the InoutConfirmation to complete the return + // material + MInOutConfirm[] confirmations = delivery.getConfirmations(true); + assertEquals(1, confirmations.length, "Expected 1 Shipment Confirmation Document"); + + assertTrue(confirmations[0].getM_InOutConfirm_ID() != delivery.getM_InOut_ID(), "Test should run with M_InOut_ID != M_InOutConfirm_ID"); + + info = MWorkflow.runDocumentActionWorkflow(confirmations[0], DocAction.ACTION_Complete); + if (info.isError()) + { + fail("Failed to complete shipment confirmation" + (Util.isEmpty(info.getSummary()) ? "" : " : " + info.getSummary())); + return; + } + confirmations[0].load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, confirmations[0].getDocStatus(), "Expected Completed Status for Shipment Confirmation"); + + info = MWorkflow.runDocumentActionWorkflow(delivery, DocAction.ACTION_Complete); + delivery.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, delivery.getDocStatus(), "Expected Completed Status for Shipment"); + + if (!delivery.isPosted()) + { + String error = DocumentEngine.postImmediate(Env.getCtx(), delivery.getAD_Client_ID(), MInOut.Table_ID, delivery.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + cost.load(getTrxName()); + + // Complete the MInOutConfirm to complete the delivery + assertEquals(BD_2, cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost price"); + assertEquals(new BigDecimal("8.00"), cost.getCurrentQty().setScale(2, RoundingMode.HALF_UP), "Unexpected current cost Qty"); + // Assert no storage records created for expense type product + MStorageOnHand[] storages = MStorageOnHand.getAll( Env.getCtx(), product.get_ID(), + DictionaryIDs.M_Locator.HQ.id, getTrxName(), false, 0); + assertEquals(0, storages.length,"No storage records should be created for expense type product"); + + } + finally + { + rollback(); + if (product != null) + product.deleteEx(true); + if (category != null) + category.deleteEx(true); + } + + } + + @Test + public void testInvoice() + { + MProductCategory category = createProductCategory(); + MProduct product = createProduct(category.get_ID()); + + try + { + MAcctSchema as = getAccountingSchema(); + MCost cost = getCost(as, product); + assertNotNull(cost, "No MCost record found"); + // Create PO And MR + MInOutLine rLine = createPOAndMRForProduct(product); + cost.load(getTrxName()); + + // Create Invoice from the MR + MInOut mInOut = (MInOut) rLine.getM_InOut(); + MInvoice invoice = new MInvoice(mInOut, rLine.getM_InOut().getMovementDate()); + invoice.setDocStatus(DocAction.STATUS_Drafted); + invoice.setDocAction(DocAction.ACTION_Complete); + invoice.saveEx(); + + MOrderLine oLine = (MOrderLine) rLine.getC_OrderLine(); + MInvoiceLine iLine = new MInvoiceLine(invoice); + iLine.setOrderLine(oLine); + iLine.setLine(10); + iLine.setProduct(new MProduct(Env.getCtx(), product, getTrxName())); + iLine.setQty(BigDecimal.TEN); + iLine.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(invoice, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + invoice.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, invoice.getDocStatus()); + invoice.getDocsPostProcess().clear(); + if (!invoice.isPosted()) + { + String error = DocumentEngine.postImmediate( Env.getCtx(), invoice.getAD_Client_ID(), + invoice.get_Table_ID(), invoice.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + // get ProductExpense Of the Invoice + ProductCost pc = new ProductCost(Env.getCtx(), iLine.getM_Product_ID(), 0, getTrxName()); + MAccount productExpense = pc.getAccount(ProductCost.ACCTTYPE_P_Expense, as); + + // get AccPayable of the Invoice + MAccount acctPT = new MAccount(Env.getCtx(), as.getAcctSchemaDefault().getV_Liability_Acct(), getTrxName()); + + StringBuilder whereClause = new StringBuilder(); + + whereClause + .append(MFactAcct.COLUMNNAME_AD_Table_ID) + .append("=") + .append(MInvoice.Table_ID) + .append(" AND ") + .append(MFactAcct.COLUMNNAME_Record_ID) + .append("=") + .append(iLine.getC_Invoice_ID()) + .append(" AND ") + .append(MFactAcct.COLUMNNAME_C_AcctSchema_ID) + .append("=") + .append(as.getC_AcctSchema_ID()); + + int[] ids = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause.toString(), getTrxName()); + + for (int id : ids) + { + // Test Accounting for Invoice + MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); + if (fa.getAccount_ID() == acctPT.getAccount_ID()) + { + assertEquals(BD_20, fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), ""); + assertEquals(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); + } + else if (fa.getAccount_ID() == productExpense.getAccount_ID()) + { + assertEquals(BD_20, fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), ""); + assertEquals(BigDecimal.TEN.setScale(2, RoundingMode.HALF_UP), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); + } + } + + //Accounting MatchInv + MMatchInv[] matchInvoices = MMatchInv.getInOut(Env.getCtx(), rLine.getM_InOut_ID(), getTrxName()); + assertEquals(1, matchInvoices.length); + assertEquals(rLine.getM_InOutLine_ID(), matchInvoices[0].getM_InOutLine_ID()); + assertTrue(matchInvoices[0].getQty().compareTo(BigDecimal.TEN) == 0, + "Expected 5, get " + matchInvoices[0].getQty()); + + // get NotInvoicedReceipts for the MatchInv + Doc doc = DocManager.getDocument(as, MMatchInv.Table_ID, matchInvoices[0].get_ID(), getTrxName()); + doc.setC_BPartner_ID(matchInvoices[0].getC_InvoiceLine().getC_Invoice().getC_BPartner_ID()); + MAccount acctNIR = doc.getAccount(Doc.ACCTTYPE_NotInvoicedReceipts, as); + + // get Product Expense for the MatchInv + pc = new ProductCost(Env.getCtx(), matchInvoices[0].getM_Product_ID(), 0, getTrxName()); + productExpense = pc.getAccount(ProductCost.ACCTTYPE_P_Expense, as); + + whereClause = null; + whereClause = new StringBuilder(); + whereClause + .append(MFactAcct.COLUMNNAME_AD_Table_ID) + .append("=") + .append(MMatchInv.Table_ID) + .append(" AND ") + .append(MFactAcct.COLUMNNAME_Record_ID) + .append("=") + .append(matchInvoices[0].get_ID()) + .append(" AND ") + .append(MFactAcct.COLUMNNAME_C_AcctSchema_ID) + .append("=") + .append(as.getC_AcctSchema_ID()); + + int[] idsMatchInv = MFactAcct.getAllIDs(MFactAcct.Table_Name, whereClause.toString(), getTrxName()); + + for (int id : idsMatchInv) + { + // Test Accounting for MatchINV + MFactAcct fa = new MFactAcct(Env.getCtx(), id, getTrxName()); + if (fa.getAccount_ID() == acctNIR.getAccount_ID()) + { + assertEquals(BD_20, fa.getAmtAcctDr().setScale(2, RoundingMode.HALF_UP), ""); + assertEquals(BigDecimal.TEN.setScale(2, RoundingMode.HALF_UP), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); + } + else if (fa.getAccount_ID() == productExpense.getAccount_ID()) + { + assertEquals(BD_20, fa.getAmtAcctCr().setScale(2, RoundingMode.HALF_UP), ""); + assertEquals(new BigDecimal("-10.00"), fa.getQty().setScale(2, RoundingMode.HALF_UP), ""); + } + } + + } + finally + { + rollback(); + if (product != null) + product.deleteEx(true); + if (category != null) + category.deleteEx(true); + } + + } + + @Test + public void testExpenseProductInBOM() + { + MProductCategory category = createProductCategory(); + MProduct product = createProduct(category.get_ID()); + MProduct fgProdcut = null; + + try + { + MAcctSchema as = getAccountingSchema(); + MCost cost = getCost(as, product); + assertNotNull(cost, "No MCost record found"); + + createPOAndMRForProduct(product); + cost.load(getTrxName()); + + // create product in which BOM is true + fgProdcut = new MProduct(Env.getCtx(), 0, null); + fgProdcut.setName("Test_BOM"); + fgProdcut.setIsBOM(true); + fgProdcut.setIsStocked(true); + fgProdcut.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + fgProdcut.setM_Product_Category_ID(DictionaryIDs.M_Product_Category.STANDARD.id); + fgProdcut.setProductType(MProduct.PRODUCTTYPE_Item); + fgProdcut.setM_Product_Category_ID(category.get_ID()); + fgProdcut.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + fgProdcut.saveEx(); + + MCost fgCost = MCost.get(fgProdcut, 0, as, 0, DictionaryIDs.M_CostElement.MATERIAL.id, getTrxName()); + + // create productBOM + MPPProductBOM bom = new MPPProductBOM(Env.getCtx(), 0, getTrxName()); + bom.setM_Product_ID(fgProdcut.get_ID()); + bom.setBOMType(MPPProductBOM.BOMTYPE_CurrentActive); + bom.setBOMUse(MPPProductBOM.BOMUSE_Master); + bom.setName(fgProdcut.getName()); + bom.saveEx(); + + // Add expense type product in the BOM Line + MPPProductBOMLine bomLine = new MPPProductBOMLine(bom); + bomLine.setM_Product_ID(product.getM_Product_ID()); + bomLine.setQtyBOM(BD_2); + bomLine.saveEx(); + + // create production for the BOM Product + MProduction production = new MProduction(Env.getCtx(), 0, getTrxName()); + production.setM_Product_ID(fgProdcut.get_ID()); + production.setM_Locator_ID(DictionaryIDs.M_Locator.HQ.id); + production.setIsUseProductionPlan(false); + production.setMovementDate(getLoginDate()); + production.setDocAction(DocAction.ACTION_Complete); + production.setDocStatus(DocAction.STATUS_Drafted); + production.setIsComplete(false); + production.setProductionQty(BD_2); + production.setPP_Product_BOM_ID(bom.getPP_Product_BOM_ID()); + production.saveEx(); + + // create production lines from the process + MProcess process = MProcess.get(Env.getCtx(), 53226);// CreateProductionAndCreateLines + ProcessInfo pi = new ProcessInfo(process.getName(), process.get_ID()); + pi.setAD_Client_ID(getAD_Client_ID()); + pi.setAD_User_ID(getAD_User_ID()); + pi.setRecord_ID(production.get_ID()); + pi.setTransactionName(getTrxName()); + ServerProcessCtl.process(pi, getTrx(), false); + assertFalse(pi.isError(), pi.getSummary()); + + production.load(getTrxName()); + MProductionLine[] plines = production.getLines(); + assertEquals("Y", production.getIsCreated(), "MProduction.IsCreated != Y"); + assertTrue(plines.length > 0, "No Production Lines"); + assertEquals(2, plines.length, "Unexpected number of production lines"); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(production, DocAction.ACTION_Complete); + production.load(getTrxName()); + assertFalse(info.isError(), info.getSummary()); + assertEquals(DocAction.STATUS_Completed, production.getDocStatus(), + "Production Status=" + production.getDocStatus()); + fgCost.load(getTrxName()); + + cost.load(getTrxName()); + + // Test the Cost and stock for the Expense type's Product + assertEquals(new BigDecimal("6.00"), cost.getCurrentQty().setScale(2, RoundingMode.HALF_UP), ""); + assertEquals(BD_2, cost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), ""); + // Assert no storage records created for expense type product + MStorageOnHand[] storages = MStorageOnHand.getAll( Env.getCtx(), product.get_ID(), + DictionaryIDs.M_Locator.HQ.id, getTrxName(), false, 0); + assertEquals(0, storages.length,"No storage records should be created for expense type product"); + + // test Stock and cost of the Product in which BOM is created + assertEquals(BD_2, fgCost.getCurrentQty().setScale(2, RoundingMode.HALF_UP), ""); + assertEquals(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP), fgCost.getCurrentCostPrice().setScale(2, RoundingMode.HALF_UP), ""); + // Assert that OnHand Qty for the item type finished good Product created + BigDecimal onHand = MStorageOnHand.getQtyOnHand(fgProdcut.get_ID(),DictionaryIDs.M_Warehouse.HQ.id, 0, getTrxName()); + assertEquals(BD_2, onHand.setScale(2, RoundingMode.HALF_UP), "Storge for the Item Type Product Should not be Created"); + + } + finally + { + rollback(); + if (product != null) + product.deleteEx(true); + if (fgProdcut != null) + fgProdcut.deleteEx(true); + if (category != null) + category.deleteEx(true); + } + } + + /** + * Create Expense Type Product + * + * @param category_ID Standard Costing + * @return + */ + private MProduct createProduct(int category_ID) + { + MProduct product = new MProduct(Env.getCtx(), 0, null); + product.setName("testStandardCosting"); + product.setValue("testStandardCosting"); + product.setProductType(MProduct.PRODUCTTYPE_ExpenseType); + product.setIsStocked(false); + product.setIsSold(true); + product.setIsPurchased(true); + product.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id); + product.setM_Product_Category_ID(category_ID); + product.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id); + product.saveEx(); + + MPriceListVersion plv = MPriceList.get(DictionaryIDs.M_PriceList.PURCHASE.id).getPriceListVersion(null); + MProductPrice pp = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pp.setM_PriceList_Version_ID(plv.getM_PriceList_Version_ID()); + pp.setM_Product_ID(product.get_ID()); + pp.saveEx(); + + MPriceListVersion plvs = MPriceList.get(DictionaryIDs.M_PriceList.STANDARD.id).getPriceListVersion(null); + MProductPrice pps = new MProductPrice(Env.getCtx(), 0, getTrxName()); + pps.setM_PriceList_Version_ID(plvs.getM_PriceList_Version_ID()); + pps.setM_Product_ID(product.get_ID()); + pps.saveEx(); + + return product; + } + + /** + * Create PO And MR for the expense type product + * + * @param product + * @return + */ + private MInOutLine createPOAndMRForProduct(MProduct product) + { + MOrder order = new MOrder(Env.getCtx(), 0, getTrxName()); + order.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id)); + order.setC_DocTypeTarget_ID(DictionaryIDs.C_DocType.PURCHASE_ORDER.id); + order.setIsSOTrx(false); + order.setSalesRep_ID(DictionaryIDs.AD_User.GARDEN_ADMIN.id); + order.setDocStatus(DocAction.STATUS_Drafted); + order.setDocAction(DocAction.ACTION_Complete); + Timestamp today = TimeUtil.getDay(System.currentTimeMillis()); + order.setDateOrdered(today); + order.setDatePromised(today); + order.saveEx(); + + MOrderLine oLine = new MOrderLine(order); + oLine.setLine(10); + oLine.setProduct(product); + oLine.setQty(BigDecimal.TEN); + oLine.setDatePromised(today); + oLine.setPrice(BD_2); + oLine.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(order, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + order.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, order.getDocStatus()); + + MInOut receipt = new MInOut(order, DictionaryIDs.C_DocType.MM_RECEIPT.id, order.getDateOrdered()); + receipt.setDocStatus(DocAction.STATUS_Drafted); + receipt.setDocAction(DocAction.ACTION_Complete); + receipt.saveEx(); + + MInOutLine rLine = new MInOutLine(receipt); + rLine.setOrderLine(oLine, 0, BigDecimal.TEN); + rLine.setQty(BigDecimal.TEN); + rLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, receipt.getDocStatus()); + if (!receipt.isPosted()) + { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt.getAD_Client_ID(), receipt.get_Table_ID(), + receipt.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + return rLine; + } + + /** + * Create SO And Shipment for the Expense type Product + * + * @param product + * @return + */ + private MInOutLine createSOAndShipmentForProduct(MProduct product) + { + MOrder order = new MOrder(Env.getCtx(), 0, getTrxName()); + order.setBPartner(MBPartner.get(Env.getCtx(), DictionaryIDs.C_BPartner.PATIO.id)); + order.setC_DocTypeTarget_ID(DictionaryIDs.C_DocType.STANDARD_ORDER.id); + order.setIsSOTrx(true); + order.setSalesRep_ID(DictionaryIDs.AD_User.GARDEN_ADMIN.id); + order.setDocStatus(DocAction.STATUS_Drafted); + order.setDocAction(DocAction.ACTION_Complete); + Timestamp today = TimeUtil.getDay(System.currentTimeMillis()); + order.setDateOrdered(today); + order.setDatePromised(today); + order.saveEx(); + + MOrderLine oLine = new MOrderLine(order); + oLine.setLine(10); + oLine.setProduct(product); + oLine.setQty(BD_5); + oLine.setDatePromised(today); + oLine.setPrice(BD_2); + oLine.saveEx(); + + ProcessInfo info = MWorkflow.runDocumentActionWorkflow(order, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + order.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, order.getDocStatus()); + + MInOut receipt = new MInOut(order, DictionaryIDs.C_DocType.MM_SHIPMENT.id, order.getDateOrdered()); + receipt.setDocStatus(DocAction.STATUS_Drafted); + receipt.setDocAction(DocAction.ACTION_Complete); + receipt.setIsSOTrx(true); + receipt.saveEx(); + + MInOutLine rLine = new MInOutLine(receipt); + rLine.setOrderLine(oLine, 0, BD_5); + rLine.setQty(BD_5); + rLine.saveEx(); + + info = MWorkflow.runDocumentActionWorkflow(receipt, DocAction.ACTION_Complete); + assertFalse(info.isError(), info.getSummary()); + receipt.load(getTrxName()); + assertEquals(DocAction.STATUS_Completed, receipt.getDocStatus()); + + if (!receipt.isPosted()) + { + String error = DocumentEngine.postImmediate(Env.getCtx(), receipt.getAD_Client_ID(), receipt.get_Table_ID(), + receipt.get_ID(), false, getTrxName()); + assertNull(error, error); + } + + return rLine; + } + + private MCost getCost(MAcctSchema as, MProduct product) + { + return product.getCostingRecord(as, getAD_Org_ID(), 0, MAcctSchema.COSTINGMETHOD_StandardCosting); + } + + /** + * Get Accounting Schema + * + * @return + */ + private MAcctSchema getAccountingSchema() + { + MClient client = MClient.get(Env.getCtx()); + MAcctSchema as = client.getAcctSchema(); + return as; + } + + /** + * Create Product Category "Standard Costing" + * + * @return + */ + private MProductCategory createProductCategory() + { + MProductCategory category = new MProductCategory(Env.getCtx(), 0, null); + category.setName("Standard Costing"); + category.saveEx(); + + String whereClause = "M_Product_Category_ID=?"; + List categoryAccts = new Query(Env.getCtx(), MProductCategoryAcct.Table_Name, whereClause, null) + .setParameters(category.get_ID()) + .list(); + for (MProductCategoryAcct categoryAcct : categoryAccts) + { + categoryAcct.setCostingMethod(MAcctSchema.COSTINGMETHOD_StandardCosting); + categoryAcct.saveEx(); + } + return category; + } +} \ No newline at end of file