feat: Enhance production line handling and inventory management

- Added a new callout in MID_CalloutProductionLine to set locator based on production details and product type.
- Updated MID_DocInvoice to handle service products correctly during invoice processing.
- Introduced MID_MStorageOnHand to manage storage on hand with additional methods for creating and retrieving storage records.
- Created MID_Production and MID_ProductionLine classes to extend production functionalities, including transaction handling and validation for end products.
- Implemented SQL query for invoice details with comprehensive data retrieval for reporting purposes.
This commit is contained in:
faisolavolut 2025-07-19 22:13:17 +08:00
parent 0fd0590bc5
commit 014b14c47b
11 changed files with 1050 additions and 7 deletions

View File

@ -6,6 +6,7 @@ import org.adempiere.base.IColumnCallout;
import org.compiere.model.CalloutEngine;
import org.compiere.model.GridField;
import org.compiere.model.GridTab;
import org.compiere.model.MDocType;
import org.compiere.model.MInOut;
import org.compiere.model.MOrder;
@ -19,13 +20,91 @@ public class MID_CalloutInOut extends CalloutEngine implements IColumnCallout{
if(mField.getColumnName().equals(MInOut.COLUMNNAME_MovementDate)) {
mTab.setValue(MInOut.COLUMNNAME_DateAcct, value);
}
if(mField.getColumnName().equals(MInOut.COLUMNNAME_C_Order_ID)) {
order(ctx, WindowNo, mTab, mField, value);
}
// if(mField.getColumnName().equals(MInOut.COLUMNNAME_C_Order_ID)) {
// setAJU(ctx, WindowNo, mTab, mField, value, oldValue);
// }
return null;
}
/**
* C_Order - Order Defaults.
* @param ctx
* @param WindowNo
* @param mTab
* @param mField
* @param value
* @return error message or ""
*/
public String order (Properties ctx, int WindowNo, GridTab mTab, GridField mField, Object value)
{
Integer C_Order_ID = (Integer)value;
if (C_Order_ID == null || C_Order_ID.intValue() == 0)
return "";
// No Callout Active to fire dependent values
if (isCalloutActive()) // prevent recursive
return "";
// Get Details
MOrder order = new MOrder (ctx, C_Order_ID.intValue(), null);
if (order.get_ID() != 0)
{
mTab.setValue("DateOrdered", order.getDateOrdered());
mTab.setValue("POReference", order.getPOReference());
mTab.setValue("AD_Org_ID", Integer.valueOf(order.getAD_Org_ID()));
mTab.setValue("AD_OrgTrx_ID", Integer.valueOf(order.getAD_OrgTrx_ID()));
mTab.setValue("C_Activity_ID", Integer.valueOf(order.getC_Activity_ID()));
mTab.setValue("C_Campaign_ID", Integer.valueOf(order.getC_Campaign_ID()));
mTab.setValue("C_Project_ID", Integer.valueOf(order.getC_Project_ID()));
mTab.setValue("User1_ID", Integer.valueOf(order.getUser1_ID()));
mTab.setValue("User2_ID", Integer.valueOf(order.getUser2_ID()));
mTab.setValue("M_Warehouse_ID", Integer.valueOf(order.getM_Warehouse_ID()));
//
mTab.setValue("DeliveryRule", order.getDeliveryRule());
mTab.setValue("DeliveryViaRule", order.getDeliveryViaRule());
mTab.setValue("M_Shipper_ID", Integer.valueOf(order.getM_Shipper_ID()));
mTab.setValue("FreightCostRule", order.getFreightCostRule());
mTab.setValue("FreightAmt", order.getFreightAmt());
mTab.setValue("C_BPartner_ID", Integer.valueOf(order.getC_BPartner_ID()));
mTab.setValue("SalesRep_ID", Integer.valueOf(order.getSalesRep_ID()));
//[ 1867464 ]
mTab.setValue("C_BPartner_Location_ID", Integer.valueOf(order.getC_BPartner_Location_ID()));
if (order.getAD_User_ID() > 0)
mTab.setValue("AD_User_ID", Integer.valueOf(order.getAD_User_ID()));
else
mTab.setValue("AD_User_ID", null);
if (order.isDropShip()) {
mTab.setValue(MInOut.COLUMNNAME_IsDropShip, order.isDropShip());
mTab.setValue(MInOut.COLUMNNAME_DropShip_BPartner_ID, order.getDropShip_BPartner_ID());
mTab.setValue(MInOut.COLUMNNAME_DropShip_Location_ID, order.getDropShip_Location_ID());
mTab.setValue(MInOut.COLUMNNAME_DropShip_User_ID, order.getDropShip_User_ID());
}
}
/**
* Modification: set corresponding document type
*/
if (mTab.getValue("C_DocType_ID") != null)
{
mTab.setValue("C_DocType_ID", mTab.getValue("C_DocType_ID"));
}
// int docTypeId = order.getC_DocType_ID();
// int relatedDocTypeId = 0;
//
// if (docTypeId == 0)
// {
// docTypeId = order.getC_DocTypeTarget_ID();
// }
//
// relatedDocTypeId = MDocType.getShipmentReceiptDocType(docTypeId);
//
// mTab.setValue("C_DocType_ID", relatedDocTypeId);
return "";
} // order
public String setAJU(Properties ctx, int WindowNo, GridTab mTab, GridField mField, Object value, Object oldValue) {
MOrder order = new MOrder(ctx, (int)value, null);

View File

@ -6,8 +6,13 @@ import org.adempiere.base.IColumnCallout;
import org.compiere.model.CalloutEngine;
import org.compiere.model.GridField;
import org.compiere.model.GridTab;
import org.compiere.model.MInOut;
import org.compiere.model.MInOutLine;
import org.compiere.model.MLocator;
import org.compiere.model.MOrderLine;
import org.compiere.model.MProduct;
import org.compiere.model.MProduction;
import org.compiere.model.Query;
public class MID_CalloutInOutLine extends CalloutEngine implements IColumnCallout{
@ -19,10 +24,37 @@ public class MID_CalloutInOutLine extends CalloutEngine implements IColumnCallou
if(mField.getColumnName().equals(MInOutLine.COLUMNNAME_C_OrderLine_ID)) {
this.setDescription(ctx, WindowNo, mTab, mField, value, oldValue);
}
if(mField.getColumnName().equals(MInOutLine.COLUMNNAME_M_Product_ID)) {
setLocator(ctx, WindowNo, mTab, mField, value, oldValue);
}
return null;
}
public String setLocator(Properties ctx, int WindowNo, GridTab mTab, GridField mField, Object value,
Object oldValue) {
int MProduct_ID = (int) mTab.getValue("M_Product_ID");
int InOutID = (int) mTab.getValue("M_InOut_ID");
MInOut inout = new MInOut(ctx, InOutID, null);
MProduct product = new MProduct(ctx, MProduct_ID, null);
boolean isService = (boolean) product.get_Value("IsService");
if (isService) {
if (inout.getM_Warehouse_ID() > 0) {
int Warehouse_ID = inout.getM_Warehouse_ID();
// get locator in same warehouse IsEnableNegative
int M_Location_ID = new Query(ctx, MLocator.Table_Name, "IsEnableNegative = 'Y' and M_Warehouse_ID=?",
null)
.setParameters(Warehouse_ID)
.setOnlyActiveRecords(true)
.firstId();
if (M_Location_ID > 0) {
mTab.setValue("M_Locator_ID", M_Location_ID);
}
return "";
}
}
return "";
}
public String setDescription(Properties ctx, int WindowNo, GridTab mTab, GridField mField, Object value, Object oldValue) {
MOrderLine ol = new MOrderLine(ctx, (int)value, null);

View File

@ -7,6 +7,9 @@ import org.adempiere.base.IColumnCallout;
import org.compiere.model.CalloutEngine;
import org.compiere.model.GridField;
import org.compiere.model.GridTab;
import org.compiere.model.MInventory;
import org.compiere.model.MProduct;
import org.compiere.model.Query;
import org.compiere.model.X_M_InventoryLine;
import org.compiere.util.DB;
@ -16,9 +19,50 @@ public class MID_CalloutInventoryLine extends CalloutEngine implements IColumnCa
public String start(Properties ctx, int WindowNo, GridTab mTab, GridField mField, Object value, Object oldValue) {
if (mField.getColumnName().equals("QtyEntered"))
return setInternalUseQty(ctx, WindowNo, mTab, mField, value, oldValue);
if( mField.getColumnName().equals("M_Product_ID"))
return setUOMByProduct(ctx, WindowNo, mTab, mField, value, oldValue);
if(mField.getColumnName().equals("C_Charge_ID")) {
if(value == null) {
// get parent Inventory
log.info("C_Charge_ID is null, trying to set C_Charge_ID from M_Inventory_ID");
int M_Inventory_ID = (int) mTab.getValue("M_Inventory_ID");
if(M_Inventory_ID > 0) {
MInventory inventory = new MInventory(ctx, M_Inventory_ID, null);
int DocumentType = inventory.getC_DocType_ID();
//
int chargeID = DB.getSQLValueEx(null, " SELECT cc.C_Charge_ID FROM C_ChargeType_DocType cctd JOIN C_Charge cc ON cc.C_ChargeType_ID = cctd.C_ChargeType_ID WHERE cctd.C_DocType_ID = ? LIMIT 1 ", DocumentType);
if (chargeID > 0) {
log.info("C_Charge_ID: " + chargeID);
mTab.setValue("C_Charge_ID", chargeID);
} else {
mTab.setValue("C_Charge_ID", null);
}
}
} else if(value instanceof BigDecimal) {
}
}
return null;
}
public String setUOMByProduct(Properties ctx, int WindowNo, GridTab mTab, GridField mField, Object value, Object oldValue) {
if (value == null) {
return "";
}
int M_Product_ID = (int) value;
if (M_Product_ID <= 0) {
return "";
}
MProduct product = MProduct.get(ctx, M_Product_ID);
int C_UOM_ID = product.getC_UOM_ID();
if (C_UOM_ID > 0) {
mTab.setValue("C_UOM_ID", C_UOM_ID);
} else {
mTab.setValue("C_UOM_ID", null);
}
return "";
}
public String setInternalUseQty(Properties ctx, int WindowNo, GridTab mTab, GridField mField, Object value, Object oldValue) {
if (value == null) {
return "";

View File

@ -5,7 +5,10 @@ import org.adempiere.base.IColumnCallout;
import org.compiere.model.CalloutEngine;
import org.compiere.model.GridField;
import org.compiere.model.GridTab;
import org.compiere.model.MLocator;
import org.compiere.model.MProduct;
import org.compiere.model.MProduction;
import org.compiere.model.Query;
import org.compiere.util.Env;
public class MID_CalloutProductionLine extends CalloutEngine implements IColumnCallout {
@ -22,10 +25,37 @@ public class MID_CalloutProductionLine extends CalloutEngine implements IColumnC
if(value==null) return "";
MProduct pro = new MProduct(ctx, (int)value, null);
mTab.setValue("C_UOM_ID", pro.getC_UOM_ID());
setLocator(ctx, WindowNo, mTab, mField, value, oldValue);
}
return null;
}
public String setLocator(Properties ctx, int WindowNo, GridTab mTab, GridField mField, Object value,
Object oldValue) {
int MProduct_ID = (int) mTab.getValue("M_Product_ID");
int ProductionID = (int) mTab.getValue("M_Production_ID");
MProduction production = new MProduction(ctx, ProductionID, null);
MProduct product = new MProduct(ctx, MProduct_ID, null);
boolean isService = (boolean) product.get_Value("IsService");
if (isService) {
if (production.getM_Locator_ID() > 0) {
MLocator locator = new MLocator(ctx, production.getM_Locator_ID(), null);
int Warehouse_ID = locator.getM_Warehouse_ID();
// get locator in same warehouse IsEnableNegative
int M_Location_ID = new Query(ctx, MLocator.Table_Name, "IsEnableNegative = 'Y' and M_Warehouse_ID=?",
null)
.setParameters(Warehouse_ID)
.setOnlyActiveRecords(true)
.firstId();
if (M_Location_ID > 0) {
mTab.setValue("M_Locator_ID", M_Location_ID);
}
return "";
}
}
return "";
}
public String setConversion(Properties ctx, int WindowNo, GridTab mTab, GridField mField, Object value,
Object oldValue) {
if (value == null || mTab.getValue("M_Product_ID") == null) {

View File

@ -607,13 +607,26 @@ public class MID_DocInvoice extends Doc
serviceAmt = serviceAmt.add(amt);
}
//
if (line.getM_Product_ID() != 0
&& line.getProduct().isService()) // otherwise Inv Matching
&& 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());
}
// lanjut nanti
// else if(line.getM_Product_ID() != 0
// && !line.getProduct().isStocked()
// && line.getProduct().get_ValueAsBoolean("AllowNegativeCosting")) {
// 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());
//
// }
}
}
// Set Locations

