/******************************************************************************
 * Product: Compiere ERP & CRM Smart Business Solution                        *
 * Copyright (C) 1999-2007 ComPiere, Inc. All Rights Reserved.                *
 * This program is free software, you can redistribute it and/or modify it    *
 * under the terms version 2 of the GNU General Public License as published   *
 * by the Free Software Foundation. This program is distributed in the hope   *
 * that it will be useful, but WITHOUT ANY WARRANTY, without even the implied *
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.           *
 * See the GNU General Public License for more details.                       *
 * You should have received a copy of the GNU General Public License along    *
 * with this program, if not, write to the Free Software Foundation, Inc.,    *
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.                     *
 * For the text or an alternative of this public license, you may reach us    *
 * ComPiere, Inc., 3600 Bridge Parkway #102, Redwood City, CA 94065, USA      *
 * or via info@compiere.org or http://www.compiere.org/license.html           *
 *****************************************************************************/
package org.compiere.framework;

import java.io.*;
import java.math.*;
import java.sql.*;
import java.util.*;
import java.util.logging.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import org.compiere.*;
import org.compiere.api.*;
import org.compiere.common.*;
import org.compiere.model.*;
import org.compiere.util.*;
import org.w3c.dom.*;

/**
 *  Persistent Object.
 *  Superclass for actual implementations
 *
 *  @author Jorg Janke
 *  @version $Id: PO.java,v 1.1 2008/01/16 07:34:32 jrmt Exp $
 */