View File

@ -37,6 +37,7 @@ import balinusa.midsuit.model.MID_MRequisitionTrxLine;
import balinusa.midsuit.model.MID_MRequsitionTax;
import balinusa.midsuit.model.MID_PPO;
import balinusa.midsuit.model.MID_PPOLine;
import balinusa.midsuit.model.MID_Production;
import balinusa.midsuit.model.MID_UnrealizedRate;
import balinusa.midsuit.model.MID_UnrealizedRateLine;
import balinusa.midsuit.model.MWarehouse;
@ -100,6 +101,8 @@ public class MID_ModelFactory implements IModelFactory{
mapTableModels.put(MWarehouse.Table_Name, "balinusa.midsuit.model.MWarehouse");
mapTableModels.put(MID_MInvoice.Table_Name, "balinusa.midsuit.model.MID_MInvoice");
mapTableModels.put(MID_MOrderLine.Table_Name, "balinusa.midsuit.model.MID_MOrderLine");
mapTableModels.put(MID_Production.Table_Name, "balinusa.midsuit.model.MID_Production");
}
@Override

View File

@ -310,7 +310,7 @@ public class MID_MInventory extends MInventory {
}
}
}
log.info("Check 1: qtyDiff " + qtyDiff);
// If Quantity Count minus Quantity Book = Zero, then no change in Inventory
if (qtyDiff.signum() == 0)
continue;

View File

@ -0,0 +1,248 @@
package balinusa.midsuit.model;
import java.util.Properties;
import java.util.logging.Level;
import org.adempiere.exceptions.AdempiereException;
import org.adempiere.exceptions.NegativeInventoryDisallowedException;
import org.compiere.model.I_M_AttributeSet;
import org.compiere.model.I_M_Cost;
import org.compiere.model.I_M_ProductionPlan;
import org.compiere.model.MAccount;
import org.compiere.model.MAcctSchema;
import org.compiere.model.MAttributeSet;
import org.compiere.model.MAttributeSetInstance;
import org.compiere.model.MClient;
import org.compiere.model.MClientInfo;
import org.compiere.model.MCost;
import org.compiere.model.MCostElement;
import org.compiere.model.MLocator;
import org.compiere.model.MOrderLine;
import org.compiere.model.MOrg;
import org.compiere.model.MProduct;
import org.compiere.model.MProductCategory;
import org.compiere.model.MProduction;
import org.compiere.model.MProductionLine;
import org.compiere.model.MProductionLineMA;
import org.compiere.model.MProductionPlan;
import org.compiere.model.MRequisitionLine;
import org.compiere.model.MSequence;
import org.compiere.model.MStorageOnHand;
import org.compiere.model.MStorageReservation;
import org.compiere.model.MTable;
import org.compiere.model.MTransaction;
import org.compiere.model.MTree;
import org.compiere.model.MTreeNode;
import org.compiere.model.MTree_Base;
import org.compiere.model.MWarehouse;
import org.compiere.model.ModelValidationEngine;
import org.compiere.model.ModelValidator;
import org.compiere.model.Query;
import org.compiere.model.X_AD_Tree;
import org.compiere.model.X_M_CostElement;
import org.compiere.model.X_M_Product;
import org.compiere.process.DocAction;
import org.compiere.util.CLogger;
import org.compiere.util.DB;
import org.compiere.util.Env;
import org.compiere.util.Msg;
import org.compiere.util.Util;
import org.eevolution.model.MPPProductBOM;
import org.eevolution.model.MPPProductBOMLine;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Timestamp;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
public class MID_MStorageOnHand extends MStorageOnHand{
/**
*
*/
private static final long serialVersionUID = 1563913040977233621L;
private boolean isValueEmptyBeforeSave = false;
private static CLogger s_log = CLogger.getCLogger (MCost.class);
public MID_MStorageOnHand(Properties ctx, String M_StorageOnHand_UU, String trxName) {
super(ctx, M_StorageOnHand_UU, trxName);
// TODO Auto-generated constructor stub
}
/**
* @param ctx
* @param M_Locator_ID
* @param M_Product_ID
* @param M_AttributeSetInstance_ID
* @param trxName
* @deprecated
* @return MStorageOnHand
*/
@Deprecated
public static MID_MStorageOnHand get (Properties ctx, int M_Locator_ID,
int M_Product_ID, int M_AttributeSetInstance_ID, String trxName) {
return get (ctx, M_Locator_ID, M_Product_ID, M_AttributeSetInstance_ID, null, trxName);
}
/**
* NEW MStorageOnHand Constructor
* @param locator (parent) locator
* @param M_Product_ID product
* @param M_AttributeSetInstance_ID attribute
* @param dateMPolicy
*/
private MID_MStorageOnHand (MLocator locator, int M_Product_ID, int M_AttributeSetInstance_ID, Timestamp dateMPolicy)
{
super(locator.getCtx(), 0, locator.get_TrxName());
setClientOrg(locator);
setM_Locator_ID (locator.getM_Locator_ID());
setM_Product_ID (M_Product_ID);
setM_AttributeSetInstance_ID (M_AttributeSetInstance_ID);
dateMPolicy = Util.removeTime(dateMPolicy);
setDateMaterialPolicy(dateMPolicy);
} // MStorageOnHand
/**
* Create or Get On Hand Storage
* @param ctx context
* @param M_Locator_ID locator
* @param M_Product_ID product
* @param M_AttributeSetInstance_ID instance
* @param dateMPolicy optional DateMaterialPolicy filter
* @param trxName transaction
* @param forUpdate true to acquire DB lock with FOR UPDATE clause
* @return existing or new MStorageOnHand
*/
public static MID_MStorageOnHand getCreate (Properties ctx, int M_Locator_ID,
int M_Product_ID, int M_AttributeSetInstance_ID,Timestamp dateMPolicy, String trxName, boolean forUpdate)
{
return getCreate(ctx, M_Locator_ID, M_Product_ID, M_AttributeSetInstance_ID, dateMPolicy, trxName, forUpdate, 0);
}
/**
* Create or Get On Hand Storage
* @param ctx context
* @param M_Locator_ID locator
* @param M_Product_ID product
* @param M_AttributeSetInstance_ID instance
* @param dateMPolicy optional DateMaterialPolicy filter
* @param trxName transaction
* @param forUpdate true to acquire DB lock with FOR UPDATE clause
* @param timeout if forUpdate is true, value for query timeout (0 for no timeout).
* @return existing or new MStorageOnHand
*/
public static MID_MStorageOnHand getCreate (Properties ctx, int M_Locator_ID,
int M_Product_ID, int M_AttributeSetInstance_ID,Timestamp dateMPolicy, String trxName, boolean forUpdate, int timeout)
{
// Retrieve or create the base MStorageOnHand object
MStorageOnHand so = MStorageOnHand.getCreate(ctx, M_Locator_ID, M_Product_ID, M_AttributeSetInstance_ID, dateMPolicy, trxName, false);
// Ensure the returned object is of type MID_MStorageOnHand
if (so instanceof MID_MStorageOnHand) {
return (MID_MStorageOnHand) so;
} else {
// Create a new MID_MStorageOnHand instance if necessary
MID_MStorageOnHand mid_so = new MID_MStorageOnHand(ctx, so.getM_StorageOnHand_UU(), trxName);
mid_so.setM_Locator_ID(so.getM_Locator_ID());
mid_so.setM_Product_ID(so.getM_Product_ID());
mid_so.setM_AttributeSetInstance_ID(so.getM_AttributeSetInstance_ID());
mid_so.setDateMaterialPolicy(so.getDateMaterialPolicy());
mid_so.setQtyOnHand(so.getQtyOnHand());
return mid_so;
}
} // getCreate
/**
* Get On Hand Storage
* @param ctx context
* @param M_Locator_ID locator
* @param M_Product_ID product
* @param M_AttributeSetInstance_ID instance
* @param dateMPolicy optional DateMaterialPolicy filter
* @param trxName transaction
* @return existing MStorageOnHand or null
*/
public static MID_MStorageOnHand get (Properties ctx, int M_Locator_ID,
int M_Product_ID, int M_AttributeSetInstance_ID,Timestamp dateMPolicy, String trxName)
{
String sqlWhere = "M_Locator_ID=? AND M_Product_ID=? AND ";
if (M_AttributeSetInstance_ID == 0)
sqlWhere += "(M_AttributeSetInstance_ID=? OR M_AttributeSetInstance_ID IS NULL)";
else
sqlWhere += "M_AttributeSetInstance_ID=?";
if (dateMPolicy != null)
sqlWhere += " AND DateMaterialPolicy=trunc(cast(? as date))";
Query query = new Query(ctx, MID_MStorageOnHand.Table_Name, sqlWhere, trxName);
if (dateMPolicy != null)
query.setParameters(M_Locator_ID, M_Product_ID, M_AttributeSetInstance_ID, dateMPolicy);
else
query.setParameters(M_Locator_ID, M_Product_ID, M_AttributeSetInstance_ID);
MID_MStorageOnHand retValue = query.first();
if (retValue == null) {
if (s_log.isLoggable(Level.FINE)) s_log.fine("Not Found - M_Locator_ID=" + M_Locator_ID
+ ", M_Product_ID=" + M_Product_ID + ", M_AttributeSetInstance_ID=" + M_AttributeSetInstance_ID);
} else {
if (s_log.isLoggable(Level.FINE)) s_log.fine("M_Locator_ID=" + M_Locator_ID
+ ", M_Product_ID=" + M_Product_ID + ", M_AttributeSetInstance_ID=" + M_AttributeSetInstance_ID);
}
return retValue;
} // get
/**
* Create or Get On Hand Storage
* @param ctx context
* @param M_Locator_ID locator
* @param M_Product_ID product
* @param M_AttributeSetInstance_ID instance
* @param trxName transaction
* @return existing or new MStorageOnHand
*/
public static MID_MStorageOnHand getCreate(Properties ctx, int M_Locator_ID,
int M_Product_ID, int M_AttributeSetInstance_ID, Timestamp dateMPolicy, String trxName) {
// Retrieve or create the base MStorageOnHand object
MStorageOnHand so = MStorageOnHand.getCreate(ctx, M_Locator_ID, M_Product_ID, M_AttributeSetInstance_ID, dateMPolicy, trxName, false);
// Ensure the returned object is of type MID_MStorageOnHand
if (so instanceof MID_MStorageOnHand) {
return (MID_MStorageOnHand) so;
} else {
// Create a new MID_MStorageOnHand instance if necessary
MID_MStorageOnHand mid_so = new MID_MStorageOnHand(ctx, so.getM_StorageOnHand_UU(), trxName);
mid_so.setM_Locator_ID(so.getM_Locator_ID());
mid_so.setM_Product_ID(so.getM_Product_ID());
mid_so.setM_AttributeSetInstance_ID(so.getM_AttributeSetInstance_ID());
mid_so.setDateMaterialPolicy(so.getDateMaterialPolicy());
mid_so.setQtyOnHand(so.getQtyOnHand());
return mid_so;
}
}
/**
* Add addition to quantity on hand with direct SQL - not using cached value - solving IDEMPIERE-2629
* @param addition
*/
public void addQtyOnHand(BigDecimal addition) {
final String sql = "UPDATE M_StorageOnHand SET QtyOnHand=QtyOnHand+?, Updated=getDate(), UpdatedBy=? " +
"WHERE M_Product_ID=? AND M_Locator_ID=? AND M_AttributeSetInstance_ID=? AND DateMaterialPolicy=?";
DB.executeUpdateEx(sql,
new Object[] {addition, Env.getAD_User_ID(Env.getCtx()), getM_Product_ID(), getM_Locator_ID(), getM_AttributeSetInstance_ID(), getDateMaterialPolicy()},
get_TrxName());
load(get_TrxName());
if (getQtyOnHand().signum() == -1) {
MWarehouse wh = MWarehouse.get(Env.getCtx(), getM_Warehouse_ID());
if (wh.isDisallowNegativeInv()) {
MLocator loc = MLocator.get(Env.getCtx(), getM_Locator_ID());
// boolean isEnableNegative "Y" / "N" in M_Locator
boolean isEnableNegative = (boolean) loc.get_Value("IsEnableNegative");
log.info("addQtyOnHand - Locator=" + loc.get_ValueAsString("Value") + ", QtyOnHand=" + getQtyOnHand() + ", addition=" + addition
+ ", isEnableNegative=" + isEnableNegative + "Value of M_Locator.IsEnableNegative is " + loc.get_Value("IsEnableNegative"));
if (!isEnableNegative) {
throw new NegativeInventoryDisallowedException(getCtx(), getM_Warehouse_ID(), getM_Product_ID(), getM_AttributeSetInstance_ID(), getM_Locator_ID(),
getQtyOnHand().subtract(addition), addition.negate());
}
}
}
}
}