public abstract class PO 
	implements Serializable, Comparator<PO>, Evaluatee
{
	/**
	 * 	Set Document Value Workflow Manager
	 *	@param docWFMgr mgr
	 */
	public static void setDocWorkflowMgr (DocWorkflowMgr docWFMgr)
	{
		s_docWFMgr = docWFMgr;
		s_log.config (s_docWFMgr.toString());
	}	//	setDocWorkflowMgr
	
	/**
	 * 	Convert String To int.
	 * 	Null throws an error
	 *	@param stringValue value
	 *	@return int
	 */
	public static int convertToInt (String stringValue)
	{
		if (stringValue == null || stringValue.length() == 0)
			throw new IllegalArgumentException("Value is NULL");
		return Integer.parseInt(stringValue);
	}	//	convertToInt
	
	/**
	 * 	Convert String To Timestamp.
	 * 	Null is passed through
	 *	@param stringValue value
	 *	@return Timestamp
	 */
	public static Timestamp convertToTimestamp (String stringValue)
	{
		if (stringValue == null || stringValue.length() == 0)
			return null;
		//	long Time format
		try
		{
			long time = Long.parseLong(stringValue);
			return new Timestamp(time);
		}
		catch (Exception e)
		{
		}
		//	JDBC yyyy-mm-dd hh:mm:ss.fffffffff
		try
		{
			return Timestamp.valueOf(stringValue);
		}
		catch (Exception e)
		{
		}
		throw new IllegalArgumentException("Cannot convert to Timestamp: " + stringValue);
	}	//	convertToTimestamp
	
	/**
	 * 	Convert String To BigDecimal.
	 * 	Null is passed through
	 *	@param stringValue value
	 *	@return BigDecimal
	 */
	public static BigDecimal convertToBigDecimal (String stringValue)
	{
		if (stringValue == null || stringValue.length() == 0)
			return null;
		return new BigDecimal(stringValue);
	}	//	convertToBigDecimal
	
	/**
	 * 	Convert String To boolean.
	 * 	'Y','true' is true - rest (incl null) is false
	 *	@param stringValue value
	 *	@return boolean
	 */
	public static boolean convertToBoolean (String stringValue)
	{
		return stringValue != null 
			&& ("Y".equals(stringValue) 
				|| "true".equals(stringValue)		//	boolean
				|| "on".equals (stringValue));		//	web
	}	//	convertToBoolean

	
	/** Document Value Workflow Manager		*/
	private static DocWorkflowMgr		s_docWFMgr = null;
	
	/** User Maintained Entity Type				*/
	static protected final String ENTITYTYPE_UserMaintained = "U";
	/** Dictionary Maintained Entity Type		*/
	static protected final String ENTITYTYPE_Dictionary = "D";
	
	/**************************************************************************
	 *  Create New Persisent Object
	 *  @param ctx context
	 */
	public PO (Ctx ctx)
	{
		this (ctx, 0, null, null);
	}   //  PO

	/**
	 *  Create & Load existing Persistent Object
	 *  @param ID  The unique ID of the object
	 *  @param ctx context
	 *  @param trxName transaction name
	 */
	public PO (Ctx ctx, int ID, String trxName)
	{
		this (ctx, ID, null, trxName);
	}   //  PO
	
	/**
	 *  Create & Load existing Persistent Object.
	 *  @param ctx context
	 *  @param rs optional - load from current result set position (no navigation, not closed)
	 *  	if null, a new record is created.
	 *  @param trxName transaction name
	 */
	public PO (Ctx ctx, ResultSet rs, String trxName)
	{
		this (ctx, 0, rs, trxName);
	}	//	PO

	/**
	 *  Create & Load existing Persistent Object.
	 *  <pre>
	 *  You load
	 * 		- an existing single key record with 	new PO (ctx, Record_ID)
	 * 			or									new PO (ctx, Record_ID, trxName)
	 * 			or									new PO (ctx, rs, get_TrxName())
	 * 		- a new single key record with			new PO (ctx, 0)
	 * 		- an existing multi key record with		new PO (ctx, rs, get_TrxName())
	 * 		- a new multi key record with			new PO (ctx, null)
	 *  The ID for new single key records is created automatically,
	 *  you need to set the IDs for multi-key records explicitly.
	 *	</pre>
	 *  @param ctx context
	 *  @param ID the ID if 0, the record defaults are applied - ignored if re exists
	 *  @param trxName transaction name
	 *  @param rs optional - load from current result set position (no navigation, not closed)
	 */
	private PO (Ctx ctx, int ID, ResultSet rs, String trxName)
	{
		if (ctx == null)
			throw new IllegalArgumentException ("No Context");
		p_ctx = ctx;
		p_info = initPO(ctx);
		if (p_info == null || p_info.getTableName() == null)
			throw new IllegalArgumentException ("Invalid PO Info - " + p_info);
		//
		int size = p_info.getColumnCount();
		m_oldValues = new Object[size];
		m_newValues = new Object[size];
		m_trxName = trxName;
		if (rs != null)
			load(rs);		//	will not have virtual columns
		else
			load(ID, trxName);
	}   //  PO

	/**
	 * 	Create New PO by Copying existing (key not copied).
	 * 	@param ctx context
	 * 	@param source source object
	 * 	@param AD_Client_ID client
	 * 	@param AD_Org_ID org
	 */
	public PO (Ctx ctx, PO source, int AD_Client_ID, int AD_Org_ID)
	{
		this (ctx, 0, null, null);	//	create new
		//
		if (source != null)
			copyValues (source, this);
		setAD_Client_ID(AD_Client_ID);
		setAD_Org_ID(AD_Org_ID);
	}	//	PO

	/**
	 * 	VO Constructor
	 *	@param ctx context
	 *	@param vo value object
	 */
	public PO (Ctx ctx, VO vo)
	{
		this (ctx, 0, null);
		load(vo);
	}	//	PO

	/**	Logger							*/
	protected transient CLogger	log = CLogger.getCLogger (getClass());
	/** Static Logger					*/
	private static CLogger		s_log = CLogger.getCLogger (PO.class);

	/** Context                 */
	protected Ctx				p_ctx;
	/** Model Info              */
	protected volatile POInfo	p_info = null;

	/** Original Values         */
	private Object[]    		m_oldValues = null;
	/** New Values              */
	private Object[]    		m_newValues = null;

	/** Record_IDs          		*/
	private Object[]       		m_IDs = new Object[] {I_ZERO};
	/** Key Columns					*/
	private String[]         	m_KeyColumns = null;
	/** Create New for Multi Key 	*/
	private boolean				m_createNew = false;
	/**	Attachment with entriess	*/
	private MAttachment			m_attachment = null;
	/**	Deleted ID					*/
	private int					m_idOld = 0;
	/** Custom Columns 				*/
	private HashMap<String,String>	m_custom = null;
	
	/** Zero Integer				*/
	protected static final Integer I_ZERO = new Integer(0);
	/** Accounting Columns			*/
	private ArrayList <String>	s_acctColumns = null;
	/** Change VO					*/
	protected ChangeVO			p_changeVO = null;

	/** Access Level S__ 100	4	System info			*/
	public static final int ACCESSLEVEL_SYSTEM = 4;
	/** Access Level _C_ 010	2	Client info			*/
	public static final int ACCESSLEVEL_CLIENT = 2;
	/** Access Level __O 001	1	Organization info	*/
	public static final int ACCESSLEVEL_ORG = 1;
	/**	Access Level SCO 111	7	System shared info	*/
	public static final int ACCESSLEVEL_ALL = 7;
	/** Access Level SC_ 110	6	System/Client info	*/
	public static final int ACCESSLEVEL_SYSTEMCLIENT = 6;
	/** Access Level _CO 011	3	Client shared info	*/
	public static final int ACCESSLEVEL_CLIENTORG = 3;

	
	/**
	 *  Initialize and return PO_Info
	 *  @param ctx context
	 *  @return POInfo
	 */
	abstract protected POInfo initPO (Ctx ctx);

	/**
	 * 	Get Table Access Level
	 *	@return Access Level
	 */
	abstract protected int get_AccessLevel();
	
	/**
	 *  String info
	 *  @return String info
	 */
	public String toString()
	{
		StringBuffer sb = new StringBuffer("PO[")
			.append(get_WhereClause(true)).append("]");
		return sb.toString();
	}	//  toString

	/**
	 *  Extended String info
	 *  @return String info
	 */
	public String toStringX()
	{
		return toString();
	}	//  toString
	
	/**
	 * 	Equals based on ID
	 * 	@param cmp comparator
	 * 	@return true if ID the same
	 */
	public boolean equals (Object cmp)
	{
		if (cmp == null)
			return false;
		if (!(cmp instanceof PO))
			return false;
		if (cmp.getClass().equals(this.getClass()))
		{
			if (m_IDs.length < 2)
				return ((PO)cmp).get_ID() == get_ID();
			//	Multi-Key Compare
			String keyColumns[] = get_KeyColumns();
			for (int i = 0; i < keyColumns.length; i++)
			{
				String keyColumn = keyColumns[i];
				Object o1 = get_Value(keyColumn);
				Object o2 = ((PO)cmp).get_Value(keyColumn);
				if (!Util.isEqual(o1, o2))
					return false;
			}
			return true;
		}
		return super.equals(cmp);
	}	//	equals

	/**
	 * 	Used for HashTable
	 */
    public int hashCode()
    {
    	StringBuffer keyValue = new StringBuffer().append(get_Table_ID()).append(7);
    	for (int i = 0; i < m_IDs.length; i++)
    		keyValue.append(m_IDs[i]);
    	try
    	{
    		return Integer.parseInt(keyValue.toString());
    	}
    	catch (Exception e)
    	{
    	}
    	return super.hashCode();
    }	//	hashCode


	/**
	 * 	Compare based on DocumentNo, Value, Name, Description
	 *	@param o1 Object 1
	 *	@param o2 Object 2
	 *	@return -1 if o1 < o2
	 */
	public int compare (PO o1, PO o2)
	{
		if (o1 == null)
			return -1;
		else if (o2 == null)
			return 1;
		//	same class
		if (o1.getClass().equals(o2.getClass()))
		{
			int index = get_ColumnIndex("DocumentNo");
			if (index == -1)
				index = get_ColumnIndex("Value");
			if (index == -1)
				index = get_ColumnIndex("Name");
			if (index == -1)
				index = get_ColumnIndex("Description");
			if (index != -1)
			{
				PO po1 = (PO)o1;
				Object comp1 = po1.get_Value(index);
				PO po2 = (PO)o2;
				Object comp2 = po2.get_Value(index);
				if (comp1 == null)
					return -1;
				else if (comp2 == null)
					return 1;
				return comp1.toString().compareTo(comp2.toString());
			}
		}
		return o1.toString().compareTo(o2.toString());
	}	//	compare

	/**
	 *  Get TableName.
	 *  @return table name
	 */
	public String get_TableName()
	{
		return p_info.getTableName();
	}   //  get_TableName

	/**
	 *  Get Key Columns.
	 *  @return table name
	 */
	public String[] get_KeyColumns()
	{
		return m_KeyColumns;
	}   //  get_KeyColumns

	/**
	 *  Is Column a Key Column?
	 *  @return true if key column
	 */
	public boolean is_KeyColumn(String columnName)
	{
		for (int i = 0; i < m_KeyColumns.length; i++)
        {
	        if (m_KeyColumns.equals(columnName))
	        	return true;
        }
		return false;
	}   //  ist_KeyColumn

	/**
	 *  Get AD Table ID.
	 *  @return AD_Table_ID
	 */
	public int get_Table_ID()
	{
		return p_info.getAD_Table_ID();
	}   //  get_TableID
	
	/**
	 *  Return Single Key Record ID
	 *  @return ID or 0
	 */
	public int get_ID()
	{
		Object oo = m_IDs[0];
		if (oo != null && oo instanceof Integer)
			return ((Integer)oo).intValue();
		return 0;
	}   //  getID

	/**
	 *  Return Deleted Single Key Record ID
	 *  @return ID or 0
	 */
	public int get_IDOld()
	{
		return m_idOld;
	}   //  getID
	
	/**
	 * 	Get Context
	 * 	@return context
	 */
	public Ctx getCtx()
	{
		return p_ctx;
	}	//	getCtx

	/**
	 * 	Get Logger
	 *	@return logger
	 */
	public CLogger get_Logger()
	{
		return log;
	}	//	getLogger
	
	/**************************************************************************
	 *  Get Value
	 *  @param index index
	 *  @return value
	 */
	public final Object get_Value (int index)
	{
		if (index < 0 || index >= get_ColumnCount())
		{
			log.log(Level.WARNING, "Index invalid - " + index);
			return null;
		}
		if (m_newValues[index] != null)
		{
			if (m_newValues[index].equals(Null.NULL))
				return null;
			return m_newValues[index];
		}
		return m_oldValues[index];
	}   //  get_Value

	/**
	 *  Get Value as int
	 *  @param index index
	 *  @return int value or 0
	 */
	protected int get_ValueAsInt (int index)
	{
		Object value = get_Value(index);
		if (value == null)
			return 0;
		if (value instanceof Integer)
			return ((Integer)value).intValue();
		try
		{
			return Integer.parseInt(value.toString());
		}
		catch (NumberFormatException ex)
		{
			log.warning(p_info.getColumnName(index) + " - " + ex.getMessage());
			return 0;
		}
	}   //  get_ValueAsInt

	/**
	 *  Get Value
	 *  @param columnName column name
	 *  @return value or null
	 */
	public final Object get_Value (String columnName)
	{
		int index = get_ColumnIndex(columnName);
		if (index < 0)
		{
			log.log(Level.WARNING, "Column not found - " + columnName);
			Trace.printStack();
			return null;
		}
		return get_Value (index);
	}   //  get_Value

	/**
	 * 	Get Column Value
	 *	@param columnName name
	 *	@return value or ""
	 */
	public String get_ValueAsString (String columnName)
	{
		Object value = get_Value(columnName);
		if (value == null)
			return "";
		if (value instanceof Timestamp)
		{
			long time = ((Timestamp)value).getTime();
			return String.valueOf(time);
		}
		else if(value instanceof Boolean) {
		if(((Boolean)value).booleanValue())
			return "Y";
		else
			return "N";
		}
		return value.toString();
	}	//	get_ValueAsString

	/**
	 * 	Get Column Values as String
	 *	@param variableNames column names
	 *	@return string array
	 */
	public String[] get_ValuesAsString (String[] columnNames)
	{
		String[] retValue = new String[columnNames.length];
		for (int i = 0; i < columnNames.length; i++)
			retValue[i] = get_ValueAsString(columnNames[i]);
		return retValue;
	}	//	get_ValuesAsString

	/**
	 *  Get Value of Column
	 *  @param AD_Column_ID column
	 *  @return value or null
	 */
	public final Object get_ValueOfColumn (int AD_Column_ID)
	{
		int index = p_info.getColumnIndex(AD_Column_ID);
		if (index < 0)
		{
			log.log(Level.WARNING, "Not found - AD_Column_ID=" + AD_Column_ID);
			return null;
		}
		return get_Value (index);
	}   //  get_ValueOfColumn

	/**
	 *  Get Old Value
	 *  @param index index
	 *  @return value
	 */
	public final Object get_ValueOld (int index)
	{
		if (index < 0 || index >= get_ColumnCount())
		{
			log.log(Level.WARNING, "Index invalid - " + index);
			return null;
		}
		return m_oldValues[index];
	}   //  get_ValueOld
	
	/**
	 *  Get Old Value
	 *  @param columnName column name
	 *  @return value or null
	 */
	public final Object get_ValueOld (String columnName)
	{
		int index = get_ColumnIndex(columnName);
		if (index < 0)
		{
			log.log(Level.WARNING, "Column not found - " + columnName);
			return null;
		}
		return get_ValueOld (index);
	}   //  get_ValueOld
	
	/**
	 *  Get Old Value as int
	 *  @param columnName column name
	 *  @return int value or 0
	 */
	protected int get_ValueOldAsInt (String columnName)
	{
		Object value = get_ValueOld(columnName);
		if (value == null)
			return 0;
		if (value instanceof Integer)
			return ((Integer)value).intValue();
		try
		{
			return Integer.parseInt(value.toString());
		}
		catch (NumberFormatException ex)
		{
			log.warning(columnName + " - " + ex.getMessage());
			return 0;
		}
	}   //  get_ValueOldAsInt

	/**
	 *  Is Value Changed
	 *  @param index index
	 *  @return true if changed
	 */
	public final boolean is_ValueChanged (int index)
	{
		if (index < 0 || index >= get_ColumnCount())
		{
			log.log(Level.WARNING, "Index invalid - " + index);
			return false;
		}
		if (m_newValues[index] == null)
			return false;
		return !m_newValues[index].equals(m_oldValues[index]);
	}   //  is_ValueChanged
	
	/**
	 *  Is Value Changed
	 *  @param columnName column name
	 *  @return true if changed
	 */
	public final boolean is_ValueChanged (String columnName)
	{
		int index = get_ColumnIndex(columnName);
		if (index < 0)
		{
			log.log(Level.WARNING, "Column not found - " + columnName);
			return false;
		}
		return is_ValueChanged (index);
	}   //  is_ValueChanged
	
	/**
	 *  Return new - old.
	 * 	- New Value if Old Valus is null
	 * 	- New Value - Old Value if Number 
	 * 	- otherwise null
	 *  @param index index
	 *  @return new - old or null if not appropiate or not changed
	 */
	public final Object get_ValueDifference (int index)
	{
		if (index < 0 || index >= get_ColumnCount())
		{
			log.log(Level.WARNING, "Index invalid - " + index);
			return null;
		}
		Object nValue = m_newValues[index];
		//	No new Value or NULL
		if (nValue == null || nValue == Null.NULL)
			return null;
		//
		Object oValue = m_oldValues[index];
		if (oValue == null || oValue == Null.NULL)
			return nValue;
		if (nValue instanceof BigDecimal)
		{
			BigDecimal obd = (BigDecimal)oValue;
			return ((BigDecimal)nValue).subtract(obd);
		}
		else if (nValue instanceof Integer)
		{
			int result = ((Integer)nValue).intValue();
			result -= ((Integer)oValue).intValue();
			return new Integer(result);
		}
		//
		log.warning("Invalid type - New=" + nValue);
		return null;
	}   //  get_ValueDifference
	
	/**
	 *  Return new - old.
	 * 	- New Value if Old Valus is null
	 * 	- New Value - Old Value if Number 
	 * 	- otherwise null
	 *  @param columnName column name
	 *  @return new - old or null if not appropiate or not changed
	 */
	public final Object get_ValueDifference (String columnName)
	{
		int index = get_ColumnIndex(columnName);
		if (index < 0)
		{
			log.log(Level.WARNING, "Column not found - " + columnName);
			return null;
		}
		return get_ValueDifference (index);
	}   //  get_ValueDifference
	
	
	/**************************************************************************
	 *  Set Value
	 *  @param ColumnName column name
	 *  @param value value
	 *  @return true if value set
	 */
	public final boolean set_Value (String ColumnName, Object value)
	{
		if (value instanceof String 
			&& ColumnName.equals("WhereClause")
			&& value.toString().toUpperCase().indexOf("=NULL") != -1)
			log.warning("Invalid Null Value - " + ColumnName + "=" + value);
		
		int index = get_ColumnIndex(ColumnName);
		if (index < 0)
		{
			log.log(Level.SEVERE, "Column not found - " + ColumnName);
			return false;
		}
		if (ColumnName.endsWith("_ID") && value instanceof String )
		{
			log.warning("String converted to Integer for " + ColumnName + "=" + value);
			value = Integer.parseInt((String)value);
		}
			
		return set_Value (index, value);
	}   //  setValue

	/**
	 *  Set Encrypted Value
	 *  @param ColumnName column name
	 *  @param value value
	 *  @return true if value set
	 */
	protected final boolean set_ValueE (String ColumnName, Object value)
	{
		return set_Value (ColumnName, value);
	}   //  setValueE

	/**
	 *  Set Value if updateable and correct class.
	 *  (and to NULL if not mandatory)
	 *  @param index index
	 *  @param value value
	 *  @return true if value set
	 */
	public final boolean set_Value (int index, Object value)
	{
		if (index < 0 || index >= get_ColumnCount())
		{
			log.log(Level.WARNING, "Index invalid - " + index);
			return false;
		}
		String ColumnName = p_info.getColumnName(index);
		String colInfo = " - " + ColumnName;
		//
		if (p_info.isVirtualColumn(index))
		{
			log.log(Level.WARNING, "Virtual Column" + colInfo);
			return false;
		}
		//
		if (!p_info.isColumnUpdateable(index))
		{
			colInfo += " - NewValue=" + value + " - OldValue=" + get_Value(index);
			log.log(Level.WARNING, "Column not updateable" + colInfo);
			return false;
		}
		//
		if (value == null)
		{
			if (p_info.isColumnMandatory(index))
			{
				log.log(Level.WARNING, "Cannot set mandatory column to null " + colInfo);
			//	Trace.printStack();
				return false;
			}
			m_newValues[index] = Null.NULL;          //  correct
			log.finer(ColumnName + " = null");
		}
		else
		{
			Class<?> clazz = p_info.getColumnClass(index);
			//  matching class or generic object
			if (value.getClass().equals(clazz) 
				|| clazz == Object.class)
				m_newValues[index] = value;     //  correct
			//  Integer can be set as BigDecimal 
			else if (value.getClass() == BigDecimal.class 
				&& clazz == Integer.class)
				m_newValues[index] = new Integer (((BigDecimal)value).intValue());
			//	Set Boolean
			else if (clazz == Boolean.class 
				&& ("Y".equals(value) || "N".equals(value)) )
				m_newValues[index] = new Boolean("Y".equals(value));
			//	Button
			else if (p_info.getColumnDisplayType(index) == DisplayType.Button)
			{
				if (ColumnName.endsWith("_ID"))
				{
					if (value instanceof Integer)
						m_newValues[index] = value;
					else
						m_newValues[index] = new Integer(value.toString());
				}
				else
					m_newValues[index] = value.toString();
			}
			else
			{
				log.log(Level.SEVERE, ColumnName
					+ " - Class invalid: " + value.getClass().toString()
					+ ", Should be " + clazz.toString() + ": " + value);
				return false;
			}
			//	Validate (Min/Max)
			String error = p_info.validate(index, value);
			if (error != null)
			{
				log.log(Level.WARNING, ColumnName + "=" + value + " - " + error);
				return false;
			}
			//	Length for String
			if (clazz == String.class)
			{
				String stringValue = value.toString();
				int length = p_info.getFieldLength(index);
				if (stringValue.length() > length && length > 0)
				{
					log.warning(ColumnName + " - Value too long - truncated to length=" + length);
					m_newValues[index] = stringValue.substring(0,length);
				}
			}
			log.finest(ColumnName + " = " + m_newValues[index]);
		}
		set_Keys (ColumnName, m_newValues[index]);
		if (p_changeVO != null)
			p_changeVO.addChangedValue(ColumnName, value);
		return true;
	}   //  setValue

	/**
	 *  Set Value w/o check (update, r/o, ..).
	 * 	Used when Column is R/O
	 *  Required for key and parent values
	 *  @param ColumnName column name
	 *  @param value value
	 *  @return true if value set
	 */
	public final boolean set_ValueNoCheck (String ColumnName, Object value)
	{
		boolean success = true;
		int index = get_ColumnIndex(ColumnName);
		if (index < 0)
		{
			log.log(Level.SEVERE, "Column not found - " + ColumnName);
			return false;
		}
		if (value == null)
			m_newValues[index] = Null.NULL;		//	write direct
		else
		{
			Class<?> clazz = p_info.getColumnClass(index);
			//  matching class or generic object
			if (value.getClass().equals(clazz) 
				|| clazz == Object.class)
				m_newValues[index] = value;     //  correct
			//  Integer can be set as BigDecimal 
			else if (value.getClass() == BigDecimal.class 
				&& clazz == Integer.class)
				m_newValues[index] = new Integer (((BigDecimal)value).intValue());
			//	Set Boolean
			else if (clazz == Boolean.class 
				&& ("Y".equals(value) || "N".equals(value)) )
				m_newValues[index] = new Boolean("Y".equals(value));
			else
			{
				log.warning (ColumnName
					+ " - Class invalid: " + value.getClass().toString()
					+ ", Should be " + clazz.toString() + ": " + value);
				m_newValues[index] = value;     //  correct
			}
			//	Validate (Min/Max)
			String error = p_info.validate(index, value);
			if (error != null)
			{
				success = false;
				log.warning(ColumnName + "=" + value + " - " + error);
			}
			//	length for String
			if (clazz == String.class)
			{
				String stringValue = value.toString();
				int length = p_info.getFieldLength(index);
				if (stringValue.length() > length && length > 0)
				{
					log.warning(ColumnName + " - Value too long - truncated to length=" + length);
					m_newValues[index] = stringValue.substring(0,length);
				}
			}
		}
		log.finest(ColumnName + " = " + m_newValues[index] 
				+ " (" + (m_newValues[index]==null ? "-" : m_newValues[index].getClass().getName()) + ")");
		set_Keys (ColumnName, m_newValues[index]);
		if (p_changeVO != null)
			p_changeVO.addChangedValue(ColumnName, value);
		return success;
	}   //  set_ValueNoCheck

	/**
	 *  Set Encrypted Value w/o check (update, r/o, ..).
	 * 	Used when Column is R/O
	 *  Required for key and parent values
	 *  @param ColumnName column name
	 *  @param value value
	 *  @return true if value set
	 */
	protected final boolean set_ValueNoCheckE (String ColumnName, Object value)
	{
		return set_ValueNoCheckE (ColumnName, value);
	}	//	set_ValueNoCheckE

	/**
	 * 	Set Value from String.
	 * 	No Check if column is updateable or mandatory
	 *	@param index index
	 *	@param stringValue value
	 *	@return error message or null
	 */
	protected String set_ValueString (int index, String stringValue)
		throws Exception
	{
		String error = null;
		if (index < 0 || index >= get_ColumnCount())
			return "Index invalid - " + index;
		String ColumnName = p_info.getColumnName(index);
		String colInfo = " - " + ColumnName;
		//
		if (p_info.isVirtualColumn(index))
			return "Virtual Column" + colInfo;
		/**	Updateable
		if (!p_info.isColumnUpdateable(index))
		{
			colInfo += " - NewValue=" + stringValue + " - OldValue=" + get_Value(index);
			return "Column not updateable" + colInfo;
		}
		/** Mandatory	*/
		if (stringValue == null)
		{
		//	if (p_info.isColumnMandatory(index))
		//		return "Cannot set mandatory column to null " + colInfo;
			m_newValues[index] = Null.NULL;          //  correct
			log.finer(ColumnName + " = null");
		}
		else
		{
			Class<?> clazz = p_info.getColumnClass(index);
			//	Button
			if (p_info.getColumnDisplayType(index) == DisplayType.Button)
			{
				if (ColumnName.endsWith("_ID"))
					m_newValues[index] = new Integer(stringValue);
				else
					m_newValues[index] = stringValue;
			}
			else if (clazz == String.class)
			{
				m_newValues[index] = stringValue;
				int length = p_info.getFieldLength(index);
				if (stringValue.length() > length && length > 0)
				{
					log.warning(ColumnName + " - Value too long - truncated to length=" + length);
					m_newValues[index] = stringValue.substring(0,length);
				}
			}
			//  Integer  
			else if (clazz == Integer.class)
				m_newValues[index] = new Integer(stringValue);
			//	BigDecimal
			else if (clazz == BigDecimal.class)
				m_newValues[index] = new BigDecimal(stringValue);
			//	Set Timestamp
			else if (clazz == Timestamp.class)
			{
				long time = Long.parseLong(stringValue);
				m_newValues[index] = new Timestamp(time);
			}
			//	Set Boolean
			else if (clazz == Boolean.class)
			{
				if (!("Y".equals(stringValue) || "N".equals(stringValue)))
					log.warning ("Boolean value = " + stringValue);
				m_newValues[index] = new Boolean("Y".equals(stringValue));
			}
			else
			{
				return ColumnName + ": Class unknown: " + clazz;
			}
			//	Validate (Min/Max)
			error = p_info.validate(index, stringValue);
			if (error != null)
				return ColumnName + ": " + error;
			log.finest(ColumnName + " = " + m_newValues[index]);
		}
		set_Keys (ColumnName, m_newValues[index]);
		return error;
	}	//	set_ValueString
	
	/**
	 *  Set Value of Column
	 *  @param AD_Column_ID column
	 *  @param value value
	 */
	public final void set_ValueOfColumn (int AD_Column_ID, Object value)
	{
		int index = p_info.getColumnIndex(AD_Column_ID);
		if (index < 0)
			log.log(Level.SEVERE, "Not found - AD_Column_ID=" + AD_Column_ID);
		String ColumnName = p_info.getColumnName(index);
		if (ColumnName.equals("IsApproved"))
			set_ValueNoCheck(ColumnName, value);
		else
			set_Value (index, value);
	}   //  setValueOfColumn
	
	
	/**
	 * 	Set Custom Column
	 *	@param columnName column
	 *	@param value value
	 */
	public final void set_CustomColumn (String columnName, Object value)
	{
		if (m_custom == null)
			m_custom = new HashMap<String,String>();
		String valueString = "NULL";
		if (value == null)
			;
		else if (value instanceof Number)
			valueString = value.toString();
		else if (value instanceof Boolean)
			valueString = ((Boolean)value).booleanValue() ? "'Y'" : "'N'";
		else if (value instanceof Timestamp)
			valueString = DB.TO_DATE((Timestamp)value, false);
		else //	if (value instanceof String)
			valueString = DB.TO_STRING(value.toString());
		//	Save it
		log.log(Level.INFO, columnName + "=" + valueString);
		m_custom.put(columnName, valueString);
	}	//	set_CustomColumn
	
	
	/**
	 *  Set (numeric) Key Value
	 *  @param ColumnName column name
	 *  @param value value
	 */
	private void set_Keys (String ColumnName, Object value)
	{
		//	Update if KeyColumn
		for (int i = 0; i < m_IDs.length; i++)
		{
			if (ColumnName.equals (m_KeyColumns[i]))
				m_IDs[i] = value;
		}	//	for all key columns
	}	//	setKeys

	
	/**************************************************************************
	 *  Get Column Count
	 *  @return column count
	 */
	protected int get_ColumnCount()
	{
		return p_info.getColumnCount();
	}   //  getColumnCount

	/**
	 *  Get Column Name
	 *  @param index index
	 *  @return ColumnName
	 */
	public String get_ColumnName (int index)
	{
		return p_info.getColumnName (index);
	}   //  getColumnName

	/**
	 *  Get Column Label
	 *  @param index index
	 *  @return Column Label
	 */
	protected String get_ColumnLabel (int index)
	{
		return p_info.getColumnLabel (index);
	}   //  getColumnLabel

	/**
	 *  Get Column Description
	 *  @param index index
	 *  @return column description
	 */
	protected String get_ColumnDescription (int index)
	{
		return p_info.getColumnDescription (index);
	}   //  getColumnDescription

	/**
	 *  Is Column Mandatory
	 *  @param index index
	 *  @return true if column mandatory
	 */
	protected boolean isColumnMandatory (int index)
	{
		return p_info.isColumnMandatory(index);
	}   //  isColumnNandatory

	/**
	 *  Is Column Updateable
	 *  @param index index
	 *  @return true if column updateable
	 */
	protected boolean isColumnUpdateable (int index)
	{
		return p_info.isColumnUpdateable(index);
	}	//	isColumnUpdateable

	/**
	 *  Set Column Updateable
	 *  @param index index
	 *  @param updateable column updateable
	 */
	protected void set_ColumnUpdateable (int index, boolean updateable)
	{
		p_info.setColumnUpdateable(index, updateable);
	}	//	setColumnUpdateable

	/**
	 * 	Set all columns updateable
	 * 	@param updateable updateable
	 */
	protected void setUpdateable (boolean updateable)
	{
		p_info.setUpdateable (updateable);
	}	//	setUpdateable

	/**
	 *  Get Column DisplayType
	 *  @param index index
	 *  @return display type
	 */
	protected int get_ColumnDisplayType (int index)
	{
		return p_info.getColumnDisplayType(index);
	}	//	getColumnDisplayType

	/**
	 *  Get Lookup
	 *  @param index index
	 *  @return Lookup or null
	 */
	protected Lookup get_ColumnLookup(int index)
	{
		return p_info.getColumnLookup(index);
	}   //  getColumnLookup

	/**
	 *  Get Column Index
	 *  @param columnName column name
	 *  @return index of column with ColumnName or -1 if not found
	 */
	public final int get_ColumnIndex (String columnName)
	{
		return p_info.getColumnIndex(columnName);
	}   //  getColumnIndex

	/**
	 * 	Get Display Value of value
	 *	@param columnName columnName
	 *	@param currentValue current value
	 *	@return String value with "./." as null
	 */
	protected String get_DisplayValue(String columnName, boolean currentValue)
	{
		Object value = currentValue ? get_Value(columnName) : get_ValueOld(columnName);
		if (value == null)
			return "./.";
		String retValue = value.toString();
		int index = get_ColumnIndex(columnName);
		if (index < 0)
			return retValue;
		int dt = get_ColumnDisplayType(index);
		if (DisplayType.isText(dt) || DisplayType.YesNo == dt)
			return retValue;
		//	Lookup
		Lookup lookup = get_ColumnLookup(index);
		if (lookup != null)
			return lookup.getDisplay(value);
		//	Other
		return retValue;
	}	//	get_DisplayValue


	/**
	 * 	Copy old values of From to new values of To.
	 *  Does not copy Keys
	 * 	@param from old, existing & unchanged PO
	 *  @param to new, not saved PO
	 * 	@param AD_Client_ID client
	 * 	@param AD_Org_ID org
	 */
	protected static void copyValues (PO from, PO to, int AD_Client_ID, int AD_Org_ID)
	{
		copyValues (from, to);
		to.setAD_Client_ID(AD_Client_ID);
		to.setAD_Org_ID(AD_Org_ID);
	}	//	copyValues

	/**
	 * 	Copy old values of From to new values of To.
	 *  Does not copy Keys and AD_Client_ID/AD_Org_ID
	 * 	@param from old, existing & unchanged PO
	 *  @param to new, not saved PO
	 */
	protected static void copyValues (PO from, PO to)
	{
		s_log.fine("From ID=" + from.get_ID() + " - To ID=" + to.get_ID());
		//	Different Classes
		if (from.getClass() != to.getClass())
		{
			for (int i1 = 0; i1 < from.m_oldValues.length; i1++)
			{
				if (from.p_info.isVirtualColumn(i1)
					|| from.p_info.isKey(i1))		//	KeyColumn
					continue;
				String colName = from.p_info.getColumnName(i1);
				//  Ignore Standard Values
				if (colName.startsWith("Created") 
					|| colName.startsWith("Updated")
					|| colName.equals("IsActive")
					|| colName.equals("AD_Client_ID") 
					|| colName.equals("AD_Org_ID"))
					;	//	ignore
				else
				{
					for (int i2 = 0; i2 < to.m_oldValues.length; i2++)
					{
						if (to.p_info.getColumnName(i2).equals(colName))
						{
							to.m_newValues[i2] = from.m_oldValues[i1];
							break;
						}
					}
				}
			}	//	from loop
		}
		else	//	same class
		{
			for (int i = 0; i < from.m_oldValues.length; i++)
			{
				if (from.p_info.isVirtualColumn(i)
					|| from.p_info.isKey(i))		//	KeyColumn
					continue;
				String colName = from.p_info.getColumnName(i);
				//  Ignore Standard Values
				if (colName.startsWith("Created") 
					|| colName.startsWith("Updated")
					|| colName.equals("IsActive")
					|| colName.equals("AD_Client_ID") 
					|| colName.equals("AD_Org_ID"))
					;	//	ignore
				else
					to.m_newValues[i] = from.m_oldValues[i];
			}
		}	//	same class
	}	//	copy

	/**
	 * 	Get Change VO
	 *	@return change vo
	 */
	public ChangeVO get_ChangeVO()
	{
		return p_changeVO;
	}	//	get_ChangeVO
	
	/**
	 * 	Set Change VO
	 *	@param change vo
	 */
	public void set_ChangeVO(ChangeVO change)
	{
		p_changeVO = change;
	}	//	set_ChangeVO

	/**************************************************************************
	 *  Load record with ID
	 * 	@param ID ID
	 * 	@param trxName transaction name
	 */
	public void load (int ID, String trxName)
	{
		log.finest("ID=" + ID);
		if (ID > 0)
		{
			m_IDs = new Object[] {new Integer(ID)};
			String keyColumn = null; //	p_info.getTableName() + "_ID";
			String parentColumn = null;
			for (int i = 0; i < p_info.getColumnCount(); i++)
			{
				if (p_info.isKey(i))
				{
					keyColumn = p_info.getColumnName(i);
					break;
				}
				if (p_info.isColumnParent(i))
					parentColumn = p_info.getColumnName(i);
			}
			if (keyColumn == null)
				keyColumn = parentColumn;
			if (keyColumn == null)
				throw new IllegalStateException("No PK for " + p_info.getTableName());
			m_KeyColumns = new String[] {keyColumn};
			load(trxName);
		}
		else	//	new
		{
			loadDefaults();
			m_createNew = true;
			setKeyInfo();	//	sets m_IDs
			loadComplete(true);
		}
	}	//	load


	/**
	 *  (re)Load record with m_ID[*]
	 *  @param trxName transaction
	 *  @return true if loaded
	 */
	public boolean load (String trxName)
	{
		m_trxName = trxName;
		boolean success = true;
		StringBuffer sql = new StringBuffer("SELECT ");
		int size = get_ColumnCount();
		for (int i = 0; i < size; i++)
		{
			if (i != 0)
				sql.append(",");
			sql.append(p_info.getColumnSQL(i));	//	Normal and Virtual Column
		}
		sql.append(" FROM ").append(p_info.getTableName())
			.append(" WHERE ")
			.append(get_WhereClause(false));

		//
	//	int index = -1;
		if (CLogMgt.isLevelFinest())
			log.finest(get_WhereClause(true));
		PreparedStatement pstmt = null;
		try
		{
			pstmt = DB.prepareStatement(sql.toString(), m_trxName);	//	local trx only
			for (int i = 0; i < m_IDs.length; i++)
			{
				Object oo = m_IDs[i];
				if (oo instanceof Integer)
					pstmt.setInt(i+1, ((Integer)m_IDs[i]).intValue());
				else
					pstmt.setString(i+1, m_IDs[i].toString());
			}
			ResultSet rs = pstmt.executeQuery();
			if (rs.next())
			{
				success = load(rs);
			}
			else
			{
				log.log(Level.SEVERE, "NO Data found for " + get_WhereClause(true), new Exception());
				m_IDs = new Object[] {I_ZERO};
				success = false;
			//	throw new DBException("NO Data found for " + get_WhereClause(true));
			}
			rs.close();
			pstmt.close();
			pstmt = null;
			m_createNew = false;
			//	reset new values
			m_newValues = new Object[size];
		}
		catch (Exception e)
		{
			String msg = "";
			if (m_trxName != null)
				msg = "[" + m_trxName + "] - ";
			msg += get_WhereClause(true)
			//	+ ", Index=" + index 
			//	+ ", Column=" + get_ColumnName(index)
			//	+ ", " + p_info.toString(index)
				+ ", SQL=" + sql.toString();
			success = false;
			m_IDs = new Object[] {I_ZERO};
			log.log(Level.SEVERE, msg, e);
		//	throw new DBException(e);
		}
		//	Finish
		try
		{
			if (pstmt != null)
				pstmt.close();
			pstmt = null;
		}
		catch (SQLException e1)
		{
		}
		loadComplete(success);
		return success;
	}   //  load


	/**
	 * 	Load from the current position of a ResultSet
	 * 	@param rs result set
	 * 	@return true if loaded
	 */
	protected boolean load (ResultSet rs)
	{
		int size = get_ColumnCount();
		boolean success = true;
		int index = 0;
		log.finest("(rs)");
		//  load column values
		for (index = 0; index < size; index++)
		{
			String columnName = p_info.getColumnName(index);
			if (DB.isMSSQLServer() && columnName.equals("[LineNo]"))
				columnName = "LineNo";
			//	Make sure Virtual Column is part of Result Set
			if (p_info.isVirtualColumn(index))
			{
				try	
				{
					int ii = rs.findColumn(columnName);
					if (ii < 0)
						continue;
				}
				catch (Exception e)
				{
					continue;
				}
			}
			Class<?> clazz = p_info.getColumnClass(index);
			int dt = p_info.getColumnDisplayType(index);
			try
			{
				if (clazz == Integer.class)
					m_oldValues[index] = decrypt(index, new Integer(rs.getInt(columnName)));
				else if (clazz == BigDecimal.class)
					m_oldValues[index] = decrypt(index, rs.getBigDecimal(columnName));
				else if (clazz == Boolean.class)
					m_oldValues[index] = new Boolean ("Y".equals(decrypt(index, rs.getString(columnName))));
				else if (clazz == Timestamp.class)
					m_oldValues[index] = decrypt(index, rs.getTimestamp(columnName));
				else if (DisplayType.isLOB(dt))
					m_oldValues[index] = get_LOB (rs.getObject(columnName));
				else if (clazz == String.class)
					m_oldValues[index] = decrypt(index, rs.getString(columnName));
				else
					m_oldValues[index] = loadSpecial(rs, index);
				//	NULL
				if (rs.wasNull() && m_oldValues[index] != null)
					m_oldValues[index] = null;
				//
				if (CLogMgt.isLevelAll())
					log.finest(String.valueOf(index) + ": " + p_info.getColumnName(index) 
						+ "(" + clazz + ") = " + m_oldValues[index]);
			}
			catch (SQLException e)
			{
				if (p_info.isVirtualColumn(index))	//	if rs constructor used
					log.log(Level.FINER, "Virtual Column not loaded: " + columnName);
				else
				{
					log.log(Level.WARNING, "(rs) - " + String.valueOf(index) 
						+ ": " + p_info.getTableName() + "." + p_info.getColumnName(index) 
						+ " (" + clazz + ") - " + e);
					success = false;
				}
			}
		}
		m_createNew = false;
		setKeyInfo();
		loadComplete(success);
		return success;
	}	//	load

	/**
	 * 	Load from HashMap.
	 * 	Do not use unless you know what you are doing.
	 * 	@param hmIn hash map
	 * 	@return true if loaded
	 */
	public boolean load (Map<String,String> hmIn)
	{
		int size = get_ColumnCount();
		boolean success = true;
		//
		boolean keyHasNoValue = true;
		String[] keyColumns = get_KeyColumns();
		if (keyColumns.length > 0)
		{
			for (int i = 0; i < keyColumns.length; i++)
            {
	            String keyColumn = keyColumns[i];
	            String value = hmIn.get(keyColumn);
	            if (value != null)
	            {
	            	int index = p_info.getColumnIndex(keyColumn);
					m_oldValues[index] = new Integer(value);
	            }
	            keyHasNoValue = false;
            }
		}
		//
		log.finest("(hm)");
		int index = 0;
		//  load column values
		for (index = 0; index < size; index++)
		{
			String columnName = p_info.getColumnName(index);
			String value = (String)hmIn.get(columnName);
			if (value == null || value.length() == 0)
				continue;
			Class<?> clazz = p_info.getColumnClass(index);
			int dt = p_info.getColumnDisplayType(index);
			try
			{
				if (clazz == Integer.class)
					m_newValues[index] = new Integer(value);
				else if (clazz == BigDecimal.class)
					m_newValues[index] = new BigDecimal(value);
				else if (clazz == Boolean.class)
					m_newValues[index] = new Boolean ("Y".equals(value) 
						|| "on".equals (value) || "true".equals(value));
				else if (clazz == Timestamp.class)
					m_newValues[index] = convertToTimestamp(value);
				else if (DisplayType.isLOB(dt))
					m_newValues[index] = null;	//	get_LOB (rs.getObject(columnName));
				else if (clazz == String.class)
				{
					m_newValues[index] = value;
					int length = p_info.getFieldLength (index);
					if (value.length() > length)
					{
						m_newValues[index] = value.substring (0, length);
						log.warning(columnName + ": truncated to length=" + length + " from=" + value);
					}
				}
				else
					m_newValues[index] = null;	// loadSpecial(rs, index);
				//
				if (CLogMgt.isLevelAll())
					log.finest(String.valueOf(index) + ": " + p_info.getColumnName(index) 
						+ "(" + clazz + ") = " + m_oldValues[index]);
			}
			catch (Exception e)
			{
				if (p_info.isVirtualColumn(index))	//	if rs constructor used
					log.log(Level.FINER, "Virtual Column not loaded: " + columnName);
				else
				{
					log.log(Level.SEVERE, "(ht) - " + String.valueOf(index) 
						+ ": " + p_info.getTableName() + "." + p_info.getColumnName(index) 
						+ " (" + clazz + ") - " + e);
					success = false;
				}
			}
		}
		//	Overwrite
		//	setStandardDefaults();
		setKeyInfo();
		loadComplete(success);
		//
		m_createNew = keyHasNoValue;
		return success;
	}	//	load

	/**
	 *  Create HashMap with data as Strings
	 *  @return HashMap
	 */
	protected HashMap<String,String> get_HashMap()
	{
		HashMap<String,String> hmOut = new HashMap<String,String>();
		fillMap(hmOut);
		return hmOut;
	}	//	get_HashMap
	
	/**
	 * 	Get Object as VO 
	 *	@return
	 */
	public VO getVO()
	{
		VO vo = new VO();
		fillMap (vo);
		return vo;
	}	//	getVO
	
	/**
	 *  Fill map with data as Strings
	 *  @param hmOut map
	 *  @return HashMap
	 */
	private void fillMap(Map<String,String> hmOut)
	{
		int size = get_ColumnCount();
		for (int i = 0; i < size; i++)
		{
			Object value = get_Value(i);
			//	Don't insert NULL values (allows Database defaults)
			if (value == null 
				|| p_info.isVirtualColumn(i))
				continue;
			//	Display Type
			int dt = p_info.getColumnDisplayType(i);
			//  Based on class of definition, not class of value
			Class<?> c = p_info.getColumnClass(i);
			String stringValue = null;
			if (c == Object.class)
				;	//	saveNewSpecial (value, i));
			else if (value == null || value.equals (Null.NULL))
				;
			else if (value instanceof Integer || value instanceof BigDecimal)
				stringValue = value.toString();
			else if (c == Boolean.class)
			{
				boolean bValue = false;
				if (value instanceof Boolean)
					bValue = ((Boolean)value).booleanValue();
				else
					bValue = "Y".equals(value);
				stringValue = bValue ? "Y" : "N";
			}
			else if (value instanceof Timestamp)
				stringValue = value.toString();
			else if (c == String.class)
				stringValue = (String)value;
			else if (dt == DisplayType.TextLong)
				stringValue = (String)value;
			else if (dt == DisplayType.Binary)
				stringValue = Util.toHexString((byte[])value);
			else
				;	//	saveNewSpecial (value, i));
			//
			if (stringValue != null)
				hmOut.put(p_info.getColumnName(i), stringValue);
		}
		//	Custom Columns
		if (m_custom != null)
		{
			Iterator<String> it = m_custom.keySet().iterator();
			while (it.hasNext())
			{
				String column = it.next();
			//	int index = p_info.getColumnIndex(column);
				String value = (String)m_custom.get(column);
				if (value != null)
					hmOut.put(column, value);
			}
			m_custom = null;
		}
	}   //  fillMap
		
	/**
	 *  Load Special data (images, ..).
	 *  To be extended by sub-classes
	 *  @param rs result set
	 *  @param index zero based index
	 *  @return value value
	 *  @throws SQLException
	 */
	protected Object loadSpecial (ResultSet rs, int index) throws SQLException
	{
		log.finest("(NOP) - " + p_info.getColumnName(index));
		return null;
	}   //  loadSpecial

	/**
	 *  Load is complete
	 * 	@param success success
	 *  To be extended by sub-classes
	 */
	protected void loadComplete (boolean success)
	{
	}   //  loadComplete


	/**
	 *	Load Defaults
	 */
	protected void loadDefaults()
	{
		setStandardDefaults();
		//
		/** @todo defaults from Field */
	//	MField.getDefault(p_info.getDefaultLogic(i));
	}	//	loadDefaults

	/**
	 *  Set Default values.
	 *  Client, Org, Created/Updated, *By, IsActive
	 */
	protected void setStandardDefaults()
	{
		int size = get_ColumnCount();
		for (int i = 0; i < size; i++)
		{
			if (p_info.isVirtualColumn(i))
				continue;
			String colName = p_info.getColumnName(i);
			//  Set Standard Values
			if (colName.endsWith("tedBy"))
				m_newValues[i] = new Integer (p_ctx.getAD_User_ID());
			else if (colName.equals("Created") || colName.equals("Updated"))
				m_newValues[i] = new Timestamp (System.currentTimeMillis());
			else if (colName.equals(p_info.getTableName() + "_ID"))    //  KeyColumn
				m_newValues[i] = I_ZERO;
			else if (colName.equals("IsActive"))
				m_newValues[i] = new Boolean(true);
			else if (colName.equals("AD_Client_ID"))
				m_newValues[i] = new Integer(p_ctx.getAD_Client_ID());
			else if (colName.equals("AD_Org_ID"))
				m_newValues[i] = new Integer(p_ctx.getAD_Org_ID());
			else if (colName.equals("Processed"))
				m_newValues[i] = new Boolean(false);
			else if (colName.equals("Processing"))
				m_newValues[i] = new Boolean(false);
			else if (colName.equals("Posted"))
				m_newValues[i] = new Boolean(false);
		}
	}   //  setDefaults

	/**
	 * 	Set Key Info (IDs and KeyColumns).
	 */
	private void setKeyInfo()
	{
		//	Search for Primary Key
		for (int i = 0; i < p_info.getColumnCount(); i++)
		{
			if (p_info.isKey(i))
			{
				String ColumnName = p_info.getColumnName(i);
				m_KeyColumns = new String[] {ColumnName};
				if (p_info.getColumnName(i).endsWith("_ID"))
				{
					Integer ii = (Integer)get_Value(i);
					if (ii == null)
						m_IDs = new Object[] {I_ZERO};
					else
						m_IDs = new Object[] {ii};
					log.finest("(PK) " + ColumnName + "=" + ii);
				}
				else
				{
					Object oo = get_Value(i);
					if (oo == null)
						m_IDs = new Object[] {null};
					else
						m_IDs = new Object[] {oo};
					log.finest("(PK) " + ColumnName + "=" + oo);
				}
				return;
			}
		}	//	primary key search

		//	Search for Parents
		ArrayList<String> columnNames = new ArrayList<String>();
		for (int i = 0; i < p_info.getColumnCount(); i++)
		{
			if (p_info.isColumnParent(i))
				columnNames.add(p_info.getColumnName(i));
		}
		//	Set FKs
		int size = columnNames.size();
		if (size == 0)
			throw new IllegalStateException("No PK nor FK - " + p_info.getTableName());
		m_IDs = new Object[size];
		m_KeyColumns = new String[size];
		for (int i = 0; i < size; i++)
		{
			m_KeyColumns[i] = (String)columnNames.get(i);
			if (m_KeyColumns[i].endsWith("_ID"))
			{
				Integer ii = null;
				try
				{
					ii = (Integer)get_Value(m_KeyColumns[i]);
				}
				catch (Exception e)
				{
					log.log(Level.SEVERE, "", e);
				}
				if (ii != null)
					m_IDs[i] = ii;
			}
			else
				m_IDs[i] = get_Value(m_KeyColumns[i]);
			log.finest("(FK) " + m_KeyColumns[i] + "=" + m_IDs[i]);
		}
	}	//	setKeyInfo

	
	/**************************************************************************
	 *  Are all mandatory Fields filled (i.e. can we save)?.
	 *  Stops at first null mandatory field
	 *  @return true if all mandatory fields are ok
	 */
	protected boolean isMandatoryOK()
	{
		int size = get_ColumnCount();
		for (int i = 0; i < size; i++)
		{
			if (p_info.isColumnMandatory(i))
			{
				if (p_info.isVirtualColumn(i))
					continue;
				if (get_Value(i) == null || get_Value(i).equals(Null.NULL))
				{
					log.info(p_info.getColumnName(i));
					return false;
				}
			}
		}
		return true;
	}   //  isMandatoryOK

	
	/**************************************************************************
	 * 	Set AD_Client
	 * 	@param AD_Client_ID client
	 */
	final public void setAD_Client_ID (int AD_Client_ID)
	{
		set_ValueNoCheck ("AD_Client_ID", new Integer(AD_Client_ID));
	}	//	setAD_Client_ID

	/**
	 * 	Get AD_Client
	 * 	@return AD_Client_ID
	 */
	public final int getAD_Client_ID()
	{
		Integer ii = (Integer)get_Value("AD_Client_ID");
		if (ii == null)
			return 0;
		return ii.intValue();
	}	//	getAD_Client_ID

	/**
	 * 	Set AD_Org
	 * 	@param AD_Org_ID org
	 */
	final public void setAD_Org_ID (int AD_Org_ID)
	{
		set_ValueNoCheck ("AD_Org_ID", new Integer(AD_Org_ID));
	}	//	setAD_Org_ID

	/**
	 * 	Get AD_Org
	 * 	@return AD_Org_ID
	 */
	public int getAD_Org_ID()
	{
		Integer ii = (Integer)get_Value("AD_Org_ID");
		if (ii == null)
			return 0;
		return ii.intValue();
	}	//	getAD_Org_ID

	/**
	 * 	Overwrite Client Org if different
	 *	@param AD_Client_ID client
	 *	@param AD_Org_ID org
	 */
	public void setClientOrg (int AD_Client_ID, int AD_Org_ID)
	{
		if (AD_Client_ID != getAD_Client_ID())
			setAD_Client_ID(AD_Client_ID);
		if (AD_Org_ID != getAD_Org_ID())
			setAD_Org_ID(AD_Org_ID);
	}	//	setClientOrg
	
	/**
	 * 	Overwrite Client Org if different
	 *	@param po persistent object
	 */
	public void setClientOrg (PO po)
	{
		setClientOrg(po.getAD_Client_ID(), po.getAD_Org_ID());
	}	//	setClientOrg

	/**
	 * 	Set Active
	 * 	@param active active
	 */
	public final void setIsActive (boolean active)
	{
		set_Value("IsActive", new Boolean(active));
	}	//	setActive

	/**
	 *	Is Active
	 *  @return is active
	 */
	public final boolean isActive()
	{
		Boolean bb = (Boolean)get_Value("IsActive");
		if (bb != null)
			return bb.booleanValue();
		return false;
	}	//	isActive

	/**
	 * 	Get Created
	 * 	@return created
	 */
	final public Timestamp getCreated()
	{
		return (Timestamp)get_Value("Created");
	}	//	getCreated

	/**
	 * 	Get Updated
	 *	@return updated
	 */
	final public Timestamp getUpdated()
	{
		return (Timestamp)get_Value("Updated");
	}	//	getUpdated

	/**
	 * 	Get CreatedBy
	 * 	@return AD_User_ID
	 */
	final public int getCreatedBy()
	{
		Integer ii = (Integer)get_Value("CreatedBy");
		if (ii == null)
			return 0;
		return ii.intValue();
	}	//	getCreateddBy

	/**
	 * 	Get UpdatedBy
	 * 	@return AD_User_ID
	 */
	final public int getUpdatedBy()
	{
		Integer ii = (Integer)get_Value("UpdatedBy");
		if (ii == null)
			return 0;
		return ii.intValue();
	}	//	getUpdatedBy

	/**
	 * 	Set UpdatedBy
	 * 	@param AD_User_ID user
	 */
	final protected void setUpdatedBy (int AD_User_ID)
	{
		set_ValueNoCheck ("UpdatedBy", new Integer(AD_User_ID));
	}	//	setAD_User_ID

	/**
	 * 	Get Translation of column
	 *	@param columnName
	 *	@param AD_Language
	 *	@return translation or null if not found
	 */
	protected String get_Translation (String columnName, String AD_Language)
	{
		if (columnName == null || AD_Language == null 
			|| m_IDs.length > 1 || m_IDs[0].equals(I_ZERO) 
			|| !(m_IDs[0] instanceof Integer))
		{
			log.severe ("Invalid Argument: ColumnName" + columnName
				+ ", AD_Language=" + AD_Language 
				+ ", ID.length=" + m_IDs.length + ", ID=" + m_IDs[0]);
			return null;
		}
		int ID = ((Integer)m_IDs[0]).intValue();
		String retValue = null;
		StringBuffer sql = new StringBuffer ("SELECT ").append(columnName)
			.append(" FROM ").append(p_info.getTableName()).append("_Trl WHERE ")
			.append(m_KeyColumns[0]).append("=?")
			.append(" AND AD_Language=?");
		PreparedStatement pstmt = null;
		try
		{
			pstmt = DB.prepareStatement (sql.toString(), get_TrxName());
			pstmt.setInt (1, ID);
			pstmt.setString (2, AD_Language);
			ResultSet rs = pstmt.executeQuery ();
			if (rs.next ())
				retValue = rs.getString(1);
			rs.close ();
			pstmt.close ();
			pstmt = null;
		}
		catch (Exception e)
		{
			log.log(Level.SEVERE, sql.toString(), e);
		}
		try
		{
			if (pstmt != null)
				pstmt.close ();
			pstmt = null;
		}
		catch (Exception e)
		{
			pstmt = null;
		}
		return retValue;
	}	//	get_Translation

	/**
	 * 	Is new record
	 *	@return true if new
	 */
	public boolean is_new()
	{
		if (m_createNew)
			return true;
		//
		for (int i = 0; i < m_IDs.length; i++)
		{
			if (m_IDs[i].equals(I_ZERO))
				continue;
			return false;	//	one value is non-zero
		}
		return true;
	}	//	is_new
	
	/**
	 * 	Update PO based on string context
	 *	@param context row context
	 *	@return error message or null
	 */
	public String update (Map<String,String> context)
	{
		String error = null;
		Set<Map.Entry<String,String>> set = context.entrySet();
		Iterator<Map.Entry<String,String>> it = set.iterator();
		while (it.hasNext())
		{
			Map.Entry<String,String> entry = it.next();
			String columnName = entry.getKey();
			int index = get_ColumnIndex(columnName);
			if (index != -1)	//	ignore all other
			{
				//	Ignore Key Column
				if (is_KeyColumn(columnName))
					continue;
				//	Virtual Column
				if (p_info.isVirtualColumn(index))
					continue;
				//	Don't allow to overwrite Std Column for users
				if (columnName.equals("Created") || columnName.equals("CreatedBy")
					|| columnName.equals("Updated") || columnName.equals("UpdatedBy"))
					continue;
				//	New 
				String stringValue = entry.getValue();
				if (stringValue != null && 
					(stringValue.equals(Null.NULLString) 
						|| stringValue.length () == 0))
					stringValue = null;
				//	Old
				String oldValue = get_ValueAsString(columnName);
				if (oldValue.length() == 0)
					oldValue = null;
				
				//	Same
				if ((oldValue == null && stringValue == null)
					|| (stringValue != null && oldValue != null 
						&& stringValue.equals(oldValue)))
					continue;
				//
				try
				{
					error = set_ValueString (index, stringValue);
				}
				catch (Exception e)
				{
					error = e.getLocalizedMessage();
					if (error == null)
						error = e.toString();
				}
				if (error != null)
					return error;
			}
		}
		return error;
	}	//	update
	
	/**************************************************************************
	 *  Update Value or create new record.
	 * 	To reload call load() - not updated
	 *  @return true if saved
	 */
	public boolean save()
	{
		CLogger.resetLast();
		boolean newRecord = is_new();	//	save locally as load resets
		if (!newRecord && !is_Changed())
		{
			log.fine("Nothing changed - " + p_info.getTableName());
			return true;
		}

		//	Organization Check
		if (getAD_Org_ID() == 0 
			&& (get_AccessLevel() == ACCESSLEVEL_ORG
				|| (get_AccessLevel() == ACCESSLEVEL_CLIENTORG 
					&& MClientShare.isOrgLevelOnly(getAD_Client_ID(), get_Table_ID()))))
		{
			log.saveError("FillMandatory", Msg.getElement(getCtx(), "AD_Org_ID"));
			return false;
		}
		//	Should be Org 0
		if (getAD_Org_ID() != 0)
		{
			boolean reset = get_AccessLevel() == ACCESSLEVEL_SYSTEM;
			if (!reset && MClientShare.isClientLevelOnly(getAD_Client_ID(), get_Table_ID()))
			{
				reset = get_AccessLevel() == ACCESSLEVEL_CLIENT
					|| get_AccessLevel() == ACCESSLEVEL_SYSTEMCLIENT
					|| get_AccessLevel() == ACCESSLEVEL_CLIENTORG; 
			}
			if (reset)
			{
				log.warning("Set Org to 0");
				setAD_Org_ID(0);
			}
		}
		//	Before Save
		MAssignSet.execute(this, newRecord);	//	Automatic Assignment
		try
		{
			if (!beforeSave(newRecord))
			{
				log.warning("beforeSave failed - " + toString());
				return false;
			}
		}
		catch (Exception e)
		{
			log.log(Level.WARNING, "beforeSave - " + toString(), e);
			log.saveError("Error", e.toString(), false);
		//	throw new DBException(e);
			return false;
		}
		//	Model Validator
		String errorMsg = ModelValidationEngine.get().fireModelChange
			(this, newRecord ? ModelValidator.CHANGETYPE_NEW : ModelValidator.CHANGETYPE_CHANGE);
		if (errorMsg != null)
		{
			log.warning("Validation failed - " + errorMsg);
			log.saveError("Error", errorMsg);
			return false;
		}
		//	Save
		if (newRecord)
			return saveNew();
		else
			return saveUpdate();
	}	//	save
	
	/**
	 * 	Finish Save Process
	 *	@param newRecord new
	 *	@param success success
	 *	@return true if saved
	 */
	private boolean saveFinish (boolean newRecord, boolean success)
	{
		//	Translations
		if (success)
		{
			if (newRecord)
				insertTranslations();
			else
				updateTranslations();
			updatePreferences();
		}
		//
		try
		{
			success = afterSave (newRecord, success);
			if (success && newRecord)
				insertTreeNode();
		}
		catch (Exception e)
		{
			log.log(Level.WARNING, "afterSave", e);
			log.saveError("Error", e.toString(), false);
			success = false;
		//	throw new DBException(e);
		}
		//	OK
		if (success)
		{
			if (s_docWFMgr == null)
			{
				try
				{
					Class.forName("org.compiere.wf.DocWorkflowManager");
				}
				catch (Exception e)
				{
				}
			}
			if (s_docWFMgr != null)
				s_docWFMgr.process (this, p_info.getAD_Table_ID());
			
			//	Copy to Old values
			int size = p_info.getColumnCount();
			for (int i = 0; i < size; i++)
			{
				if (m_newValues[i] != null)
				{
					if (m_newValues[i] == Null.NULL)
						m_oldValues[i] = null;
					else
						m_oldValues[i] = m_newValues[i];
				}
			}
			m_newValues = new Object[size];
		}
		m_createNew = false;
		if (!newRecord)
			CacheMgt.get().reset(p_info.getTableName());
		return success;
	}	//	saveFinish

	/**
	 *  Update Value or create new record.
	 * 	To reload call load() - not updated
	 *	@param trxName transaction
	 *  @return true if saved
	 */
	public boolean save (String trxName)
	{
		set_TrxName(trxName);
		return save();
	}	//	save

	/**
	 * 	Is there a Change to be saved?
	 *	@return true if record changed
	 */
	public boolean is_Changed()
	{
		int size = get_ColumnCount();
		for (int i = 0; i < size; i++)
		{
			if (m_newValues[i] != null)
				return true;	//	something changed
		}
		return false;
	}	//	is_Change
	
	/**
	 * 	Called before Save for Pre-Save Operation
	 * 	@param newRecord new record
	 *	@return true if record can be saved
	 */
	protected boolean beforeSave(boolean newRecord)
	{
		/** Prevents saving
		log.saveError("Error", Msg.parseTranslation(getCtx(), "@C_Currency_ID@ = @C_Currency_ID@"));
		log.saveError("FillMandatory", Msg.getElement(getCtx(), "PriceEntered"));
		/** Issues message
		log.saveWarning(AD_Message, message);
		log.saveInfo (AD_Message, message);
		**/
		return true;
	}	//	beforeSave

	/**
	 * 	Called after Save for Post-Save Operation
	 * 	@param newRecord new record
	 *	@param success true if save operation was success
	 *	@return if save was a success
	 */
	protected boolean afterSave (boolean newRecord, boolean success)
	{
		return success;
	}	//	afterSave

	/**
	 * 	Update Record directly
	 * 	@return true if updated
	 */
	protected boolean saveUpdate()
	{
		String where = get_WhereClause(true);
		//
		boolean changes = false;
		StringBuffer sql = new StringBuffer ("UPDATE ");
		sql.append(p_info.getTableName()).append( " SET ");
		boolean updated = false;
		boolean updatedBy = false;
		lobReset();

		//	Change Log
		MSession session = MSession.get (p_ctx, true);
		int AD_ChangeLog_ID = 0;
		
		int size = get_ColumnCount();
		for (int i = 0; i < size; i++)
		{
			Object value = m_newValues[i];
			if (value == null
				|| p_info.isVirtualColumn(i))
				continue;
			//  we have a change
			Class<?> c = p_info.getColumnClass(i);
			int dt = p_info.getColumnDisplayType(i);
			String columnName = p_info.getColumnName(i); 
			//
			//	updated/by	
			if (columnName.equals("UpdatedBy"))
			{
				if (updatedBy)	//	explicit
					continue;
				updatedBy = true;
			}
			else if (columnName.equals("Updated"))
			{
				if (updated)
					continue;
				updated = true;
			}
			if (DisplayType.isLOB(dt))
			{
				lobAdd (value, i, dt);
				//	If no changes set UpdatedBy explicitly to ensure commit of lob
				if (!changes & !updatedBy)
				{
					int AD_User_ID = p_ctx.getAD_User_ID();
					set_ValueNoCheck("UpdatedBy", new Integer(AD_User_ID));
					sql.append("UpdatedBy=").append(AD_User_ID);
					changes = true;
					updatedBy = true;
				}
				continue;
			}
			//	Update Document No
			if (columnName.equals("DocumentNo"))
			{
				String strValue = (String)value;
				if (strValue.startsWith("<") && strValue.endsWith(">"))
				{
					value = null;
					int AD_Client_ID = getAD_Client_ID();
					int index = p_info.getColumnIndex("C_DocTypeTarget_ID");
					if (index == -1)
						index = p_info.getColumnIndex("C_DocType_ID");
					if (index != -1)		//	get based on Doc Type (might return null)
						value = DB.getDocumentNo(get_ValueAsInt(index), m_trxName);
					if (value == null)	//	not overwritten by DocType and not manually entered
						value = DB.getDocumentNo(AD_Client_ID, p_info.getTableName(), m_trxName);
				}
				else
					log.warning("DocumentNo updated: " + m_oldValues[i] + " -> " + value);
			}
				
			if (changes)
				sql.append(", ");
			changes = true;
			sql.append(columnName).append("=");
			
			//  values
			if (value == Null.NULL)
				sql.append("NULL");
			else if (value instanceof Integer || value instanceof BigDecimal)
				sql.append(encrypt(i,value));
			else if (c == Boolean.class)
			{
				boolean bValue = false;
				if (value instanceof Boolean)
					bValue = ((Boolean)value).booleanValue();
				else
					bValue = "Y".equals(value);
				sql.append(encrypt(i,bValue ? "'Y'" : "'N'"));
			}
			else if (value instanceof Timestamp)
				sql.append(DB.TO_DATE((Timestamp)encrypt(i,value),p_info.getColumnDisplayType(i) == DisplayType.Date));
			else
				sql.append(encrypt(i,DB.TO_STRING(value.toString())));
			
			//	Change Log	- Only 
			if (session != null
				&& !p_info.isEncrypted(i)		//	not encrypted
				&& !p_info.isVirtualColumn(i)	//	no virtual column
				&& !"Password".equals(columnName)
				)
			{
				Object oldV = m_oldValues[i];
				Object newV = value;
				if (oldV != null && oldV == Null.NULL)
					oldV = null;
				if (newV != null && newV == Null.NULL)
					newV = null;
				//
				Object keyInfo = get_ID();
				if (m_IDs.length != 1)
					keyInfo = get_WhereClause(true);
				MChangeLog cLog = session.changeLog (
					m_trxName, AD_ChangeLog_ID, 
					p_info.getAD_Table_ID(), p_info.getColumn(i).AD_Column_ID, 
					keyInfo, getAD_Client_ID(), getAD_Org_ID(), oldV, newV,
					get_TableName(), MChangeLog.CHANGELOGTYPE_Update);
				if (cLog != null)
					AD_ChangeLog_ID = cLog.getAD_ChangeLog_ID();
			}
		}	//   for all fields

		//	Custom Columns (cannot be logged as no column)
		if (m_custom != null)
		{
			Iterator<String> it = m_custom.keySet().iterator();
			while (it.hasNext())
			{
				if (changes)
					sql.append(", ");
				changes = true;
				//
				String columnName = it.next();
				int index = p_info.getColumnIndex(columnName);
				String value = (String)m_custom.get(columnName);
				if (value != null && value.equals(Null.NULLString))
					value = "NULL";
				sql.append(columnName).append("=").append(value);	//	no encryption
				
				//	Change Log
				if (session != null
					&& !p_info.isEncrypted(index)		//	not encrypted
					&& !p_info.isVirtualColumn(index)	//	no virtual column
					&& !"Password".equals(columnName)
					)
				{
					Object oldV = m_oldValues[index];
					Object newV = value;
					if (oldV != null && oldV == Null.NULL)
						oldV = null;
					if (newV != null && newV == Null.NULL)
						newV = null;
					//
					MChangeLog cLog = session.changeLog (
						m_trxName, AD_ChangeLog_ID, 
						p_info.getAD_Table_ID(), p_info.getColumn(index).AD_Column_ID, 
						m_IDs, getAD_Client_ID(), getAD_Org_ID(), oldV, newV,
						get_TableName(), MChangeLog.CHANGELOGTYPE_Update);
					if (cLog != null)
						AD_ChangeLog_ID = cLog.getAD_ChangeLog_ID();
				}
			}
			m_custom = null;
		}
		
		//	Something changed
		if (changes)
		{
			if (m_trxName == null)
				log.fine(p_info.getTableName() + "." + where);
			else
				log.fine("[" + m_trxName + "] - " + p_info.getTableName() + "." + where);
			if (!updated)	//	Updated not explicitly set
			{
				Timestamp now = new Timestamp(System.currentTimeMillis()); 
				set_ValueNoCheck("Updated", now);
				sql.append(",Updated=").append(DB.TO_DATE(now, false));
			}
			if (!updatedBy)	//	UpdatedBy not explicitly set
			{
				int AD_User_ID = p_ctx.getAD_User_ID();
				set_ValueNoCheck("UpdatedBy", new Integer(AD_User_ID));
				sql.append(",UpdatedBy=").append(AD_User_ID);
			}
			sql.append(" WHERE ").append(where);
			/** @todo status locking goes here */
			
			log.finest(sql.toString());
			int no = DB.executeUpdate(sql.toString(), m_trxName);
			boolean ok = no == 1;
			if (ok)
				ok = lobSave();
			else
			{
				if (m_trxName == null)
					log.log(Level.WARNING, "#" + no 
						+ " - " + p_info.getTableName() + "." + where);
				else
					log.log(Level.WARNING, "#" + no
						+ " - [" + m_trxName + "] - " + p_info.getTableName() + "." + where);
			}
			return saveFinish (false, ok);
		}
		
		//  nothing changed, so OK
		return saveFinish (false, true);
	}   //  saveUpdate

	/**
	 *  Create New Record
	 *  @return true if new record inserted
	 */
	private boolean saveNew()
	{
		//  Set ID for single key - Multi-Key values need explicitly be set previously
		if (m_IDs.length == 1 && p_info.hasKeyColumn()
			&& m_KeyColumns[0].endsWith("_ID"))	//	AD_Language, EntityType
		{
			int no = saveNew_getID();
			if (no <= 0)
				no = DB.getNextID(getAD_Client_ID(), p_info.getTableName(), m_trxName);
			if (no <= 0)
			{
				log.severe("No NextID (" + no + ")");
				return saveFinish (true, false);
			}
			m_IDs[0] = new Integer(no);
			set_ValueNoCheck(m_KeyColumns[0], m_IDs[0]);
		}
		if (m_trxName == null)
			log.fine(p_info.getTableName() + " - " + get_WhereClause(true));
		else
			log.fine("[" + m_trxName + "] - " + p_info.getTableName() + " - " + get_WhereClause(true));
		
		//	Set new DocumentNo
		String columnName = "DocumentNo";
		int index = p_info.getColumnIndex(columnName);
		if (index != -1)
		{
			String value = (String)get_Value(index);
			if (value != null && value.startsWith("<") && value.endsWith(">"))
				value = null;
			if (value == null || value.length() == 0)
			{
				int dt = p_info.getColumnIndex("C_DocTypeTarget_ID");
				if (dt == -1)
					dt = p_info.getColumnIndex("C_DocType_ID");
				if (dt != -1)		//	get based on Doc Type (might return null)
					value = DB.getDocumentNo(get_ValueAsInt(dt), m_trxName);
				if (value == null)	//	not overwritten by DocType and not manually entered
					value = DB.getDocumentNo(getAD_Client_ID(), p_info.getTableName(), m_trxName);
				set_ValueNoCheck(columnName, value);
			}
		}
		//	Set empty Value
		columnName = "Value";
		index = p_info.getColumnIndex(columnName);
		if (index != -1)
		{
			String value = (String)get_Value(index);
			if (value == null || value.length() == 0)
			{
				value = DB.getDocumentNo (getAD_Client_ID(), p_info.getTableName(), m_trxName);
				set_ValueNoCheck(columnName, value);
			}
		}
		
		lobReset();

		boolean ok = saveNewInsertSQL();
		return saveFinish (true, ok);
	}   //  saveNew

	/**
	 * 	Create Insert SQL and execute
	 * 	@return success
	 */
	public boolean saveNewInsertSQL() 
	{
		//	Change Log
		MSession session = MSession.get (p_ctx, false);
		int AD_ChangeLog_ID = 0;

		//	SQL
		StringBuffer sqlInsert = new StringBuffer("INSERT INTO ");
		sqlInsert.append(p_info.getTableName()).append(" (");
		StringBuffer sqlValues = new StringBuffer(") VALUES (");
		int size = get_ColumnCount();
		boolean doComma = false;
		for (int i = 0; i < size; i++)
		{
			if (p_info.isVirtualColumn(i))
				continue;
			Object value = get_Value(i);
			//	Don't insert NULL values (allows Database defaults)
			if (value == null || value == Null.NULL)
				continue;
			
			//	Display Type
			int dt = p_info.getColumnDisplayType(i);
			if (DisplayType.isLOB(dt))
			{
				lobAdd (value, i, dt);
				continue;
			}
			
			//	** add column **
			if (doComma)
			{
				sqlInsert.append(",");
				sqlValues.append(",");
			}
			else
				doComma = true;
			String columnName = p_info.getColumnName(i);
			sqlInsert.append(columnName);
			//
			//  Based on class of definition, not class of value
			Class<?> c = p_info.getColumnClass(i);
			try
			{
				if (c == Object.class) //  may have need to deal with null values differently
					sqlValues.append (saveNewSpecial (value, i));
				else if (value == null || value.equals (Null.NULL))
					sqlValues.append ("NULL");
				else if (value instanceof Integer || value instanceof BigDecimal)
					sqlValues.append (encrypt(i,value));
				else if (c == Boolean.class)
				{
					boolean bValue = false;
					if (value instanceof Boolean)
						bValue = ((Boolean)value).booleanValue();
					else
						bValue = "Y".equals(value);
					sqlValues.append (encrypt(i,bValue ? "'Y'" : "'N'"));
				}
				else if (value instanceof Timestamp)
					sqlValues.append (DB.TO_DATE ((Timestamp)encrypt(i,value), p_info.getColumnDisplayType (i) == DisplayType.Date));
				else if (c == String.class)
					sqlValues.append (encrypt(i,DB.TO_STRING ((String)value)));
				else if (DisplayType.isLOB(dt))
					sqlValues.append("null");		//	no db dependent stuff here
				else
					sqlValues.append (saveNewSpecial (value, i));
			}
			catch (Exception e)
			{
				String msg = "";
				if (m_trxName != null)
					msg = "[" + m_trxName + "] - ";
				msg += p_info.toString(i) 
					+ " - Value=" + value 
					+ "(" + (value==null ? "null" : value.getClass().getName()) + ")";
				log.log(Level.SEVERE, msg, e);
				throw new DBException(e);	//	fini
			}

			//	Change Log 
			if (session != null
				&& !p_info.isEncrypted(i)		//	not encrypted
				&& !p_info.isVirtualColumn(i)	//	no virtual column
				&& !"Password".equals(columnName)
				)
			{
				Object keyInfo = get_ID();
				if (m_IDs.length != 1)
					keyInfo = get_WhereClause(true);
				MChangeLog cLog = session.changeLog (
					m_trxName, AD_ChangeLog_ID, 
					p_info.getAD_Table_ID(), p_info.getColumn(i).AD_Column_ID, 
					keyInfo, getAD_Client_ID(), getAD_Org_ID(), null, value,
					get_TableName(), MChangeLog.CHANGELOGTYPE_Insert);
				if (cLog != null)
					AD_ChangeLog_ID = cLog.getAD_ChangeLog_ID();
			}
		}
		//	Custom Columns
		int index;
		if (m_custom != null)
		{
			Iterator<String> it = m_custom.keySet().iterator();
			while (it.hasNext())
			{
				String columnName = it.next();
				index = p_info.getColumnIndex(columnName);
				String value = (String)m_custom.get(columnName);
				if (value == null || value.equals(Null.NULLString))
					continue;
				if (doComma)
				{
					sqlInsert.append(",");
					sqlValues.append(",");
				}
				else
					doComma = true;
				sqlInsert.append(columnName);
				String value2 = DB.TO_STRING(encrypt(index, value));
				sqlValues.append(value2);

				//	Change Log 
				if (session != null
					&& !p_info.isEncrypted(index)		//	not encrypted
					&& !p_info.isVirtualColumn(index)	//	no virtual column
					&& !"Password".equals(columnName)
					)
				{
					MChangeLog cLog = session.changeLog (
						m_trxName, AD_ChangeLog_ID, 
						p_info.getAD_Table_ID(), p_info.getColumn(index).AD_Column_ID, 
						m_IDs, getAD_Client_ID(), getAD_Org_ID(), null, value,
						get_TableName(), MChangeLog.CHANGELOGTYPE_Insert);
					if (cLog != null)
						AD_ChangeLog_ID = cLog.getAD_ChangeLog_ID();
				}
			}
			m_custom = null;
		}
		sqlInsert.append(sqlValues)
			.append(")");
		//
		int no = DB.executeUpdate(sqlInsert.toString(), m_trxName);
		boolean ok = (no == 1);
		if (ok)
		{
			ok = lobSave();
			if (!load(m_trxName))		//	re-read Info
			{
				if (m_trxName == null)
					log.log(Level.SEVERE, "reloading");
				else
					log.log(Level.SEVERE, "[" + m_trxName + "] - reloading");
				ok = false;;
			}
		}
		else
		{
			String msg = "Not inserted - ";
			if (CLogMgt.isLevelFiner())
				msg += sqlInsert.toString();
			else
				msg += get_TableName();
			if (m_trxName == null)
				log.log(Level.WARNING, msg);
			else
				log.log(Level.WARNING, "[" + m_trxName + "]" + msg);
		}
		return ok;
	}	//	saveNewInsertSQL

	/**
	 * 	Get ID for new record during save.
	 * 	You can overwite this to explicitly set the ID
	 *	@return ID to be used or 0 for fedault logic
	 */
	protected int saveNew_getID()
	{
		return 0;
	}	//	saveNew_getID

	
	/**
	 * 	Create Single/Multi Key Where Clause
	 * 	@param withValues if true uses actual values otherwise ?
	 * 	@return where clause
	 */
	public String get_WhereClause (boolean withValues)
	{
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < m_IDs.length; i++)
		{
			if (i != 0)
				sb.append(" AND ");
			sb.append(m_KeyColumns[i]).append("=");
			if (withValues)
			{
				if (m_KeyColumns[i].endsWith("_ID"))
					sb.append(m_IDs[i]);
				else
					sb.append("'").append(m_IDs[i]).append("'");
			}
			else
				sb.append("?");
		}
		return sb.toString();
	}	//	getWhereClause


	/**
	 *  Save Special Data.
	 *  To be extended by sub-classes
	 *  @param value value
	 *  @param index index
	 *  @return SQL code for INSERT VALUES clause
	 */
	protected String saveNewSpecial (Object value, int index)
	{
		String colName = p_info.getColumnName(index);
		String colClass = p_info.getColumnClass(index).toString();
		String colValue = value == null ? "null" : value.getClass().toString();
	//	int dt = p_info.getColumnDisplayType(index);

		log.log(Level.SEVERE, "Unknown class for column " + colName
			+ " (" + colClass + ") - Value=" + colValue);
			
		if (value == null)
			return "NULL";
		return value.toString();
	}   //  saveNewSpecial

	/**
	 * 	Encrypt data.
	 * 	Not: LOB, special values/Objects
	 *	@param index index
	 *	@param xx data
	 *	@return xx
	 */
	private Object encrypt (int index, Object xx)
	{
		if (xx == null)
			return null;
		if (index != -1 && p_info.isEncrypted(index))
			return SecureEngine.encrypt(xx);
		return xx;
	}	//	encrypt
	
	/**
	 * 	Encrypt data.
	 * 	Not: LOB, special values/Objects
	 *	@param index index
	 *	@param xx data
	 *	@return xx
	 */
	private String encrypt (int index, String xx)
	{
		if (xx == null)
			return null;
		if (index != -1 && p_info.isEncrypted(index))
			return SecureEngine.encrypt(xx);
		return xx;
	}	//	encrypt

	/**
	 * 	Decrypt data
	 *	@param index index
	 *	@param yy data
	 *	@return yy
	 */
	private Object decrypt (int index, Object yy)
	{
		if (yy == null)
			return null;
		if (index != -1 && p_info.isEncrypted(index))
			return SecureEngine.decrypt(yy);
		return yy;
	}	//	decrypt
	
	/**************************************************************************
	 * 	Delete Current Record
	 * 	@param force delete also processed records
	 * 	@return true if deleted
	 */
	public boolean delete (boolean force)
	{
		CLogger.resetLast();
		if (is_new())
			return true;
		
		int AD_Table_ID = p_info.getAD_Table_ID();
		int Record_ID = get_ID();
		
		if (!force)
		{
			int iProcessed = get_ColumnIndex("Processed");
			if  (iProcessed != -1)
			{
				Boolean processed = (Boolean)get_Value(iProcessed);
				if (processed != null && processed.booleanValue())
				{
					log.warning("Record processed");	//	CannotDeleteTrx
					log.saveError("Processed", "Processed", false);
					return false;
				}
			}	//	processed
		}	//	force
		
		try
		{
			if (!beforeDelete())
			{
				log.warning("beforeDelete failed");
				return false;
			}
		}
		catch (Exception e)
		{
			log.log(Level.WARNING, "beforeDelete", e);
			log.saveError("Error", e.toString(), false);
		//	throw new DBException(e);
			return false;
		}
		//	Delete Restrict AD_Table_ID/Record_ID (Requests, ..)
		String errorMsg = PO_Record.exists(AD_Table_ID, Record_ID, m_trxName);
		if (errorMsg != null)
		{
			log.saveError("CannotDelete", errorMsg);
			return false;
		}
		//
		errorMsg = ModelValidationEngine.get().fireModelChange
			(this, ModelValidator.CHANGETYPE_DELETE);
		if (errorMsg != null)
		{
			log.saveError("Error", errorMsg);
			return false;
		}
		Trx localTrx = null;
		String localTrxName = m_trxName;
		if (localTrxName == null)
		{
			localTrxName = Trx.createTrxName("POdel");
			localTrx = Trx.get(localTrxName, true);
		}
		//
		deleteTranslations(localTrxName);
		//	Delete Cascade AD_Table_ID/Record_ID (Attachments, ..)
		PO_Record.deleteCascade(AD_Table_ID, Record_ID, localTrxName);

		//	The Delete Statement
		StringBuffer sql = new StringBuffer ("DELETE FROM ") //jz why no FROM??
			.append(p_info.getTableName())
			.append(" WHERE ")
			.append(get_WhereClause(true));
		int no = DB.executeUpdate(sql.toString(), localTrxName);
		boolean success = no == 1;

		//	Save ID
		m_idOld = get_ID();
		//
		if (!success)
		{
			log.warning("Not deleted");
			if (localTrx != null)
				localTrx.rollback();
		}
		else
		{
			if (localTrx != null)
				localTrx.commit();
			//	Change Log
			MSession session = MSession.get (p_ctx, false);
			if (session == null)
				log.fine("No Session found");
			else if (session.isWebStoreSession()
				|| MChangeLog.isLogged(AD_Table_ID, MChangeLog.CHANGELOGTYPE_Delete))
			{
				int AD_ChangeLog_ID = 0;
				int size = get_ColumnCount();
				for (int i = 0; i < size; i++)
				{
					Object value = m_oldValues[i];
					if (value != null
						&& !p_info.isEncrypted(i)		//	not encrypted
						&& !p_info.isVirtualColumn(i)	//	no virtual column
						&& !"Password".equals(p_info.getColumnName(i))
						)
					{
						Object keyInfo = get_ID();
						if (m_IDs.length != 1)
							keyInfo = get_WhereClause(true);
						MChangeLog cLog = session.changeLog (
							m_trxName, AD_ChangeLog_ID, 
							AD_Table_ID, p_info.getColumn(i).AD_Column_ID, 
							keyInfo, getAD_Client_ID(), getAD_Org_ID(), value, null,
							get_TableName(), MChangeLog.CHANGELOGTYPE_Delete);
						if (cLog != null)
							AD_ChangeLog_ID = cLog.getAD_ChangeLog_ID();
					}
				}	//   for all fields
			}
			
			//	Housekeeping
			m_IDs[0] = I_ZERO;
			if (m_trxName == null)
				log.fine("complete");
			else
				log.fine("[" + m_trxName + "] - complete");
			m_attachment = null;
		}
		if (localTrx != null)
			localTrx.close();
		localTrx = null;
		updatePreferences();

		try
		{
			success = afterDelete (success);
			if (success)
				deleteTreeNode();
		}
		catch (Exception e)
		{
			log.log(Level.WARNING, "afterDelete", e);
			log.saveError("Error", e.toString(), false);
			success = false;
		//	throw new DBException(e);
		}

		//	Reset
		if (success)
		{
			m_idOld = 0;
			int size = p_info.getColumnCount();
			m_oldValues = new Object[size];
			m_newValues = new Object[size];
			CacheMgt.get().reset(p_info.getTableName());
		}
	//	log.info("" + success);
		return success;
	}	//	delete
	
	/**
	 * 	Delete Current Record
	 * 	@param force delete also processed records
	 *	@param trxName transaction
	 *	@return true if deleted
	 */
	public boolean delete (boolean force, String trxName)
	{
		set_TrxName(trxName);
		return delete (force);
	}	//	delete

	/**
	 * 	Executed before Delete operation.
	 *	@return true if record can be deleted
	 */
	protected boolean beforeDelete ()
	{
	//	log.saveError("Error", Msg.getMsg(getCtx(), "CannotDelete"));
		return true;
	} 	//	beforeDelete

	/**
	 * 	Executed after Delete operation.
	 * 	@param success true if record deleted
	 *	@return true if delete is a success
	 */
	protected boolean afterDelete (boolean success)
	{
		return success;
	} 	//	afterDelete

	
	/**
	 * 	Insert (missing) Translation Records
	 * 	@return false if error (true if no translation or success)
	 */
	private boolean insertTranslations()
	{
		//	Not a translation table
		if (m_IDs.length > 1 
			|| m_IDs[0].equals(I_ZERO)
			|| !p_info.isTranslated()
			|| !(m_IDs[0] instanceof Integer))
			return true;
		//
		StringBuffer iColumns = new StringBuffer();
		StringBuffer sColumns = new StringBuffer();
		for (int i = 0; i < p_info.getColumnCount(); i++)
		{
			if (p_info.isColumnTranslated(i))
			{
				iColumns.append(p_info.getColumnName(i))
					.append(",");
				sColumns.append("t.")
					.append(p_info.getColumnName(i))
					.append(",");
			}
		}
		if (iColumns.length() == 0)
			return true;

		String tableName = p_info.getTableName();
		String keyColumn = m_KeyColumns[0];
		StringBuffer sql = new StringBuffer ("INSERT INTO ")
			.append(tableName).append("_Trl (AD_Language,")
			.append(keyColumn).append(", ")
			.append(iColumns)
			.append(" IsTranslated,AD_Client_ID,AD_Org_ID,Created,CreatedBy,Updated,UpdatedBy) ")
			.append("SELECT l.AD_Language,t.")
			.append(keyColumn).append(", ")
			.append(sColumns)
			.append(" 'N',t.AD_Client_ID,t.AD_Org_ID,t.Created,t.CreatedBy,t.Updated,t.UpdatedBy ")
			.append("FROM AD_Language l, ").append(tableName).append(" t ")
			.append("WHERE l.IsActive='Y' AND l.IsSystemLanguage='Y' AND l.IsBaseLanguage='N' AND t.")
			.append(keyColumn).append("=").append(get_ID())
			.append(" AND NOT EXISTS (SELECT * FROM ").append(tableName)
			.append("_Trl tt WHERE tt.AD_Language=l.AD_Language AND tt.")
			.append(keyColumn).append("=t.").append(keyColumn).append(")");
			/* 	Alternative *
			.append(" AND EXISTS (SELECT * FROM ").append(tableName)
			.append("_Trl tt WHERE tt.AD_Language!=l.AD_Language OR tt.")
			.append(keyColumn).append("!=t.").append(keyColumn).append(")");
			/** */
		int no = DB.executeUpdate(sql.toString(), m_trxName);
		log.fine("#" + no);
		return no > 0;
	}	//	insertTranslations
	
	/**
	 * 	Update Translations.
	 * 	@return false if error (true if no translation or success)
	 */
	private boolean updateTranslations()
	{
		//	Not a translation table
		if (m_IDs.length > 1 
			|| m_IDs[0].equals(I_ZERO)
			|| !p_info.isTranslated()
			|| !(m_IDs[0] instanceof Integer))
			return true;
		//
		boolean trlColumnChanged = false;
		for (int i = 0; i < p_info.getColumnCount(); i++)
		{
			if (p_info.isColumnTranslated(i)
				&& is_ValueChanged(p_info.getColumnName(i)))
			{
				trlColumnChanged = true;
				break;
			}
		}
		if (!trlColumnChanged)
			return true;
		//
		MClient client = MClient.get(getCtx());
		//
		String tableName = p_info.getTableName();
		String keyColumn = m_KeyColumns[0];
		StringBuffer sql = new StringBuffer ("UPDATE ")
			.append(tableName).append("_Trl SET ");
		//
		if (client.isAutoUpdateTrl(tableName))
		{
			for (int i = 0; i < p_info.getColumnCount(); i++)
			{
				if (p_info.isColumnTranslated(i))
				{
					String columnName = p_info.getColumnName(i);
					sql.append(columnName).append("=");
					Object value = get_Value(columnName);
					if (value == null)
						sql.append("NULL");
					else if (value instanceof String)
						sql.append(DB.TO_STRING((String)value));
					else if (value instanceof Boolean)
						sql.append(((Boolean)value).booleanValue() ? "'Y'" : "'N'");
					else if (value instanceof Timestamp)
						sql.append(DB.TO_DATE((Timestamp)value));
					else
						sql.append(value.toString());
					sql.append(",");
				}
			}
			sql.append("IsTranslated='Y'");
		}
		else
			sql.append("IsTranslated='N'");
		//
		sql.append(" WHERE ")
			.append(keyColumn).append("=").append(get_ID());
		int no = DB.executeUpdate(sql.toString(), m_trxName);
		log.fine("#" + no);
		return no >= 0;
	}	//	updateTranslations
	
	/**
	 * 	Delete Translation Records
	 * 	@param trxName transaction
	 * 	@return false if error (true if no translation or success)
	 */
	private boolean deleteTranslations(String trxName)
	{
		//	Not a translation table
		if (m_IDs.length > 1 
			|| m_IDs[0].equals(I_ZERO)
			|| !p_info.isTranslated()
			|| !(m_IDs[0] instanceof Integer))
			return true;
		//
		String tableName = p_info.getTableName();
		String keyColumn = m_KeyColumns[0];
		StringBuffer sql = new StringBuffer ("DELETE FROM ")
			.append(tableName).append("_Trl WHERE ")
			.append(keyColumn).append("=").append(get_ID());
		int no = DB.executeUpdate(sql.toString(), trxName);
		log.fine("#" + no);
		return no >= 0;
	}	//	deleteTranslations

	/**
	 * 	Insert Accounting Records
	 *	@param acctTable accounting sub table
	 *	@param acctBaseTable acct table to get data from
	 *	@param whereClause optional where clause with alisa "p" for acctBaseTable
	 *	@return true if records inserted
	 */
	protected boolean insert_Accounting (String acctTable, 
		String acctBaseTable, String whereClause)
	{
		if (s_acctColumns == null	//	cannot cache C_BP_*_Acct as there are 3
			|| acctTable.startsWith("C_BP_"))
		{
			s_acctColumns = new ArrayList<String>();
			String sql = "SELECT c.ColumnName "
				+ "FROM AD_Column c INNER JOIN AD_Table t ON (c.AD_Table_ID=t.AD_Table_ID) "
				+ "WHERE t.TableName=? AND c.IsActive='Y' AND c.AD_Reference_ID=25 ORDER BY 1";
			PreparedStatement pstmt = null;
			try
			{
				pstmt = DB.prepareStatement (sql, null);
				pstmt.setString (1, acctTable);
				ResultSet rs = pstmt.executeQuery ();
				while (rs.next ())
					s_acctColumns.add (rs.getString(1));
				rs.close ();
				pstmt.close ();
				pstmt = null;
			}
			catch (Exception e)
			{
				log.log(Level.SEVERE, acctTable, e);
			}
			try
			{
				if (pstmt != null)
					pstmt.close ();
				pstmt = null;
			}
			catch (Exception e)
			{
				pstmt = null;
			}
			if (s_acctColumns.size() == 0)
			{
				log.severe ("No Columns for " + acctTable);
				return false;
			}
		}
		
		//	Create SQL Statement - INSERT
		StringBuffer sb = new StringBuffer("INSERT INTO ")
			.append(acctTable)
			.append(" (").append(get_TableName())
			.append("_ID, C_AcctSchema_ID, AD_Client_ID,AD_Org_ID,IsActive, Created,CreatedBy,Updated,UpdatedBy ");
		for (int i = 0; i < s_acctColumns.size(); i++)
			sb.append(",").append(s_acctColumns.get(i));
		//	..	SELECT
		sb.append(") SELECT ").append(get_ID())
			.append(", p.C_AcctSchema_ID, p.AD_Client_ID,0,'Y', SysDate,")
			.append(getUpdatedBy()).append(",SysDate,").append(getUpdatedBy());
		for (int i = 0; i < s_acctColumns.size(); i++)
			sb.append(",p.").append(s_acctColumns.get(i));
		//	.. 	FROM
		sb.append(" FROM ").append(acctBaseTable)
			.append(" p WHERE p.AD_Client_ID=").append(getAD_Client_ID());
		if (whereClause != null && whereClause.length() > 0)
			sb.append (" AND ").append(whereClause);
		sb.append(" AND NOT EXISTS (SELECT * FROM ").append(acctTable)
			.append(" e WHERE e.C_AcctSchema_ID=p.C_AcctSchema_ID AND e.")
			.append(get_TableName()).append("_ID=").append(get_ID()).append(")");
		//
		int no = DB.executeUpdate(sb.toString(), get_TrxName());
		if (no > 0)
			log.fine("#" + no);
		else
			log.warning("#" + no 
				+ " - Table=" + acctTable + " from " + acctBaseTable);
		return no > 0;
	}	//	insert_Accounting
	
	/**
	 * 	Delete Accounting records.
	 * 	NOP - done by database constraints
	 *	@param acctTable accounting sub table
	 *	@return true
	 */
	protected boolean delete_Accounting(String acctTable)
	{
		return true;
	}	//	delete_Accounting

	/**
	 * 	Update Preferences.
	 * 	If record is inactive or deleted
	 */
	private void updatePreferences()
	{
		if (!isActive())
		{
			String keyColumn = get_TableName() + "_ID";
			String keyValue = String.valueOf(get_ID());
			MPreference.delete(keyColumn, keyValue);
			getCtx().deletePreference(keyColumn, keyValue);
		}
	}	//	updatePreferences

	
	/**************************************************************************
	 * 	Insert id data into Tree
	 *	@return true if inserted
	 */
	private boolean insertTreeNode()
	{
		int AD_Table_ID = get_Table_ID();
		if (!MTree.hasTree(AD_Table_ID))
			return false;
		int id = get_ID();
		int AD_Client_ID = getAD_Client_ID();
		String treeTableName = MTree.getNodeTableName(AD_Table_ID);
		int C_Element_ID = 0;
		if (AD_Table_ID == X_C_ElementValue.Table_ID)
		{
			Integer ii = (Integer)get_Value("C_Element_ID");
			if (ii != null)
				C_Element_ID = ii.intValue();
		}
		//
		StringBuffer sb = new StringBuffer ("INSERT INTO ")
			.append(treeTableName)
			.append(" (AD_Client_ID,AD_Org_ID, IsActive,Created,CreatedBy,Updated,UpdatedBy, ")
			.append("AD_Tree_ID, Node_ID, Parent_ID, SeqNo) ")
			//
			.append("SELECT t.AD_Client_ID,0, 'Y', SysDate, 0, SysDate, 0,")
			.append("t.AD_Tree_ID, ").append(id).append(", 0, 999 ")
			.append("FROM AD_Tree t ")
			.append("WHERE t.AD_Client_ID=").append(AD_Client_ID).append(" AND t.IsActive='Y'");
		//	Account Element Value handling
		if (C_Element_ID != 0)
			sb.append(" AND EXISTS (SELECT * FROM C_Element ae WHERE ae.C_Element_ID=")
				.append(C_Element_ID).append(" AND t.AD_Tree_ID=ae.AD_Tree_ID)");
		else	//	std trees
			sb.append(" AND t.IsAllNodes='Y' AND t.AD_Table_ID=").append(AD_Table_ID);
		//	Duplicate Check
		sb.append(" AND NOT EXISTS (SELECT * FROM ").append (treeTableName).append (" e ")
			.append("WHERE e.AD_Tree_ID=t.AD_Tree_ID AND Node_ID=").append(id).append(")");
		//
		int no = DB.executeUpdate(sb.toString(), get_TrxName());
		if (no > 0)
			log.fine("#" + no + " - AD_Table_ID=" + AD_Table_ID);
		else
			log.warning("#" + no + " - AD_Table_ID=" + AD_Table_ID);
		return no > 0;
	}	//	insert_Tree
	
	/**
	 * 	Delete ID Tree Nodes
	 *	@return true if actually deleted (could be non existing) 
	 */
	private boolean deleteTreeNode()
	{
		int id = get_ID();
		if (id == 0)
			id = get_IDOld();
		int AD_Table_ID = get_Table_ID();
		if (!MTree.hasTree(AD_Table_ID))
			return false;
		String treeTableName = MTree.getNodeTableName(AD_Table_ID);
		if (treeTableName == null)
			return false;
		//
		StringBuffer sb = new StringBuffer ("DELETE FROM ")
			.append(treeTableName)
			.append(" n WHERE Node_ID=").append(id)
			.append(" AND EXISTS (SELECT * FROM AD_Tree t ")
			.append("WHERE t.AD_Tree_ID=n.AD_Tree_ID AND t.AD_Table_ID=")
			.append(AD_Table_ID).append(")");
		//
		int no = DB.executeUpdate(sb.toString(), get_TrxName());
		if (no > 0)
			log.fine("#" + no + " - AD_Table_ID=" + AD_Table_ID);
		else
			log.warning("#" + no + " - AD_Table_ID=" + AD_Table_ID);
		return no > 0;
	}	//	delete_Tree
	
	
	/**************************************************************************
	 * 	Lock it.
	 * 	@return true if locked
	 */
	public boolean lock()
	{
		int index = get_ProcessingIndex();
		if (index != -1)
		{
			m_newValues[index] = Boolean.TRUE;		//	direct
			String sql = "UPDATE " + p_info.getTableName()
				+ " SET Processing='Y' WHERE (Processing='N' OR Processing IS NULL) AND " 
				+ get_WhereClause(true);
			boolean success = DB.executeUpdate(sql, null) == 1;	//	outside trx
			if (success)
				log.fine("success");
			else
				log.log(Level.WARNING, "failed");
			return success;
		}
		return false;
	}	//	lock

	/**
	 * 	Get the Column Processing index
	 * 	@return index or -1
	 */
	private int get_ProcessingIndex()
	{
		return p_info.getColumnIndex("Processing");
	}	//	getProcessingIndex

	/**
	 * 	UnLock it
	 * 	@param trxName transaction
	 * 	@return true if unlocked (false only if unlock fails)
	 */
	public boolean unlock (String trxName)
	{
		int index = get_ProcessingIndex();
		if (index != -1)
		{
			m_newValues[index] = Boolean.FALSE;		//	direct
			String sql = "UPDATE " + p_info.getTableName()
				+ " SET Processing='N' WHERE " + get_WhereClause(true);
			boolean success = DB.executeUpdate(sql, trxName) == 1;
			if (success)
				log.fine("success" + (trxName == null ? "" : " [" + trxName + "]"));
			else
				log.log(Level.WARNING, "failed" + (trxName == null ? "" : " [" + trxName + "]"));
			return success;
		}
		return true;
	}	//	unlock

	/**	Optional Transaction		*/
	private String			m_trxName = null;

	/**
	 * 	Set Trx
	 *	@param trxName transaction
	 */
	public void set_TrxName (String trxName)
	{
		m_trxName = trxName;
	}	//	setTrx

	/**
	 * 	Get Trx
	 *	@return transaction
	 */
	public String get_TrxName()
	{
		return m_trxName;
	}	//	getTrx

	
	/**************************************************************************
	 * 	Get Attachments.
	 * 	An attachment may have multiple entries
	 *	@return Attachment or null
	 */
	public MAttachment getAttachment ()
	{
		return getAttachment(false);
	}	//	getAttachment

	/**
	 * 	Get Attachments
	 * 	@param requery requery
	 *	@return Attachment or null
	 */
	public MAttachment getAttachment (boolean requery)
	{
		if (m_attachment == null || requery)
			m_attachment = MAttachment.get (getCtx(), p_info.getAD_Table_ID(), get_ID());
		return m_attachment;
	}	//	getAttachment

	/**
	 * 	Create/return Attachment for PO.
	 * 	If not exist, create new
	 *	@return attachment
	 */
	public MAttachment createAttachment()
	{
		getAttachment (false);
		if (m_attachment == null)
			m_attachment = new MAttachment (getCtx(), p_info.getAD_Table_ID(), get_ID(), null);
		return m_attachment;
	}	//	createAttachment

	
	/**
	 * 	Do we have a Attachment of type
	 * 	@param extension extension e.g. .pdf
	 * 	@return true if there is a attachment of type
	 */
	public boolean isAttachment (String extension)
	{
		getAttachment (false);
		if (m_attachment == null)
			return false;
		for (int i = 0; i < m_attachment.getEntryCount(); i++)
		{
			if (m_attachment.getEntryName(i).endsWith(extension))
			{
				log.fine("#" + i + ": " + m_attachment.getEntryName(i));
				return true;
			}
		}
		return false;
	}	//	isAttachment

	/**
	 * 	Get Attachment Data of type
	 * 	@param extension extension e.g. .pdf
	 *	@return data or null
	 */
	public byte[] getAttachmentData (String extension)
	{
		getAttachment(false);
		if (m_attachment == null)
			return null;
		for (int i = 0; i < m_attachment.getEntryCount(); i++)
		{
			if (m_attachment.getEntryName(i).endsWith(extension))
			{
				log.fine("#" + i + ": " + m_attachment.getEntryName(i));
				return m_attachment.getEntryData(i);
			}
		}
		return null;
	}	//	getAttachmentData

	/**
	 * 	Do we have a PDF Attachment
	 * 	@return true if there is a PDF attachment
	 */
	public boolean isPdfAttachment()
	{
		return isAttachment(".pdf");
	}	//	isPdfAttachment

	/**
	 * 	Get PDF Attachment Data
	 *	@return data or null
	 */
	public byte[] getPdfAttachment()
	{
		return getAttachmentData(".pdf");
	}	//	getPDFAttachment
	
	
	/**************************************************************************
	 *  Dump Record
	 */
	public void dump ()
	{
		if (CLogMgt.isLevelFinest())
		{
			log.finer(get_WhereClause (true));
			for (int i = 0; i < get_ColumnCount (); i++)
				dump (i);
		}
	}   //  dump

	/**
	 *  Dump column
	 *  @param index index
	 */
	public void dump (int index)
	{
		StringBuffer sb = new StringBuffer(" ").append(index);
		if (index < 0 || index >= get_ColumnCount())
		{
			log.finest(sb.append(": invalid").toString());
			return;
		}
		sb.append(": ").append(get_ColumnName(index))
			.append(" = ").append(m_oldValues[index])
			.append(" (").append(m_newValues[index]).append(")");
		log.finest(sb.toString());
	}   //  dump

	
	/*************************************************************************
	 * 	Get All IDs of Table.
	 * 	Used for listing all Entities
	 * 	<code>
	 	int[] IDs = PO.getAllIDs ("AD_PrintFont", null);
		for (int i = 0; i < IDs.length; i++)
		{
			pf = new MPrintFont(Env.getCtx(), IDs[i]);
			System.out.println(IDs[i] + " = " + pf.getFont());
		}
	 *	</code>
	 * 	@param TableName table name (key column with _ID)
	 * 	@param WhereClause optional where clause
	 * 	@return array of IDs or null
	 * 	@param trxName transaction
	 */
	public static int[] getAllIDs (String TableName, String WhereClause, String trxName)
	{
		ArrayList<Integer> list = new ArrayList<Integer>();
		StringBuffer sql = new StringBuffer("SELECT ");
		sql.append(TableName).append("_ID FROM ").append(TableName);
		if (WhereClause != null && WhereClause.length() > 0)
			sql.append(" WHERE ").append(WhereClause);
		try
		{
			PreparedStatement pstmt = DB.prepareStatement(sql.toString(), trxName);
			ResultSet rs = pstmt.executeQuery();
			while (rs.next())
				list.add(new Integer(rs.getInt(1)));
			rs.close();
			pstmt.close();
		}
		catch (SQLException e)
		{
			s_log.log(Level.SEVERE, sql.toString(), e);
			return null;
		}
		//	Convert to array
		int[] retValue = new int[list.size()];
		for (int i = 0; i < retValue.length; i++)
			retValue[i] = ((Integer)list.get(i)).intValue();
		return retValue;
	}	//	getAllIDs


	/**
	 * 	Get Find parameter.
	 * 	Convert to upper case and add % at the end
	 *	@param query in string
	 *	@return out string
	 */
	protected static String getFindParameter (String query)
	{
		if (query == null)
			return null;
		if (query.length() == 0 || query.equals("%"))
			return null;
		if (!query.endsWith("%"))
			query += "%";
		return query.toUpperCase();
	}	//	getFindParameter

	
	/**************************************************************************
	 * 	Load LOB
	 * 	@param value LOB
	 * 	@return object
	 */
	private Object get_LOB (Object value)
	{
		log.fine("Value=" + value);
		if (value == null)
			return null;
		//
		Object retValue = null;
		long length = -99;
		try
		{
			if (value instanceof Clob)		//	returns String
			{
				Clob clob = (Clob)value;
				length = clob.length();
				retValue = clob.getSubString(1, (int)length);
			}
			else if (value instanceof Blob)	//	returns byte[]
			{
				Blob blob = (Blob)value;
				length = blob.length();
				int index = 1;	//	correct
				if (blob.getClass().getName().equals("oracle.jdbc.rowset.OracleSerialBlob"))
					index = 0;	//	Oracle Bug Invalid Arguments
								//	at oracle.jdbc.rowset.OracleSerialBlob.getBytes(OracleSerialBlob.java:130)
				retValue = blob.getBytes(index, (int)length);
			}
			else if (value instanceof String)
			{
				retValue = value;
			}
			else if (value instanceof byte[])	//EDB returns byte[] for blob
			{
				retValue = value;
			}
			else
				log.log(Level.SEVERE, "Unknown: " + value);
		}
		catch (Exception e)
		{
			log.log(Level.SEVERE, "Length=" + length, e);
		}
		return retValue;
	}	//	getLOB
	
	/**	LOB Info				*/
	private ArrayList<PO_LOB>	m_lobInfo = null;

	/**
	 * 	Reset LOB info
	 */
	private void lobReset()
	{
		m_lobInfo = null;
	}	//	resetLOB
	
	/**
	 * 	Prepare LOB save
	 *	@param value value 
	 *	@param index index
	 *	@param displayType display type
	 */	
	private void lobAdd (Object value, int index, int displayType)
	{
		log.finest("Value=" + value);
		PO_LOB lob = new PO_LOB (p_info.getTableName(), get_ColumnName(index), 
			get_WhereClause(true), displayType, value);
		if (m_lobInfo == null)
			m_lobInfo = new ArrayList<PO_LOB>();
		m_lobInfo.add(lob);
	}	//	lobAdd
	
	/**
	 * 	Save LOB
	 * 	@return true if saved or ok
	 */
	private boolean lobSave ()
	{
		if (m_lobInfo == null)
			return true;
		boolean retValue = true;
		for (int i = 0; i < m_lobInfo.size(); i++)
		{
			PO_LOB lob = (PO_LOB)m_lobInfo.get(i);
			if (!lob.save(get_TrxName()))
			{
				retValue = false;
				break;
			}
		}	//	for all LOBs
		lobReset();
		return retValue;
	}	//	saveLOB
	
	/**
	 * 	Get Object xml representation as string
	 *	@param xml optional string buffer
	 *	@return updated/new string buffer header is only added once
	 */
	public StringBuffer get_xmlString (StringBuffer xml)
	{
		if (xml == null)
			xml = new StringBuffer();
		else
			xml.append(Env.NL);
		//
		try
		{
			StringWriter writer = new StringWriter();
			StreamResult result = new StreamResult(writer);
			DOMSource source = new DOMSource(get_xmlDocument(xml.length()!=0));
			TransformerFactory tFactory = TransformerFactory.newInstance();
			Transformer transformer = tFactory.newTransformer();
			transformer.transform (source, result);
			StringBuffer newXML = writer.getBuffer();
			//
			if (xml.length() != 0)
			{	//	//	<?xml version="1.0" encoding="UTF-8"?>
				int tagIndex = newXML.indexOf("?>");
				if (tagIndex != -1)
					xml.append(newXML.substring(tagIndex+2));
				else
					xml.append(newXML);
			}
			else
				xml.append(newXML);
		}
		catch (Exception e)
		{
			log.log(Level.SEVERE, "", e);
		}
		return xml;
	}	//	get_xmlString
	
	/** Table ID Attribute		*/
	protected final static String 	XML_ATTRIBUTE_AD_Table_ID = "AD_Table_ID";
	/** Record ID Attribute		*/
	protected final static String 	XML_ATTRIBUTE_Record_ID = "Record_ID";

	/**
	 * 	Get XML Document representation
	 * 	@param noComment do not add comment
	 * 	@return XML document
	 */
	public Document get_xmlDocument(boolean noComment)
	{
		Document document = null;
		try
		{
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			document = builder.newDocument();
			if (!noComment)
				document.appendChild(document.createComment(Compiere.getSummaryAscii()));
		}
		catch (Exception e)
		{
			log.log(Level.SEVERE, "", e);
		}
		//	Root
		Element root = document.createElement(get_TableName());
		root.setAttribute(XML_ATTRIBUTE_AD_Table_ID, String.valueOf(get_Table_ID()));
		root.setAttribute(XML_ATTRIBUTE_Record_ID, String.valueOf(get_ID()));
		document.appendChild(root);
		//	Columns
		int size = get_ColumnCount();
		for (int i = 0; i < size; i++)
		{
			if (p_info.isVirtualColumn(i))
				continue;
			
			Element col = document.createElement(p_info.getColumnName(i));
			//
			Object value = get_Value(i);
			//	Display Type
			int dt = p_info.getColumnDisplayType(i);
			//  Based on class of definition, not class of value
			Class<?> c = p_info.getColumnClass(i);
			if (value == null || value.equals (Null.NULL))
				;
			else if (c == Object.class)
				col.appendChild(document.createCDATASection(value.toString()));
			else if (value instanceof Integer || value instanceof BigDecimal)
				col.appendChild(document.createTextNode(value.toString()));
			else if (c == Boolean.class)
			{
				boolean bValue = false;
				if (value instanceof Boolean)
					bValue = ((Boolean)value).booleanValue();
				else
					bValue = "Y".equals(value);
				col.appendChild(document.createTextNode(bValue ? "Y" : "N"));
			}
			else if (value instanceof Timestamp)
				col.appendChild(document.createTextNode(value.toString()));
			else if (c == String.class)
				col.appendChild(document.createCDATASection((String)value));
			else if (DisplayType.isLOB(dt))
				col.appendChild(document.createCDATASection(value.toString()));
			else
				col.appendChild(document.createCDATASection(value.toString()));
			//
			root.appendChild(col);
		}
		//	Custom Columns
		if (m_custom != null)
		{
			Iterator<String> it = m_custom.keySet().iterator();
			while (it.hasNext())
			{
				String columnName = it.next();
			//	int index = p_info.getColumnIndex(columnName);
				String value = (String)m_custom.get(columnName);
				//
				Element col = document.createElement(columnName);
				if (value != null)
					col.appendChild(document.createTextNode(value));
				root.appendChild(col);
			}
			m_custom = null;
		}
		return document;
	}	//	getDocument
	
	/**
	 * 	De-serialized object by setting the correct logger
	 * 	@param s ObjectInputStream 
	 */
	private void readObject(ObjectInputStream s)
		throws IOException, ClassNotFoundException
	{
		s.defaultReadObject();
		log = CLogger.getCLogger (getClass());
	}  // readObject
	

	
	
	/**
	 * Sets the context in p_changeVO, if p_changeVO is not null
	 * 
	 * @param windowNo
	 * @param columnName
	 * @param value
	 */
	protected void setContext( int windowNo, String columnName, String value )
	{
		// Charges - Set Context
		if( p_changeVO != null )
			p_changeVO.setContext( getCtx(), windowNo, columnName, value );
	}	
	

	/**
	 * Added the message text to p_changeVO if p_changeVO is not null. The
	 * message should have already been translated.
	 * 
	 * @param message
	 */
	protected void addError( String message )
	{
		if( p_changeVO != null )
			p_changeVO.addError( message );
	}
	
	
}   //  PO