View File

@ -0,0 +1,173 @@
package balinusa.midsuit.model;
import java.util.Properties;
import java.util.logging.Level;
import org.adempiere.exceptions.AdempiereException;
import org.compiere.model.I_M_ProductionPlan;
import org.compiere.model.MAccount;
import org.compiere.model.MAcctSchema;
import org.compiere.model.MAttributeSetInstance;
import org.compiere.model.MClient;
import org.compiere.model.MClientInfo;
import org.compiere.model.MCost;
import org.compiere.model.MCostElement;
import org.compiere.model.MOrderLine;
import org.compiere.model.MOrg;
import org.compiere.model.MProduct;
import org.compiere.model.MProductCategory;
import org.compiere.model.MProduction;
import org.compiere.model.MProductionLine;
import org.compiere.model.MProductionPlan;
import org.compiere.model.MRequisitionLine;
import org.compiere.model.MSequence;
import org.compiere.model.MStorageOnHand;
import org.compiere.model.MStorageReservation;
import org.compiere.model.MTable;
import org.compiere.model.MTree;
import org.compiere.model.MTreeNode;
import org.compiere.model.MTree_Base;
import org.compiere.model.ModelValidationEngine;
import org.compiere.model.ModelValidator;
import org.compiere.model.Query;
import org.compiere.model.X_AD_Tree;
import org.compiere.model.X_M_CostElement;
import org.compiere.model.X_M_Product;
import org.compiere.process.DocAction;
import org.compiere.util.CLogger;
import org.compiere.util.DB;
import org.compiere.util.Env;
import org.compiere.util.Msg;
import org.compiere.util.Util;
import org.eevolution.model.MPPProductBOM;
import org.eevolution.model.MPPProductBOMLine;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Enumeration;
import java.util.List;
public class MID_Production extends MProduction{
/**
*
*/
private static final long serialVersionUID = 1563913040977233621L;
private boolean isValueEmptyBeforeSave = false;
private static CLogger s_log = CLogger.getCLogger (MCost.class);
public MID_Production(Properties ctx, int M_Production_ID, String trxName) {
super(ctx, M_Production_ID, trxName);
// TODO Auto-generated constructor stub
}
/**
* @param lines
* @return true if one of the production is an end product line (IsEndProduct=Y).
*/
private boolean isHaveEndProduct(MProductionLine[] lines) {
for(MProductionLine line : lines) {
if(line.isEndProduct())
return true;
}
return false;
}
/**
* Process production lines - create material transaction
* @param lines
* @return error message or empty string
*/
protected Object processLines(MProductionLine[] lines) {
StringBuilder errors = new StringBuilder();
for ( int i = 0; i<lines.length; i++) {
// convert MProductionLine to MID_ProductionLine
MID_ProductionLine productionline = new MID_ProductionLine(getCtx(), lines[i].getM_ProductionLine_ID(), get_TrxName());
String error = productionline.createTransactions(getMovementDate(), false);
if (!Util.isEmpty(error)) {
errors.append(error);
} else {
lines[i].setProcessed( true );
lines[i].saveEx(get_TrxName());
}
}
return errors.toString();
}
@Override
public String completeIt()
{
// Re-Check
if (!m_justPrepared)
{
String status = prepareIt();
m_justPrepared = false;
if (!DocAction.STATUS_InProgress.equals(status))
return status;
}
// Set the definite document number after completed (if needed)
setDefiniteDocumentNo();
m_processMsg = ModelValidationEngine.get().fireDocValidate(this, ModelValidator.TIMING_BEFORE_COMPLETE);
if (m_processMsg != null)
return DocAction.STATUS_Invalid;
StringBuilder errors = new StringBuilder();
int processed = 0;
if (!isUseProductionPlan()) {
MProductionLine[] lines = getLines();
//IDEMPIERE-3107 Check if End Product in Production Lines exist
if(!isHaveEndProduct(lines)) {
m_processMsg = "Production does not contain End Product";
return DocAction.STATUS_Invalid;
}
errors.append(processLines(lines));
if (errors.length() > 0) {
m_processMsg = errors.toString();
return DocAction.STATUS_Invalid;
}
processed = processed + lines.length;
} else {
Query planQuery = new Query(Env.getCtx(), I_M_ProductionPlan.Table_Name, "M_ProductionPlan.M_Production_ID=?", get_TrxName());
List<MProductionPlan> plans = planQuery.setParameters(getM_Production_ID()).list();
for(MProductionPlan plan : plans) {
MProductionLine[] lines = plan.getLines();
//IDEMPIERE-3107 Check if End Product in Production Lines exist
if(!isHaveEndProduct(lines)) {
m_processMsg = String.format("Production plan (line %1$d id %2$d) does not contain End Product", plan.getLine(), plan.get_ID());
return DocAction.STATUS_Invalid;
}
if (lines.length > 0) {
errors.append(processLines(lines));
if (errors.length() > 0) {
m_processMsg = errors.toString();
return DocAction.STATUS_Invalid;
}
processed = processed + lines.length;
}
plan.setProcessed(true);
plan.saveEx();
}
}
// User Validation
String valid = ModelValidationEngine.get().fireDocValidate(this, ModelValidator.TIMING_AFTER_COMPLETE);
if (valid != null)
{
m_processMsg = valid;
return DocAction.STATUS_Invalid;
}
setProcessed(true);
setDocAction(DOCACTION_Close);
return DocAction.STATUS_Completed;
}
}

View File

@ -0,0 +1,322 @@
package balinusa.midsuit.model;
import java.util.Properties;
import java.util.logging.Level;
import org.adempiere.exceptions.AdempiereException;
import org.compiere.model.I_M_AttributeSet;
import org.compiere.model.I_M_Cost;
import org.compiere.model.I_M_ProductionPlan;
import org.compiere.model.MAccount;
import org.compiere.model.MAcctSchema;
import org.compiere.model.MAttributeSet;
import org.compiere.model.MAttributeSetInstance;
import org.compiere.model.MClient;
import org.compiere.model.MClientInfo;
import org.compiere.model.MCost;
import org.compiere.model.MCostElement;
import org.compiere.model.MLocator;
import org.compiere.model.MOrderLine;
import org.compiere.model.MOrg;
import org.compiere.model.MProduct;
import org.compiere.model.MProductCategory;
import org.compiere.model.MProduction;
import org.compiere.model.MProductionLine;
import org.compiere.model.MProductionLineMA;
import org.compiere.model.MProductionPlan;
import org.compiere.model.MRequisitionLine;
import org.compiere.model.MSequence;
import org.compiere.model.MStorageOnHand;
import org.compiere.model.MStorageReservation;
import org.compiere.model.MTable;
import org.compiere.model.MTransaction;
import org.compiere.model.MTree;
import org.compiere.model.MTreeNode;
import org.compiere.model.MTree_Base;
import org.compiere.model.ModelValidationEngine;
import org.compiere.model.ModelValidator;
import org.compiere.model.Query;
import org.compiere.model.X_AD_Tree;
import org.compiere.model.X_M_CostElement;
import org.compiere.model.X_M_Product;
import org.compiere.process.DocAction;
import org.compiere.util.CLogger;
import org.compiere.util.DB;
import org.compiere.util.Env;
import org.compiere.util.Msg;
import org.compiere.util.Util;
import org.eevolution.model.MPPProductBOM;
import org.eevolution.model.MPPProductBOMLine;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Timestamp;
import java.util.Enumeration;
import java.util.List;
public class MID_ProductionLine extends MProductionLine{
/**
*
*/
private static final long serialVersionUID = 1563913040977233621L;
private boolean isValueEmptyBeforeSave = false;
private static CLogger s_log = CLogger.getCLogger (MCost.class);
public MID_ProductionLine(Properties ctx, int M_ProductionLine_ID, String trxName) {
super(ctx, M_ProductionLine_ID, trxName, (String[]) null);
// TODO Auto-generated constructor stub
}
/**
* Create material transactions
* @param date
* @param mustBeStocked true to verify on hand quantity
* @return "" for success, error message if failed
*/
public String createTransactions(Timestamp date, boolean mustBeStocked) {
int reversalId = getProductionReversalId ();
if (reversalId <= 0 )
{
// delete existing ASI records
int deleted = deleteMA();
if (log.isLoggable(Level.FINE))log.log(Level.FINE, "Deleted " + deleted + " attribute records ");
}
MProduct prod = new MProduct(getCtx(), getM_Product_ID(), get_TrxName());
if (log.isLoggable(Level.FINE))log.log(Level.FINE,"Loaded Product " + prod.toString());
if ( !prod.isStocked() || prod.getProductType().compareTo(MProduct.PRODUCTTYPE_Item ) != 0 ) {
// no need to do any movements
if (log.isLoggable(Level.FINE))log.log(Level.FINE, "Production Line " + getLine() + " does not require stock movement");
return "";
}
StringBuilder errorString = new StringBuilder();
MAttributeSetInstance asi = new MAttributeSetInstance(getCtx(), getM_AttributeSetInstance_ID(), get_TrxName());
I_M_AttributeSet attributeset = prod.getM_AttributeSet_ID() > 0 ? MAttributeSet.get(prod.getM_AttributeSet_ID()) : null;
boolean isAutoGenerateLot = false;
if (attributeset != null)
isAutoGenerateLot = attributeset.isAutoGenerateLot();
String asiString = asi.get_ID() > 0 ? asi.getDescription() : "";
if ( asiString == null )
asiString = "";
if (log.isLoggable(Level.FINEST)) log.log(Level.FINEST, "asi Description is: " + asiString);
// create transactions for finished goods
if ( getM_Product_ID() == getEndProduct_ID()) {
if (reversalId <= 0 && isAutoGenerateLot && getM_AttributeSetInstance_ID() == 0)
{
asi = MAttributeSetInstance.generateLot(getCtx(), (MProduct)getM_Product(), get_TrxName());
setM_AttributeSetInstance_ID(asi.getM_AttributeSetInstance_ID());
}
Timestamp dateMPolicy = date;
if(getM_AttributeSetInstance_ID()>0){
Timestamp t = MStorageOnHand.getDateMaterialPolicy(getM_Product_ID(), getM_AttributeSetInstance_ID(), getM_Locator_ID(), get_TrxName());
if (t != null)
dateMPolicy = t;
}
dateMPolicy = Util.removeTime(dateMPolicy);
//for reversal, keep the ma copy from original trx
if (reversalId <= 0 )
{
MProductionLineMA lineMA = new MProductionLineMA( this,
asi.get_ID(), getMovementQty(),dateMPolicy);
if ( !lineMA.save(get_TrxName()) ) {
log.log(Level.SEVERE, "Could not save MA for " + toString());
errorString.append("Could not save MA for " + toString() + "\n" );
}
}
MTransaction matTrx = new MTransaction (getCtx(), getAD_Org_ID(),
"P+",
getM_Locator_ID(), getM_Product_ID(), asi.get_ID(),
getMovementQty(), date, get_TrxName());
matTrx.setM_ProductionLine_ID(get_ID());
if ( !matTrx.save(get_TrxName()) ) {
log.log(Level.SEVERE, "Could not save transaction for " + toString());
errorString.append("Could not save transaction for " + toString() + "\n");
}
MID_MStorageOnHand storage = MID_MStorageOnHand.getCreate(getCtx(), getM_Locator_ID(),
getM_Product_ID(), asi.get_ID(),dateMPolicy, get_TrxName());
storage.addQtyOnHand(getMovementQty());
if (log.isLoggable(Level.FINE))log.log(Level.FINE, "Created finished goods line " + getLine());
return errorString.toString();
}
// create transactions and update stock used in production
MStorageOnHand[] storages = MStorageOnHand.getAll( getCtx(), getM_Product_ID(),
getM_Locator_ID(), get_TrxName(), false, 0);
MProductionLineMA lineMA = null;
MTransaction matTrx = null;
BigDecimal qtyToMove = getMovementQty().negate();
if (qtyToMove.signum() > 0) {
for (int sl = 0; sl < storages.length; sl++) {
BigDecimal lineQty = storages[sl].getQtyOnHand();
if (log.isLoggable(Level.FINE))log.log(Level.FINE, "QtyAvailable " + lineQty );
if (lineQty.signum() > 0)
{
if (lineQty.compareTo(qtyToMove ) > 0)
lineQty = qtyToMove;
MAttributeSetInstance slASI = storages[sl].getM_AttributeSetInstance_ID() > 0 ? new MAttributeSetInstance(getCtx(),
storages[sl].getM_AttributeSetInstance_ID(),get_TrxName()) : null;
String slASIString = slASI != null ? slASI.getDescription() : "";
if (slASIString == null)
slASIString = "";
if (log.isLoggable(Level.FINEST))log.log(Level.FINEST,"slASI-Description =" + slASIString);
if (asi.getM_AttributeSet_ID() == 0 || slASIString.equals(asiString))
//storage matches specified ASI or is a costing asi (inc. 0)
// This process will move negative stock on hand quantities
{
lineMA = MProductionLineMA.get(this,storages[sl].getM_AttributeSetInstance_ID(),storages[sl].getDateMaterialPolicy());
lineMA.setMovementQty(lineMA.getMovementQty().add(lineQty.negate()));
if ( !lineMA.save(get_TrxName()) ) {
log.log(Level.SEVERE, "Could not save MA for " + toString());
errorString.append("Could not save MA for " + toString() + "\n" );
} else {
if (log.isLoggable(Level.FINE))log.log(Level.FINE, "Saved MA for " + toString());
}
matTrx = new MTransaction (getCtx(), getAD_Org_ID(),
"P-",
getM_Locator_ID(), getM_Product_ID(), lineMA.getM_AttributeSetInstance_ID(),
lineQty.negate(), date, get_TrxName());
matTrx.setM_ProductionLine_ID(get_ID());
if ( !matTrx.save(get_TrxName()) ) {
log.log(Level.SEVERE, "Could not save transaction for " + toString());
errorString.append("Could not save transaction for " + toString() + "\n");
} else {
if (log.isLoggable(Level.FINE))log.log(Level.FINE, "Saved transaction for " + toString());
}
DB.getDatabase().forUpdate(storages[sl], 120);
storages[sl].addQtyOnHand(lineQty.negate());
qtyToMove = qtyToMove.subtract(lineQty);
if (log.isLoggable(Level.FINE))log.log(Level.FINE, getLine() + " Qty moved = " + lineQty + ", Remaining = " + qtyToMove );
}
}
if ( qtyToMove.signum() == 0 )
break;
} // for available storages
}
else if (qtyToMove.signum() < 0 )
{
MClientInfo m_clientInfo = MClientInfo.get(getCtx(), getAD_Client_ID(), get_TrxName());
MAcctSchema acctSchema = new MAcctSchema(getCtx(), m_clientInfo.getC_AcctSchema1_ID(), get_TrxName());
if (asi.get_ID() == 0 && MAcctSchema.COSTINGLEVEL_BatchLot.equals(prod.getCostingLevel(acctSchema)) )
{
//add quantity to last attributesetinstance
String sqlWhere = "M_Product_ID=? AND M_Locator_ID=? AND M_AttributeSetInstance_ID > 0 ";
MStorageOnHand storage = new Query(getCtx(), MStorageOnHand.Table_Name, sqlWhere, get_TrxName())
.setParameters(getM_Product_ID(), getM_Locator_ID())
.setOrderBy(MStorageOnHand.COLUMNNAME_DateMaterialPolicy+" DESC,"+ MStorageOnHand.COLUMNNAME_M_AttributeSetInstance_ID +" DESC")
.first();
if (storage != null)
{
setM_AttributeSetInstance_ID(storage.getM_AttributeSetInstance_ID());
asi = new MAttributeSetInstance(getCtx(), storage.getM_AttributeSetInstance_ID(), get_TrxName());
asiString = asi.get_ID() > 0 ? asi.getDescription() : "";
}
else
{
String costingMethod = prod.getCostingMethod(acctSchema);
StringBuilder localWhereClause = new StringBuilder("M_Product_ID =?" )
.append(" AND C_AcctSchema_ID=?")
.append(" AND ce.CostingMethod = ? ")
.append(" AND CurrentCostPrice <> 0 ");
MCost cost = new Query(getCtx(),I_M_Cost.Table_Name,localWhereClause.toString(),get_TrxName())
.setParameters(getM_Product_ID(), acctSchema.get_ID(), costingMethod)
.addJoinClause(" INNER JOIN M_CostElement ce ON (M_Cost.M_CostElement_ID =ce.M_CostElement_ID ) ")
.setOrderBy("Updated DESC")
.first();
if (cost != null)
{
setM_AttributeSetInstance_ID(cost.getM_AttributeSetInstance_ID());
asi = new MAttributeSetInstance(getCtx(), cost.getM_AttributeSetInstance_ID(), get_TrxName());
asiString = asi.getDescription();
}
else
{
log.log(Level.SEVERE, "Cannot retrieve cost of Product r " + prod.toString());
errorString.append( "Cannot retrieve cost of Product " +prod.toString() ) ;
}
}
}
}
if ( !( qtyToMove.signum() == 0) ) {
if (mustBeStocked && qtyToMove.signum() > 0)
{
MLocator loc = new MLocator(getCtx(), getM_Locator_ID(), get_TrxName());
errorString.append( "Insufficient qty on hand of " + prod.toString() + " at "
+ loc.toString() + "\n");
}
else
{
MID_MStorageOnHand storage = MID_MStorageOnHand.getCreate(Env.getCtx(), getM_Locator_ID(), getM_Product_ID(),
asi.get_ID(), date, get_TrxName(), true);
BigDecimal lineQty = qtyToMove;
MAttributeSetInstance slASI = storage.getM_AttributeSetInstance_ID() > 0
? new MAttributeSetInstance(getCtx(), storage.getM_AttributeSetInstance_ID(),get_TrxName()) : null;
String slASIString = slASI != null ? slASI.getDescription() : "";
if (slASIString == null)
slASIString = "";
if (log.isLoggable(Level.FINEST))log.log(Level.FINEST,"slASI-Description =" + slASIString);
if (asi.getM_AttributeSet_ID() == 0 || slASIString.compareTo(asiString) == 0)
//storage matches specified ASI or is a costing asi (inc. 0)
// This process will move negative stock on hand quantities
{
lineMA = MProductionLineMA.get(this,storage.getM_AttributeSetInstance_ID(),storage.getDateMaterialPolicy());
lineMA.setMovementQty(lineMA.getMovementQty().add(lineQty.negate()));
if ( !lineMA.save(get_TrxName()) ) {
log.log(Level.SEVERE, "Could not save MA for " + toString());
errorString.append("Could not save MA for " + toString() + "\n" );
} else {
if (log.isLoggable(Level.FINE))log.log(Level.FINE, "Saved MA for " + toString());
}
matTrx = new MTransaction (getCtx(), getAD_Org_ID(),
"P-",
getM_Locator_ID(), getM_Product_ID(), asi.get_ID(),
lineQty.negate(), date, get_TrxName());
matTrx.setM_ProductionLine_ID(get_ID());
if ( !matTrx.save(get_TrxName()) ) {
log.log(Level.SEVERE, "Could not save transaction for " + toString());
errorString.append("Could not save transaction for " + toString() + "\n");
} else {
if (log.isLoggable(Level.FINE))log.log(Level.FINE, "Saved transaction for " + toString());
}
storage.addQtyOnHand(lineQty.negate());
qtyToMove = qtyToMove.subtract(lineQty);
if (log.isLoggable(Level.FINE))log.log(Level.FINE, getLine() + " Qty moved = " + lineQty + ", Remaining = " + qtyToMove );
} else {
errorString.append( "Storage doesn't match ASI " + prod.toString() + " / "
+ slASIString + " vs. " + asiString + "\n");
}
}
}
return errorString.toString();
}
}

99
dev.sql Normal file
View File

@ -0,0 +1,99 @@
select
inv.ad_client_id,
inv.ad_org_id,
bp.name as toko,
inv.documentdate as tanggal,
dt.name as doctype,
row_number() over() as no,
case
when invl.c_charge_id is null then prod.name
else ch.name
end as product,
inv.description as keterangan,
inv.documentno,
inv.dateinvoiced,
pt.name as payterm,
case
when inv.paymentrule = 'P' then 'On Credit'
else inv.paymentrule
end as payrule,
co.documentno as pono,
co.dateordered as podate,
io.documentno as shipno,
asi.lot,
inv.totallines,
uom.uomsymbol as uom,
cur.iso_code as cur,
case
when inv.docstatus = 'CO' then 'Completed'
when inv.docstatus = 'VO' then 'Void'
when inv.docstatus = 'DR' then 'Draft'
when inv.docstatus = 'RE' then 'Reversed'
when inv.docstatus = 'IN' then 'Invalid'
when inv.docstatus = 'CL' then 'Closed'
end as docstatus,
co.grandtotal,
(
select
sum(co.grandtotal) as grandtotalorder
from
(
select
co.c_order_id,
co.grandtotal
from
c_order co
left join c_orderline col on col.c_order_id = co.c_order_id
left join c_invoiceline cin on cin.c_orderline_id = col.c_orderline_id
where
cin.c_invoice_id = 1000231
group by
co.c_order_id,
co.grandtotal
) co
) as grandtotalorder,
(
select
address1 || ' ' || COALESCE(address2, '')
from
c_location
where
c_location_id = bloc.c_location_id
) as alamat,
-- (
-- select
-- sum(taxamt)
-- from
-- c_invoiceline
-- where
-- c_invoice_id = 1000231
-- ) as pajak,
coalesce(org.description, ' ') as orgdesc,
coalesce(lf.address1, ' ') as o1,
coalesce(lf.address2, ' ') as o2,
coalesce(lf.address3, ' ') as o3,
coalesce(lf.address4, ' ') as o4,
coalesce(lf.city, ' ') as city,
coalesce(f.phone, ' ') as phone,
coalesce(f.fax, ' ') as fax
from
c_invoice inv
left join c_invoiceline invl on invl.c_invoice_id = inv.c_invoice_id
left join c_orderline col on col.c_orderline_id = invl.c_orderline_id
left join c_order co on co.c_order_id = col.c_order_id
left join c_bpartner bp on bp.c_bpartner_id = inv.c_bpartner_id
left join c_paymentterm pt on pt.c_paymentterm_id = inv.c_paymentterm_id
left join M_AttributeSetInstance asi on asi.M_AttributeSetInstance_id = invl.M_AttributeSetInstance_id
left join m_product prod on prod.m_product_id = invl.m_product_id
left join c_doctype dt on dt.c_doctype_id = inv.c_doctypetarget_id
left join c_bpartner_location bloc on bloc.c_bpartner_location_id = inv.c_bpartner_location_id
left join c_uom uom on uom.c_uom_id = invl.c_uom_id
left join c_currency cur on cur.c_currency_id = inv.c_currency_id
left join c_charge ch on ch.c_charge_id = invl.c_charge_id
left join c_chargetype cht on cht.c_chargetype_id = ch.c_chargetype_id
left join ad_org org on org.ad_org_id = inv.ad_org_id
left join ad_orginfo f on f.ad_org_id = org.ad_org_id
left join c_location lf on lf.c_location_id = f.c_location_id
left join m_inout io on io.m_inout_id = inv.m_inout_id
where
inv.c_invoice_id = 1000231