/******************************************************************************
 * 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.model;

import java.beans.*;
import java.io.*;
import java.sql.*;
import java.text.*;
import java.util.*;
import java.util.logging.*;

import javax.swing.event.*;

import org.compiere.controller.*;
import org.compiere.framework.*;
import org.compiere.util.*;

/**
 *	Tab Model.
 *  - a combination of AD_Tab (the display attributes) and AD_Table information.
 *  <p>
 *  The Tab owns also it's Table model
 *  and listens to data changes to update the Field values.
 *
 *  <p>
 *  The Tab maintains the bound property: CurrentRow
 *
 *  <pre>
 *  Event Hierarchies:
 *      - dataChanged (from MTable)
 *          - setCurrentRow
 *              - Update all Field Values
 *
 *      - setValue
 *          - Update Field Value
 *          - Callout
 *  </pre>
 *  @author 	Jorg Janke
 *  @version 	$Id: GridTab.java,v 1.1 2008/01/16 07:40:54 jrmt Exp $
 */
public class GridTab implements DataStatusListener, Evaluatee, Serializable
{
	/**
	 *	Create Tab (Model) from Value Object.
	 *  <p>
	 *  MTab provides a property listener for changed rows and a
	 *  DataStatusListener for communicating changes of the underlying data
	 *  @param vo Value Object
	 *  @param onlyCurrentDays only current days
	 */
	public GridTab(GridTabVO vo, int onlyCurrentDays)
	{
		m_vo = vo;
		m_vo.onlyCurrentDays = onlyCurrentDays;
		//  Create MTable
		m_mTable = new GridTable (m_vo.ctx, m_vo.AD_Table_ID, m_vo.TableName, m_vo.WindowNo, m_vo.TabNo, true);
		m_mTable.setReadOnly(m_vo.IsReadOnly || m_vo.IsView);
		m_mTable.setDeleteable(m_vo.IsDeleteable);
		//  Load Tab
		if (true) //	vo.TabNo == 0)
			initTab(false);
		else
		{
			m_loader = new Loader();
			m_loader.setPriority(Thread.MIN_PRIORITY);
			m_loader.start();
			waitLoadCompete();
		}
	}	//	GridTab

	/** Value Object                    */
	private GridTabVO          	m_vo;

	/** The Table Model for Query   */
	private GridTable          	m_mTable = null;

	private String 				m_keyColumnName = "";
	private String 				m_linkColumnName = "";
	private String				m_extendedWhere;
	/** Attachments         */
	private HashMap<Integer,Integer>	m_Attachments = null;
	/** Chats				*/
	private HashMap<Integer,Integer>	m_Chats = null;
	/** Locks		        */
	private ArrayList<Integer>	m_Lock = null;

	/** Current Row         */
	private int					m_currentRow = -1;

	/** Property Change     */
	private PropertyChangeSupport m_propertyChangeSupport = new PropertyChangeSupport(this);
	/** Property Change Type    */
	public static final String  PROPERTY = "CurrentRow";
    /** A list of event listeners for this component.	*/
    protected EventListenerList m_listenerList = new EventListenerList();
    /** Current Data Status Event						*/
	private DataStatusEvent 	m_DataStatusEvent = null;
	/**	Query							*/
	private Query 				m_query = new Query();
	private String 				m_oldQuery = "0=9";
	private String 				m_linkValue = "999999";

	/** Order By Array if SortNo 1..3   */
	private String[]	    	m_OrderBys = new String[3];
	/** List of Key Parents     */
	private ArrayList<String>	m_parents = new ArrayList<String>(2);

	/** Map of ColumnName of source field (key) and the dependant field (value) */
	private MultiMap<String,GridField>	m_depOnField = new MultiMap<String,GridField>();

	/** Async Loader            */
	private Loader              m_loader = null;
	/** Async Loading complete  */
	private volatile boolean    m_loadComplete = false;
	/** Is Tab Included in other Tab  */
	private boolean    			m_included = false;

	/**	Logger			*/
	protected CLogger	log = CLogger.getCLogger(getClass());
	
	
	/**************************************************************************
	 *  Tab loader for Tabs > 0
	 */
	class Loader extends Thread
	{
		/**
		 *  Async Loading of Tab > 0
		 */
		public void run()
		{
			initTab (true);
		}   //  run
	}   //  Loader

	/**
	 *  Wait until load is complete
	 */
	private void waitLoadCompete()
	{
		if (m_loadComplete)
			return;
		//
		m_loader.setPriority(Thread.NORM_PRIORITY);
		log.config ("");
		while (m_loader.isAlive())
		{
			try
			{
				Thread.sleep(100);     //  1/10 sec
			}
			catch (Exception e)
			{
				log.log(Level.SEVERE, "", e);
			}
		}
		log.config ("fini");
	}   //  waitLoadComplete

	/**
	 *	Initialize Tab with record from AD_Tab_v
	 *  @param async async
	 *	@return true, if correctly initialized (ignored)
	 */
	protected boolean initTab (boolean async)
	{
		log.fine("#" + m_vo.TabNo + " - Async=" + async + " - Where=" + m_vo.WhereClause);
		m_extendedWhere = m_vo.WhereClause;

		//	Get Field Data
		if (!loadFields())
		{
			m_loadComplete = true;
			return false;
		}

		//  Order By
		m_mTable.setOrderClause(getOrderByClause(m_vo.onlyCurrentDays));

		if (async)
			log.fine("#" + m_vo.TabNo + " - Async=" + async + " - fini");
		m_loadComplete = true;
		return true;
	}	//	initTab

	/**
	 *	Dispose - clean up resources
	 */
	protected void dispose()
	{
		log.fine("#" + m_vo.TabNo);
		m_OrderBys = null;
		//
		m_parents.clear();
		m_parents = null;
		//
		m_mTable.close (true);  //  also disposes Fields
		m_mTable = null;
		//
		m_depOnField.clear();
		m_depOnField = null;
		if (m_Attachments != null)
			m_Attachments.clear();
		m_Attachments = null;
		if (m_Chats != null)
			m_Chats.clear();
		m_Chats = null;
		//
		m_vo.Fields.clear();
		m_vo.Fields = null;
		m_vo = null;
	}	//	dispose


	/**
	 *	Get Field data and add to MTable, if it's required or displayed.
	 *  Reqiored fields are keys, parents, or standard Columns
	 *  @return true if fields loaded
	 */
	private boolean loadFields()
	{
		log.fine("#" + m_vo.TabNo);

		if (m_vo.Fields == null)
			return false;

		//  Add Fields
		for (int f = 0; f < m_vo.Fields.size(); f++)
		{
			GridFieldVO voF = (GridFieldVO)m_vo.Fields.get(f);
			//	Add Fields to Table
			if (voF != null)
			{
				GridField field = new GridField (voF);
				String columnName = field.getColumnName();
				//	Record Info
				if (field.isKey())
					m_keyColumnName = columnName;
				//	Parent Column(s)
				if (field.isParentColumn())
					m_parents.add(columnName);
				//	Order By
				int sortNo = field.getSortNo();
				if (sortNo == 0)
					;
				else if (Math.abs(sortNo) == 1)
				{
					m_OrderBys[0] = columnName;
					if (sortNo < 0)
						m_OrderBys[0] += " DESC";
				}
				else if (Math.abs(sortNo) == 2)
				{
					m_OrderBys[1] = columnName;
					if (sortNo < 0)
						m_OrderBys[1] += " DESC";
				}
				else if (Math.abs(sortNo) == 3)
				{
					m_OrderBys[2] = columnName;
					if (sortNo < 0)
						m_OrderBys[2] += " DESC";
				}
				//  Add field
				m_mTable.addField(field);

				//  List of ColumnNames, this field is dependent on
				ArrayList<String> list = field.getDependentOn();
				for (int i = 0; i < list.size(); i++)
					m_depOnField.put(list.get(i), field);   //  ColumnName, Field
				//  Add fields all fields are dependent on
				if (columnName.equals("IsActive") 
					|| columnName.equals("Processed")
					|| columnName.equals("Processing"))
					m_depOnField.put(columnName, null);
			}
		}   //  for all fields
		
		//  Add Standard Fields
		if (m_mTable.getField("Created") == null)
		{
			GridField created = new GridField (GridFieldVO.createStdField(m_vo.ctx, 
				m_vo.WindowNo, m_vo.TabNo, 
				m_vo.AD_Window_ID, m_vo.AD_Tab_ID, false, true, true));
			m_mTable.addField(created);
		}
		if (m_mTable.getField("CreatedBy") == null)
		{
			GridField createdBy = new GridField (GridFieldVO.createStdField(m_vo.ctx, 
				m_vo.WindowNo, m_vo.TabNo, 
				m_vo.AD_Window_ID, m_vo.AD_Tab_ID, false, true, false));
			m_mTable.addField(createdBy);
		}
		if (m_mTable.getField("Updated") == null)
		{
			GridField updated = new GridField (GridFieldVO.createStdField(m_vo.ctx, 
				m_vo.WindowNo, m_vo.TabNo, 
				m_vo.AD_Window_ID, m_vo.AD_Tab_ID, false, false, true));
			m_mTable.addField(updated);
		}
		if (m_mTable.getField("UpdatedBy") == null)
		{
			GridField updatedBy = new GridField (GridFieldVO.createStdField(m_vo.ctx, 
				m_vo.WindowNo, m_vo.TabNo, 
				m_vo.AD_Window_ID, m_vo.AD_Tab_ID, false, false, false));
			m_mTable.addField(updatedBy);
		}
		return true;
	}	//	loadFields

	/**
	 *  Get a list of variables, this tab is dependent on.
	 *  - for display purposes
	 *  @return ArrayList
	 */
	public ArrayList<String> getDependentOn()
	{
		ArrayList<String> list = new ArrayList<String>();
		//  Display
		Evaluator.parseDepends(list, m_vo.DisplayLogic);
		//
		if (list.size() > 0 && CLogMgt.isLevelFiner())
		{
			StringBuffer sb = new StringBuffer();
			for (int i = 0; i < list.size(); i++)
				sb.append(list.get(i)).append(" ");
			log.finer("(" + m_vo.Name + ") " + sb.toString());
		}
		return list;
	}   //  getDependentOn

	/**
	 * 	Get Display Logic
	 *	@return display logic
	 */
	public String getDisplayLogic()
	{
		return m_vo.DisplayLogic;
	}	//	getDisplayLogic
	
	/**
	 *  Get TableModel.
	 *  <B>Do not directly communicate with the table model,
	 *  but through the methods of this class</B>
	 *  @return Table Model
	 */
	public GridTable getTableModel()
	{
		return m_mTable;
	}   //  getTableModel

	/**
	 *  Get Tab Icon
	 *  @return Icon
	 */
	public javax.swing.Icon getIcon()
	{
		if (m_vo.AD_Image_ID == 0)
			return null;
		//
		/** @todo Load Image */
		return null;
	}   //  getIcon

	
	/**************************************************************************
	 *  Has this field dependents ?
	 *  @param columnName column name
	 *  @return true if column has dependent
	 */
	public boolean hasDependants (String columnName)
	{
	//	m_depOnField.printToLog();
		return m_depOnField.containsKey(columnName);
	}   //  isDependentOn

	/**
	 *  Get dependents fields of columnName
	 *  @param columnName column name
	 *  @return ArrayList with GridFields dependent on columnName
	 */
	public ArrayList<GridField> getDependantFields (String columnName)
	{
		return m_depOnField.getValues(columnName);
	}   //  getDependentFields

	
	/**************************************************************************
	 *	Set Query
	 *  @param query query
	 */
	public void setQuery(Query query)
	{
		if (query == null)
			m_query = new Query();
		else
		{
			m_query = query;
			m_vo.onlyCurrentDays = 0;
		}
	}	//	setQuery

	/**
	 *	Get Query
	 *  @return query
	 */
	public Query getQuery()
	{
		return m_query;
	}	//	getQuery

	/**
	 *	Is Query Active
	 *  @return true if query active
	 */
	public boolean isQueryActive()
	{
		if (m_query != null)
			return m_query.isActive();
		return false;
	}	//	isQueryActive

	/**
	 *	Is Query New Record
	 *  @return true if query active
	 */
	public boolean isQueryNewRecord()
	{
		if (m_query != null)
			return m_query.isNewRecordQuery();
		return false;
	}	//	isQueryNewRecord

	/**
	 *  Enable Events - enable data events of tabs (add listeners)
	 */
	public void enableEvents()
	{
		//  Setup Events
		m_mTable.addDataStatusListener(this);
	//	m_mTable.addTableModelListener(this);
	}   //  enableEvents

	/**
	 *	Assemble whereClause and query MTable and position to row 0.
	 *  <pre>
	 *		Scenarios:
	 *		- Never opened 					(full query)
	 *		- query changed 				(full query)
	 *		- Detail link value changed		(full query)
	 *		- otherwise 					(refreshAll)
	 *  </pre>
	 *  @param onlyCurrentDays only current rows
	 */
	public void query (int onlyCurrentDays)
	{
		query (onlyCurrentDays, 0, false);	//	updated
	}	//	query

	/**
	 *	Assemble whereClause and query MTable and position to row 0.
	 *  <pre>
	 *		Scenarios:
	 *		- Never opened 					(full query)
	 *		- query changed 				(full query)
	 *		- Detail link value changed		(full query)
	 *		- otherwise 					(refreshAll)
	 *  </pre>
	 *  @param onlyCurrentDays if only current row, how many days back
	 *  @param maxRows maximum rows or 0 for all
	 *  @param created query based on created if true otherwise updated
	 *  @return true if queried successfully
	 */
	public boolean query (int onlyCurrentDays, int maxRows, boolean created)
	{
		log.fine("#" + m_vo.TabNo 
			+ " - OnlyCurrentDays=" + onlyCurrentDays + ", Detail=" + isDetail());
		boolean success = true;
		//	is it same query?
		boolean refresh = m_oldQuery.equals(m_query.getWhereClause())
			&& m_vo.onlyCurrentDays == onlyCurrentDays;
		m_oldQuery = m_query.getWhereClause();
		m_vo.onlyCurrentDays = onlyCurrentDays;

		/**
		 *	Set Where Clause
		 */
		//	Tab Where Clause
		StringBuffer where = new StringBuffer(m_vo.WhereClause);
		if (m_vo.onlyCurrentDays > 0)
		{
			if (where.length() > 0)
				where.append(" AND ");

			boolean showNotProcessed = findColumn ("Processed") != -1;
			//	Show only unprocessed or the one updated within x days
			if (showNotProcessed)
				where.append("(Processed='N' OR ");
			if (created)
				where.append("Created>=");
			else
				where.append("Updated>=");	
		//	where.append("addDays(current_timestamp, -");
			where.append("addDays(SysDate, -")
				.append(m_vo.onlyCurrentDays).append(")");
			if (showNotProcessed)
				where.append(")");
		}
		//	Detail Query
		if (isDetail())
		{
			String lc = getLinkColumnName();
			if (lc.equals(""))
			{
				log.warning ("No link column");
				if (where.length() > 0)
					where.append(" AND ");
				where.append (" 2=3");
				success = false;
			}
			else
			{
				String value = m_vo.ctx.getContext( m_vo.WindowNo, lc);
				//	Same link value?
				if (refresh)
					refresh = m_linkValue.equals(value);
				m_linkValue = value;
				//	Check validity
				if (value.length() == 0)
				{
					log.warning ("No value for link column " + lc);
					if (where.length() > 0)
						where.append(" AND ");
					where.append (" 2=4");
					success = false;
				}
				else
				{
					//	we have column and value
					if (where.length() > 0)
						where.append(" AND ");
					if ("NULL".equals(value.toUpperCase()))
					{
						where.append(lc).append(" IS NULL ");
						log.severe("Null Value of link column " + lc);
					}
					else
					{
						where.append(lc).append("=");
						if (lc.endsWith("_ID"))
							where.append(value);
						else
							where.append("'").append(value).append("'");
					}
				}
			}
		}	//	isDetail

		m_extendedWhere = where.toString();

		//	Final Query
		if (m_query.isActive())
		{
			String q = validateQuery(m_query);
			if (q != null)
			{
				if (where.length() > 0 )
					where.append(" AND ");
				where.append(q);
			}
		}

		/**
		 *	Query
		 */
		log.fine("#" + m_vo.TabNo + " - " + where);
		if (m_mTable.isOpen())
		{
			if (refresh)
				m_mTable.dataRefreshAll();
			else
				m_mTable.dataRequery(where.toString());
		}
		else
		{
			m_mTable.setSelectWhereClause(where.toString());
			m_mTable.open(maxRows);
		}
		//  Go to Record 0
		setCurrentRow(0, true);
		return success;
	}	//	query

	/**
	 * 	Validate Query.
	 *  If query column is not a tab column create EXISTS query
	 * 	@param query query
	 * 	@return where clause
	 */
	private String validateQuery (Query query)
	{
		if (query == null || query.getRestrictionCount() == 0)
			return null;

		//	Check: only one restriction
		if (query.getRestrictionCount() != 1)
		{
			log.fine("Ignored(More than 1 Restriction): " + query);
			return query.getWhereClause();
		}

		String colName = query.getColumnName(0);
		if (colName == null)
		{
			log.fine("Ignored(No Column): " + query);
			return query.getWhereClause();
		}
		//	a '(' in the name = function - don't try to resolve
		if (colName.indexOf('(') != -1)
		{
			log.fine("Ignored(Function): " + colName);
			return query.getWhereClause();
		}
		//	OK - Query is valid 

		//	Zooms to the same Window (Parents, ..)
		String refColName = null;
		if (colName.equals("R_RequestRelated_ID"))
			refColName = "R_Request_ID";
		else if (colName.startsWith("C_DocType"))
			refColName = "C_DocType_ID";
		else if (colName.equals("CreatedBy") || colName.equals("UpdatedBy"))
			refColName = "AD_User_ID";
		else if (colName.equals("Orig_Order_ID"))
			refColName = "C_Order_ID";
		else if (colName.equals("Orig_InOut_ID"))
			refColName = "M_InOut_ID";
		if (refColName != null)
		{
			query.setColumnName(0, refColName);
			if (getField(refColName) != null)
			{
				log.fine("Column " + colName + " replaced with synonym " + refColName);
				return query.getWhereClause();
			}
			refColName = null;
		}
			
		//	Simple Query. 
		if (getField(colName) != null)
		{
			log.fine("Field Found: " + colName);
			return query.getWhereClause();
		}

		//	Find Refernce Column e.g. BillTo_ID -> C_BPartner_Location_ID
		String sql = "SELECT cc.ColumnName "
			+ "FROM AD_Column c"
			+ " INNER JOIN AD_Ref_Table r ON (c.AD_Reference_Value_ID=r.AD_Reference_ID)"
			+ " INNER JOIN AD_Column cc ON (r.Column_Key_ID=cc.AD_Column_ID) "
			+ "WHERE c.AD_Reference_ID IN (18,30)" 	//	Table/Search
			+ " AND c.ColumnName=?";
		try
		{
			PreparedStatement pstmt = DB.prepareStatement(sql, null);
			pstmt.setString(1, colName);
			ResultSet rs = pstmt.executeQuery();
			if (rs.next())
				refColName = rs.getString(1);
			rs.close();
			pstmt.close();
		}
		catch (SQLException e)
		{
			log.log(Level.SEVERE, "(ref) - Column=" + colName, e);
			return query.getWhereClause();
		}
		//	Reference Column found
		if (refColName != null)
		{
			query.setColumnName(0, refColName);
			if (getField(refColName) != null)
			{
				log.fine("Column " + colName + " replaced with " + refColName);
				return query.getWhereClause();
			}
			colName = refColName;
		}

		//	Column NOT in Tab - create EXISTS subquery
		String tableName = null;
		String tabKeyColumn = getKeyColumnName();
		//	Column=SalesRep_ID, Key=AD_User_ID, Query=SalesRep_ID=101

		sql = "SELECT t.TableName "
			+ "FROM AD_Column c"
			+ " INNER JOIN AD_Table t ON (c.AD_Table_ID=t.AD_Table_ID) "
			+ "WHERE c.ColumnName=? AND IsKey='Y'"		//	#1 Link Column
			+ " AND EXISTS (SELECT * FROM AD_Column cc"
			+ " WHERE cc.AD_Table_ID=t.AD_Table_ID AND cc.ColumnName=?)";	//	#2 Tab Key Column
		try
		{
			PreparedStatement pstmt = DB.prepareStatement(sql, null);
			pstmt.setString(1, colName);
			pstmt.setString(2, tabKeyColumn);
			ResultSet rs = pstmt.executeQuery();
			if (rs.next())
				tableName = rs.getString(1);
			rs.close();
			pstmt.close();
		}
		catch (SQLException e)
		{
			log.log(Level.SEVERE, "Column=" + colName + ", Key=" + tabKeyColumn, e);
			return null;
		}
		
		//	Special Reference Handling
		if (tabKeyColumn.equals("AD_Reference_ID"))
		{
			//	Column=AccessLevel, Key=AD_Reference_ID, Query=AccessLevel='6'
			sql = "SELECT AD_Reference_ID FROM AD_Column WHERE ColumnName=?";
			int AD_Reference_ID = DB.getSQLValue(null, sql, colName);
			return "AD_Reference_ID=" + AD_Reference_ID;
		}
		
		//	Causes could be functions in query
		//	e.g. Column=UPPER(Name), Key=AD_Element_ID, Query=UPPER(AD_Element.Name) LIKE '%CUSTOMER%'
		if (tableName == null)
		{
			log.info ("Not successfull - Column=" 
				+ colName + ", Key=" + tabKeyColumn 
				+ ", Query=" + query);
			return query.getWhereClause();
		}

		query.setTableName("xx");
		StringBuffer result = new StringBuffer ("EXISTS (SELECT * FROM ")
			.append(tableName).append(" xx WHERE ")
			.append(query.getWhereClause(true))
			.append(" AND xx.").append(tabKeyColumn).append("=")
			.append(getTableName()).append(".").append(tabKeyColumn).append(")");
		log.fine(result.toString());
		return result.toString();
	}	//	validateQuery


	/**************************************************************************
	 *  Refresh all data
	 */
	public void dataRefreshAll ()
	{
		log.fine("#" + m_vo.TabNo);
		/** @todo does not work with alpha key */
		int keyNo = m_mTable.getKeyID(m_currentRow);
		m_mTable.dataRefreshAll();
		//  Should use RowID - not working for tables with multiple keys
		if (keyNo != -1)
		{
			if (keyNo != m_mTable.getKeyID(m_currentRow))   //  something changed
			{
				int size = getRowCount();
				for (int i = 0; i < size; i++)
				{
					if (keyNo == m_mTable.getKeyID(i))
					{
						m_currentRow = i;
						break;
					}
				}
			}
		}
		setCurrentRow(m_currentRow, true);
	}   //  dataRefreshAll

	/**
	 *  Refresh current row data
	 */
	public void dataRefresh ()
	{
		dataRefresh (m_currentRow);
	}   //  dataRefresh

	/**
	 *  Refresh row data
	 *  @param row index
	 */
	public void dataRefresh (int row)
	{
		log.fine("#" + m_vo.TabNo + " - row=" + row);
		m_mTable.dataRefresh(row);
		setCurrentRow(row, true);
	}   //  dataRefresh

	
	/**************************************************************************
	 *  Uncoditionally Save data
	 *  @param manualCmd if true, no vetoable PropertyChange will be fired for save confirmation from MTable
	 *  @return true if save complete (or nor required)
	 */
	public boolean dataSave(boolean manualCmd)
	{
		log.fine("#" + m_vo.TabNo + " - row=" + m_currentRow);
		try
		{
			boolean retValue = (m_mTable.dataSave(manualCmd) == GridTable.SAVE_OK);
			if (manualCmd)
				setCurrentRow(m_currentRow, false);
			return retValue;
		}
		catch (Exception e)
		{
			log.log(Level.SEVERE, "#" + m_vo.TabNo + " - row=" + m_currentRow, e);
		}
		return false;
	}   //  dataSave


	/**
	 *  Do we need to Save?
	 *  @param rowChange row change
	 *  @param  onlyRealChange if true the value of a field was actually changed
	 *  (e.g. for new records, which have not been changed) - default false
	 *	@return true it needs to be saved
	 */
	public boolean needSave (boolean rowChange, boolean onlyRealChange)
	{
		if (rowChange)
		{
			return m_mTable.needSave(-2, onlyRealChange);
		}
		else
		{
			if (onlyRealChange)
				return m_mTable.needSave();
			else
				return m_mTable.needSave(onlyRealChange);
		}
	}   //  isDataChanged

	/**
	 *  Ignore data changes
	 */
	public void dataIgnore()
	{
		log.fine("#" + m_vo.TabNo);
		m_mTable.dataIgnore();
		setCurrentRow(m_currentRow, false);    //  re-load data
		log.fine("#" + m_vo.TabNo + "- fini");
	}   //  dataIgnore

	
	/**
	 *  Create (copy) new Row
	 *  and process Callouts
	 *  @param copy copy
	 *  @return true if copied/new
	 */
	public boolean dataNew (boolean copy)
	{
		log.fine("#" + m_vo.TabNo);
		if (!isInsertRecord())
		{
			log.warning ("Inset Not allowed in TabNo=" + m_vo.TabNo);
			return false;
		}
		//	Prevent New Where Main Record is processed
		if (m_vo.TabNo > 0)
		{
			boolean processed = "Y".equals(m_vo.ctx.getContext( m_vo.WindowNo, "Processed"));
		//	boolean active = "Y".equals(m_vo.ctx.getContext( m_vo.WindowNo, "IsActive"));
			if (processed)
			{
				log.warning ("Not allowed in TabNo=" + m_vo.TabNo + " -> Processed=" + processed);
				return false;
			}
			log.finest("Processed=" + processed);
		}
		boolean retValue = m_mTable.dataNew (m_currentRow, copy);
		if (!retValue)
			return retValue;
		setCurrentRow(m_currentRow + 1, true);
		//  process all Callouts (no dependency check - assumed that settings are valid)
		for (int i = 0; i < getFieldCount(); i++)
			processCallout(getField(i));
		//  check validity of defaults
		for (int i = 0; i < getFieldCount(); i++)
		{
			getField(i).refreshLookup();
			getField(i).validateValue();
			getField(i).setError(false);
		}
		m_mTable.setChanged(false);
		return retValue;
	}   //  dataNew

	/**
	 *  Delete current Row
	 *  @return true if deleted
	 */
	public boolean dataDelete()
	{
		log.fine("#" + m_vo.TabNo + " - row=" + m_currentRow);
		boolean retValue = m_mTable.dataDelete(m_currentRow);
		setCurrentRow(m_currentRow, true);
		return retValue;
	}   //  dataDelete

	
	/**
	 *	Get Name of Tab
	 *  @return name
	 */
	public String getName()
	{
		return m_vo.Name;
	}	//	getName

	/**
	 *	Get Description of Tab
	 *  @return description
	 */
	public String getDescription()
	{
		return m_vo.Description;
	}	//	getDescription

	/**
	 *	Get Help of Tab
	 *  @return help
	 */
	public String getHelp()
	{
		return m_vo.Help;
	}	//	getHelp

	/**
	 *  Get Tab Level
	 *  @return tab level
	 */
	public int getTabLevel()
	{
		return m_vo.TabLevel;
	}   //  getTabLevel

	/**
	 *  Get Commit Warning
	 *  @return commit warning
	 */
	public String getCommitWarning()
	{
		return m_vo.CommitWarning;
	}   //  getCommitWarning

	/**
	 *	Return Table Model
	 *  @return MTable
	 */
	protected GridTable getMTable()
	{
		return m_mTable;
	}	//	getMTable

	/**
	 *	Return the name of the key column - may be ""
	 *  @return key column name
	 */
	public String getKeyColumnName()
	{
		return m_keyColumnName;
	}	//	getKeyColumnName

	/**
	 *	Return Name of link column
	 *  @return link column name
	 */
	public String getLinkColumnName()
	{
		return m_linkColumnName;
	}	//	getLinkColumnName

	/**
	 *	Set Name of link column.
	 * 	Set from MWindow.loadTabData
	 *  Used in MTab.isCurreny, (.setCurrentRow) .query - APanel.cmd_report
	 *    and MField.isEditable and .isDefault via context
	 *	@param linkColumnName	name of column - or sets name to AD_Column_ID, if exists
	 */
	public void setLinkColumnName (String linkColumnName)
	{
		if (linkColumnName != null)
			m_linkColumnName = linkColumnName;
		else
		{
			if (m_vo.AD_Column_ID == 0)
				return;
			//	we have a link column identified (primary parent column)
			else
			{
				String SQL = "SELECT ColumnName FROM AD_Column WHERE AD_Column_ID=?";
				try
				{
					PreparedStatement pstmt = DB.prepareStatement(SQL, null);
					pstmt.setInt(1, m_vo.AD_Column_ID);		//	Parent Link Column
					ResultSet rs = pstmt.executeQuery();
					if (rs.next())
						m_linkColumnName = rs.getString(1);
					rs.close();
					pstmt.close();
				}
				catch (SQLException e)
				{
					log.log(Level.SEVERE, "", e);
				}
				log.fine("AD_Column_ID=" + m_vo.AD_Column_ID + " - " + m_linkColumnName);
			}
		}
		m_vo.ctx.setContext(m_vo.WindowNo, m_vo.TabNo, "LinkColumnName", m_linkColumnName);
	}	//	setLinkColumnName

	/**
	 *	Is the tab current?.
	 *  <pre>
	 *	Yes 	- Table must be open
	 *			- Query String is the same
	 *			- Not Detail
	 *			- Old link column value is same as current one
	 *  </pre>
	 *  @return true if current
	 */
	public boolean isCurrent()
	{
		//	Open?
		if (!m_mTable.isOpen())
			return false;
		//	Same Query
		if (!m_oldQuery.equals(m_query.getWhereClause()))
			return false;
		//	Detail?
		if (!isDetail())
			return true;
		//	Same link column value
		String value = m_vo.ctx.getContext( m_vo.WindowNo, getLinkColumnName());
		return m_linkValue.equals(value);
	}	//	isCurrent

	/**
	 *	Is the tab/table currently open
	 *  @return true if open
	 */
	public boolean isOpen()
	{
		//	Open?
		if (m_mTable != null)
			return m_mTable.isOpen();
		return false;
	}	//	isCurrent


	/**
	 *  Is Tab Incluced in other Tab
	 *  @return true if included
	 */
	public boolean isIncluded()
	{
		return m_included;
	}   //  isIncluded

	/**
	 *  Is Tab Incluced in other Tab
	 *  @param isIncluded true if included
	 */
	public void setIncluded(boolean isIncluded)
	{
		m_included = isIncluded;
	}   //  setIncluded

	/**
	 * 	Get Only Current Days
	 *	@return history days
	 */
	public int getOnlyCurrentDays()
	{
		return m_vo.onlyCurrentDays;
	}	//	getOnlyCurrentDays
	
	/**
	 *	Return Parent ArrayList
	 *  @return parent column names
	 */
	public ArrayList<String> getParentColumnNames()
	{
		return m_parents;
	}	//	getParentColumnNames

	/**
	 *	Returns true if this is a detail record
	 *  @return true if not parent tab
	 */
	public boolean isDetail()
	{
		//	We have IsParent columns and/or a link column
		if (m_parents.size() > 0 || m_vo.AD_Column_ID != 0)
			return true;
		return false;
	}	//	isDetail

	/**
	 *	Is Printed (Document can be printed)
	 *  @return true if printing
	 */
	public boolean isPrinted()
	{
		return m_vo.AD_Process_ID != 0;
	}	//	isPrinted

	/**
	 *	Get WindowNo
	 *  @return window no
	 */
	public int getWindowNo()
	{
		return m_vo.WindowNo;
	}	//	getWindowNo

	/**
	 *	Get TabNo
	 *  @return tab no
	 */
	public int getTabNo()
	{
		return m_vo.TabNo;
	}	//	getTabNo

	/**
	 *	Get Process ID
	 *  @return Process ID
	 */
	public int getAD_Process_ID()
	{
		return m_vo.AD_Process_ID;
	}	//	getAD_Process_ID

	/**
	 *	Is High Volume?
	 *  @return true if high volume table
	 */
	public boolean isHighVolume()
	{
		return m_vo.IsHighVolume;
	}	//	isHighVolume

	/**
	 *	Is Read Only?
	 *  @return true if read only
	 */
	public boolean isReadOnly()
	{
		if (m_vo.IsReadOnly)
			return true;
		
		//  no restrictions
		if (m_vo.ReadOnlyLogic == null || m_vo.ReadOnlyLogic.equals(""))
			return m_vo.IsReadOnly;

		//  ** dynamic content **  uses get_ValueAsString
		boolean retValue = Evaluator.evaluateLogic(this, m_vo.ReadOnlyLogic);
		log.finest(m_vo.Name 
			+ " (" + m_vo.ReadOnlyLogic + ") => " + retValue);
		return retValue;
	}	//	isReadOnly

	/**
	 * 	Tab contains Always Update Field
	 *	@return true if field with always updateable
	 */
	public boolean isAlwaysUpdateField()
	{
		for (int i = 0; i < m_mTable.getColumnCount(); i++)
		{
			GridField field = m_mTable.getField(i);
			if (field.isAlwaysUpdateable())
				return true;
		}
		return false;
	}	//	isAlwaysUpdateField
	
	/**
	 *	Can we Insert Records?
	 *  @return true not read only and allowed
	 */
	public boolean isInsertRecord()
	{
		if (isReadOnly())
			return false;
		return m_vo.IsInsertRecord;
	}	//	isInsertRecord

	/**
	 *	Is the Tab Visible.
	 *	Called when constructing the window.
	 *	@param initialSetup return false only if not to be displayed
	 *  @return true, if displayed
	 */
	public boolean isDisplayed (boolean initialSetup)
	{
		//  no restrictions
		String dl = m_vo.DisplayLogic; 
		if (dl == null || dl.equals(""))
			return true;

		if (initialSetup)
		{
			if (dl.indexOf("@#") != -1)		//	global variable
			{
				String parsed = Env.parseContext (m_vo.ctx, 0, dl, false, false).trim();
				if (parsed.length() != 0)	//	variable defined
					return Evaluator.evaluateLogic(this, dl);
			}
			return true;
		}
		//
		boolean retValue = Evaluator.evaluateLogic(this, dl);
		log.config(m_vo.Name + " (" + dl + ") => " + retValue);
		return retValue;
	}	//	isDisplayed
	
	/**
	 * 	Get Variable Value (Evaluatee)
	 *	@param variableName name
	 *	@return value
	 */
	public String get_ValueAsString (String variableName)
	{
		return m_vo.ctx.getContext(m_vo.WindowNo, variableName, true);
	}	//	get_ValueAsString

	/**
	 *  Is Single Row
	 *  @return true if single row
	 */
	public boolean isSingleRow()
	{
		return m_vo.IsSingleRow;
	}   //  isSingleRow;

	/**
	 *  Set Single Row.
	 *  Temporary store of current value
	 *  @param isSingleRow toggle
	 */
	public void setSingleRow (boolean isSingleRow)
	{
		m_vo.IsSingleRow = isSingleRow;
	}   //  setSingleRow


	/**
	 *  Has Tree
	 *  @return true if tree exists
	 */
	public boolean isTreeTab()
	{
		return m_vo.HasTree;
	}   //  isTreeTab

	/**
	 *	Get Tab ID
	 *  @return Tab ID
	 */
	public int getAD_Tab_ID()
	{
		return m_vo.AD_Tab_ID;
	}	//	getAD_Tab_ID

	/**
	 *	Get Table ID
	 *  @return Table ID
	 */
	public int getAD_Table_ID()
	{
		return m_vo.AD_Table_ID;
	}	//	getAD_Table_ID

	/**
	 *	Get Window ID
	 *  @return Window ID
	 */
	public int getAD_Window_ID()
	{
		return m_vo.AD_Window_ID;
	}	//	getAD_Window_ID

	/**
	 *	Get Included Tab ID
	 *  @return Included_Tab_ID
	 */
	public int getIncluded_Tab_ID()
	{
		return m_vo.Included_Tab_ID;
	}	//	getIncluded_Tab_ID

	/**
	 *	Get TableName
	 *  @return Table Name
	 */
	public String getTableName()
	{
		return m_vo.TableName;
	}	//	getTableName

	/**
	 *	Get Tab Where Clause
	 *  @return where clause
	 */
	public String getWhereClause()
	{
		return m_vo.WhereClause;
	}	//	getWhereClause

	/**
	 * 	Is Sort Tab
	 * 	@return true if sort tab
	 */
	public boolean isSortTab()
	{
		return m_vo.IsSortTab;
	}	//	isSortTab

	/**
	 * 	Get Order column for sort tab
	 * 	@return AD_Column_ID
	 */
	public int getAD_ColumnSortOrder_ID()
	{
		return m_vo.AD_ColumnSortOrder_ID;
	}	//	getAD_ColumnSortOrder_ID

	/**
	 * 	Get Yes/No column for sort tab
	 * 	@return AD_Column_ID
	 */
	public int getAD_ColumnSortYesNo_ID()
	{
		return m_vo.AD_ColumnSortYesNo_ID;
	}	//	getAD_ColumnSortYesNo_ID

	
	/**************************************************************************
	 *	Get extended Where Clause (parent link)
	 *  @return parent link
	 */
	public String getWhereExtended()
	{
		return m_extendedWhere;
	}	//	getWhereExtended

	/**
	 *	Get Order By Clause
	 *  @param onlyCurrentDays only current rows
	 *  @return Order By Clause
	 */
	private String getOrderByClause(int onlyCurrentDays)
	{
		//	First Prio: Tab Order By
		if (m_vo.OrderByClause.length() > 0)
			return m_vo.OrderByClause;

		//	Second Prio: Fields (save it)
		m_vo.OrderByClause = "";
		for (int i = 0; i < 3; i++)
		{
			String order = m_OrderBys[i];
			if (order != null && order.length() > 0)
			{
				if (m_vo.OrderByClause.length() > 0)
					m_vo.OrderByClause += ",";
				m_vo.OrderByClause += order;
			}
		}
		if (m_vo.OrderByClause.length() > 0)
			return m_vo.OrderByClause;
		
		//	Third Prio: onlyCurrentRows
		m_vo.OrderByClause = "Created";
		if (onlyCurrentDays > 0)
			m_vo.OrderByClause += " DESC";
		return m_vo.OrderByClause;
	}	//	getOrderByClause

	
	/**************************************************************************
	 *	Transaction support.
	 *	Depending on Table returns transaction info
	 *  @return info
	 */
	public String getTrxInfo()
	{
		//	InvoiceBatch
		if (m_vo.TableName.startsWith("C_InvoiceBatch"))
		{
			int Record_ID = m_vo.ctx.getContextAsInt( m_vo.WindowNo, "C_InvoiceBatch_ID");
			log.fine(m_vo.TableName + " - " + Record_ID);
			MessageFormat mf = null;
			try
			{
				mf = new MessageFormat(Msg.getMsg(Env.getAD_Language(m_vo.ctx), "InvoiceBatchSummary"));
			}
			catch (Exception e)
			{
				log.log(Level.SEVERE, "InvoiceBatchSummary=" + Msg.getMsg(Env.getAD_Language(m_vo.ctx), "InvoiceBatchSummary"), e);
			}
			if (mf == null)
				return " ";
			/**********************************************************************
			 *	** Message: ExpenseSummary **
			 *	{0} Line(s) {1,number,#,##0.00}  - Total: {2,number,#,##0.00}
			 *
			 *	{0} - Number of lines
			 *	{1} - Toral
			 *	{2} - Currency
			 */
			Object[] arguments = new Object[3];
			boolean filled = false;
			//
			String sql = "SELECT COUNT(*), NVL(SUM(LineNetAmt),0), NVL(SUM(LineTotalAmt),0) "
				+ "FROM C_InvoiceBatchLine "
				+ "WHERE C_InvoiceBatch_ID=? AND IsActive='Y'";
			//
			try
			{
				PreparedStatement pstmt = DB.prepareStatement(sql, null);
				pstmt.setInt(1, Record_ID);
				ResultSet rs = pstmt.executeQuery();
				if (rs.next())
				{
					//	{0} - Number of lines
					Integer lines = new Integer(rs.getInt(1));
					arguments[0] = lines;
					//	{1} - Line net
					Double net = new Double(rs.getDouble(2));
					arguments[1] = net;
					//	{2} - Line net
					Double total = new Double(rs.getDouble(3));
					arguments[2] = total;
					filled = true;
				}
				rs.close();
				pstmt.close();
			}
			catch (SQLException e)
			{
				log.log(Level.SEVERE, m_vo.TableName + "\nSQL=" + sql, e);
			}
			if (filled)
				return mf.format (arguments);
			return " ";
		}	//	InvoiceBatch

		//	Order || Invoice
		else if (m_vo.TableName.startsWith("C_Order") || m_vo.TableName.startsWith("C_Invoice"))
		{
			int Record_ID;
			boolean isOrder = m_vo.TableName.startsWith("C_Order");
			//
			StringBuffer sql = new StringBuffer("SELECT COUNT(*) AS Lines,c.ISO_Code,o.TotalLines,o.GrandTotal,"
				+ "currencyBase(o.GrandTotal,o.C_Currency_ID,o.DateAcct, o.AD_Client_ID,o.AD_Org_ID) AS ConvAmt ");
			if (isOrder)
			{
				Record_ID = m_vo.ctx.getContextAsInt( m_vo.WindowNo, "C_Order_ID");
				sql.append("FROM C_Order o"
					+ " INNER JOIN C_Currency c ON (o.C_Currency_ID=c.C_Currency_ID)"
					+ " INNER JOIN C_OrderLine l ON (o.C_Order_ID=l.C_Order_ID) "
					+ "WHERE o.C_Order_ID=? ");
			}
			else
			{
				Record_ID = m_vo.ctx.getContextAsInt( m_vo.WindowNo, "C_Invoice_ID");
				sql.append("FROM C_Invoice o"
					+ " INNER JOIN C_Currency c ON (o.C_Currency_ID=c.C_Currency_ID)"
					+ " INNER JOIN C_InvoiceLine l ON (o.C_Invoice_ID=l.C_Invoice_ID) "
					+ "WHERE o.C_Invoice_ID=? ");
			}
			sql.append("GROUP BY o.C_Currency_ID, c.ISO_Code, o.TotalLines, o.GrandTotal, o.DateAcct, o.AD_Client_ID, o.AD_Org_ID");

			log.fine(m_vo.TableName + " - " + Record_ID);
			MessageFormat mf = null;
			MessageFormat mfMC = null;
			try
			{
				mf = new MessageFormat(Msg.getMsg(Env.getAD_Language(m_vo.ctx), "OrderSummary"));
				mfMC = new MessageFormat(Msg.getMsg(Env.getAD_Language(m_vo.ctx), "OrderSummaryMC"));
			}
			catch (Exception e)
			{
				log.log(Level.SEVERE, "OrderSummary/MC", e);
			}
			if (mf == null || mfMC == null)
				return " ";
			/**********************************************************************
			 *	** Message: OrderSummary/MC **
			 *	{0} Line(s) - {1,number,#,##0.00} - Total: {3}{2,number,#,##0.00} = {5}{4,number,#,##0.00}
			 *	{0} Line(s) - {1,number,#,##0.00} - Total: {3}{2,number,#,##0.00}
			 *
			 *	{0} - Number of lines
			 *	{1} - Line toral
			 *	{2} - Grand total (including tax, etc.)
			 *	{3} - Source Currency
			 *	(4) - Grand total converted to local currency
			 *	{5} - Base Currency
			 */
			Object[] arguments = new Object[6];
			boolean filled = false;
			//
			try
			{
				PreparedStatement pstmt = DB.prepareStatement(sql.toString(), null);
				pstmt.setInt(1, Record_ID);
				ResultSet rs = pstmt.executeQuery();
				if (rs.next())
				{
					//	{0} - Number of lines
					Integer lines = new Integer(rs.getInt(1));
					arguments[0] = lines;
					//	{1} - Line toral
					Double lineTotal = new Double(rs.getDouble(3));
					arguments[1] = lineTotal;
					//	{2} - Grand total (including tax, etc.)
					Double grandTotal = new Double(rs.getDouble(4));
					arguments[2] = grandTotal;
					//	{3} - Currency
					String currency = rs.getString(2);
					arguments[3] = currency;
					//	(4) - Grand total converted to Base
					Double grandBase = new Double(rs.getDouble(5));
					arguments[4] = grandBase;
					arguments[5] = m_vo.ctx.getContext("$CurrencyISO");
					filled = true;
				}
				rs.close();
				pstmt.close();
			}
			catch (SQLException e)
			{
				log.log(Level.SEVERE, m_vo.TableName + "\nSQL=" + sql, e);
			}
			if (filled)
			{
				if (arguments[2].equals(arguments[4]))
					return mf.format (arguments);
				else
					return mfMC.format (arguments);
			}
			return " ";
		}	//	Order || Invoice

		//	Expense Report
		else if (m_vo.TableName.startsWith("S_TimeExpense") && m_vo.TabNo == 0)
		{
			int Record_ID = m_vo.ctx.getContextAsInt( m_vo.WindowNo, "S_TimeExpense_ID");
			log.fine(m_vo.TableName + " - " + Record_ID);
			MessageFormat mf = null;
			try
			{
				mf = new MessageFormat(Msg.getMsg(Env.getAD_Language(m_vo.ctx), "ExpenseSummary"));
			}
			catch (Exception e)
			{
				log.log(Level.SEVERE, "ExpenseSummary=" + Msg.getMsg(Env.getAD_Language(m_vo.ctx), "ExpenseSummary"), e);
			}
			if (mf == null)
				return " ";
			/**********************************************************************
			 *	** Message: ExpenseSummary **
			 *	{0} Line(s) - Total: {1,number,#,##0.00} {2}
			 *
			 *	{0} - Number of lines
			 *	{1} - Toral
			 *	{2} - Currency
			 */
			Object[] arguments = new Object[3];
			boolean filled = false;
			//
			String SQL = "SELECT COUNT(*) AS Lines, SUM(ConvertedAmt*Qty) "
				+ "FROM S_TimeExpenseLine "
				+ "WHERE S_TimeExpense_ID=?";

			//
			try
			{
				PreparedStatement pstmt = DB.prepareStatement(SQL, null);
				pstmt.setInt(1, Record_ID);
				ResultSet rs = pstmt.executeQuery();
				if (rs.next())
				{
					//	{0} - Number of lines
					Integer lines = new Integer(rs.getInt(1));
					arguments[0] = lines;
					//	{1} - Line toral
					Double total = new Double(rs.getDouble(2));
					arguments[1] = total;
					//	{3} - Currency
					arguments[2] = " ";
					filled = true;
				}
				rs.close();
				pstmt.close();
			}
			catch (SQLException e)
			{
				log.log(Level.SEVERE, m_vo.TableName + "\nSQL=" + SQL, e);
			}
			if (filled)
				return mf.format (arguments);
			return " ";
		}	//	S_TimeExpense

		
		//	Default - No Trx Info
		return null;
	}	//	getTrxInfo

	/**
	 *  Load Dependant Information
	 */
	private void loadDependentInfo()
	{
		/**
		 * Load Order Type from C_DocTypeTarget_ID
		 */
		if (m_vo.TableName.equals("C_Order"))
		{
			int C_DocTyp_ID = 0;
			Integer target = (Integer)getValue("C_DocTypeTarget_ID");
			if (target != null)
				C_DocTyp_ID = target.intValue();
			if (C_DocTyp_ID == 0)
				return;

			String sql = "SELECT DocSubTypeSO FROM C_DocType WHERE C_DocType_ID=?";
			try
			{
				PreparedStatement pstmt = DB.prepareStatement(sql, null);
				pstmt.setInt(1, C_DocTyp_ID);
				ResultSet rs = pstmt.executeQuery();
				if (rs.next())
					m_vo.ctx.setContext(m_vo.WindowNo, "OrderType", rs.getString(1));
				rs.close();
				pstmt.close();
			}
			catch (SQLException e)
			{
				log.log(Level.SEVERE, sql, e);
			}
		}   //  loadOrderInfo
	}   //  loadDependentInfo

	
	/**************************************************************************
	 *	Load Attachments for this table
	 */
	public void loadAttachments()
	{
		log.fine("#" + m_vo.TabNo);
		if (!canHaveAttachment())
			return;

		String SQL = "SELECT AD_Attachment_ID, Record_ID FROM AD_Attachment "
			+ "WHERE AD_Table_ID=?";
		try
		{
			if (m_Attachments == null)
				m_Attachments = new HashMap<Integer,Integer>();
			else
				m_Attachments.clear();
			PreparedStatement pstmt = DB.prepareStatement(SQL, null);
			pstmt.setInt(1, m_vo.AD_Table_ID);
			ResultSet rs = pstmt.executeQuery();
			while (rs.next())
			{
				Integer key = new Integer(rs.getInt(2));
				Integer value = new Integer(rs.getInt(1));
				m_Attachments.put(key, value);
			}
			rs.close();
			pstmt.close();
		}
		catch (SQLException e)
		{
			log.log(Level.SEVERE, "loadAttachments", e);
		}
		log.config("#" + m_Attachments.size());
	}	//	loadAttachment

	/**
	 *	Can this tab have Attachments?.
	 *  <p>
	 *  It can have an attachment if it has a key column ending with _ID.
	 *  The key column is empty, if there is no single identifying key.
	 *  @return true if record can have attachment
	 */
	public boolean canHaveAttachment()
	{
		if (getKeyColumnName().endsWith("_ID"))
			return true;
		return false;
	}   //	canHaveAttachment

	/**
	 *	Returns true, if current row has an Attachment
	 *  @return true if record has attchment
	 */
	public boolean hasAttachment()
	{
		if (m_Attachments == null)
			loadAttachments();
		if (m_Attachments == null || m_Attachments.isEmpty())
			return false;
		//
		Integer key = new Integer(m_mTable.getKeyID (m_currentRow));
		return m_Attachments.containsKey(key);
	}	//	hasAttachment

	/**
	 *	Get Attachment_ID for current record.
	 *	@return ID or 0, if not found
	 */
	public int getAD_AttachmentID()
	{
		if (m_Attachments == null)
			loadAttachments();
		if (m_Attachments.isEmpty())
			return 0;
		//
		Integer key = new Integer(m_mTable.getKeyID (m_currentRow));
		Integer value = (Integer)m_Attachments.get(key);
		if (value == null)
			return 0;
		else
			return value.intValue();
	}	//	getAttachmentID

	/**************************************************************************
	 *	Load Chats for this table
	 */
	public void loadChats()
	{
		log.fine("#" + m_vo.TabNo);
		if (!canHaveAttachment())
			return;

		String sql = "SELECT CM_Chat_ID, Record_ID FROM CM_Chat "
			+ "WHERE AD_Table_ID=?";
		try
		{
			if (m_Chats == null)
				m_Chats = new HashMap<Integer,Integer>();
			else
				m_Chats.clear();
			PreparedStatement pstmt = DB.prepareStatement(sql, null);
			pstmt.setInt(1, m_vo.AD_Table_ID);
			ResultSet rs = pstmt.executeQuery();
			while (rs.next())
			{
				Integer key = new Integer(rs.getInt(2));	//	Record_ID
				Integer value = new Integer(rs.getInt(1));	//	CM_Chat_ID
				m_Chats.put(key, value);
			}
			rs.close();
			pstmt.close();
		}
		catch (SQLException e)
		{
			log.log(Level.SEVERE, sql, e);
		}
		log.config("#" + m_Chats.size());
	}	//	loadChats

	/**
	 *	Returns true, if current row has a Chat
	 *  @return true if record has chat
	 */
	public boolean hasChat()
	{
		if (m_Chats == null)
			loadChats();
		if (m_Chats == null || m_Chats.isEmpty())
			return false;
		//
		Integer key = new Integer(m_mTable.getKeyID (m_currentRow));
		return m_Chats.containsKey(key);
	}	//	hasChat

	/**
	 *	Get Chat_ID for this record.
	 *	@return ID or 0, if not found
	 */
	public int getCM_ChatID()
	{
		if (m_Chats == null)
			loadChats();
		if (m_Chats.isEmpty())
			return 0;
		//
		Integer key = new Integer(m_mTable.getKeyID (m_currentRow));
		Integer value = (Integer)m_Chats.get(key);
		if (value == null)
			return 0;
		else
			return value.intValue();
	}	//	getCM_ChatID


	/**************************************************************************
	 * 	Load Locks for Table and User
	 */	
	public void loadLocks()
	{
		int AD_User_ID = Env.getCtx().getAD_User_ID();
		log.fine("#" + m_vo.TabNo + " - AD_User_ID=" + AD_User_ID);
		if (!canHaveAttachment())
			return;

		String sql = "SELECT Record_ID "
			+ "FROM AD_Private_Access "
			+ "WHERE AD_User_ID=? AND AD_Table_ID=? AND IsActive='Y' " 
			+ "ORDER BY Record_ID";
		try
		{
			if (m_Lock == null)
				m_Lock = new ArrayList<Integer>();
			else
				m_Lock.clear();
			PreparedStatement pstmt = DB.prepareStatement(sql, null);
			pstmt.setInt(1, AD_User_ID);
			pstmt.setInt(2, m_vo.AD_Table_ID);
			ResultSet rs = pstmt.executeQuery();
			while (rs.next())
			{
				Integer key = new Integer(rs.getInt(1));
				m_Lock.add(key);
			}
			rs.close();
			pstmt.close();
		}
		catch (SQLException e)
		{
			log.log(Level.SEVERE, sql, e);
		}
		log.fine("#" + m_Lock.size());
	}	//	loadLooks
	
	/**
	 * 	Record Is Locked
	 * 	@return true if locked
	 */
	public boolean isLocked()
	{
		if (!MRole.getDefault(m_vo.ctx, false).isPersonalLock())
			return false;
		if (m_Lock == null)
			loadLocks();
		if (m_Lock == null || m_Lock.isEmpty())
			return false;
		//
		Integer key = new Integer(m_mTable.getKeyID (m_currentRow));
		return m_Lock.contains(key);
	}	//	isLocked
	
	/**
	 * 	Lock Record
	 * 	@param ctx context
	 *	@param Record_ID id
	 *	@param lock true if lock, otherwise unlock
	 */
	public void lock (Ctx ctx, int Record_ID, boolean lock)
	{
		int AD_User_ID = ctx.getAD_User_ID();
		log.fine("Lock=" + lock + ", AD_User_ID=" + AD_User_ID 
			+ ", AD_Table_ID=" + m_vo.AD_Table_ID + ", Record_ID=" + Record_ID);
		MPrivateAccess access = MPrivateAccess.get (ctx, AD_User_ID, m_vo.AD_Table_ID, Record_ID);
		if (access == null)
			access = new MPrivateAccess (ctx, AD_User_ID, m_vo.AD_Table_ID, Record_ID);
		access.setIsActive(lock);
		access.save();
		//
		loadLocks();
	}	//	lock

	
	/**************************************************************************
	 *	Data Status Listener from MTable.
	 *  - get raw info and add current row information
	 *  - update the current row
	 *  - redistribute (fire) Data Status event
	 *  @param e event
	 */
	public void dataStatusChanged (DataStatusEvent e)
	{
		log.fine("#" + m_vo.TabNo + " - " + e.toString());
		int oldCurrentRow = e.getCurrentRow();
		m_DataStatusEvent = e;          //  save it
		//  when sorted set current row to 0
		String msg = m_DataStatusEvent.getAD_Message();
		if (msg != null && msg.equals("Sorted"))
			setCurrentRow(0, true);
		//  set current row
		m_DataStatusEvent.setCurrentRow(m_currentRow);
		//  Same row - update value
		if (oldCurrentRow == m_currentRow)
		{
			GridField field = m_mTable.getField(e.getChangedColumn());
			if (field != null)
			{
				Object value = m_mTable.getValueAt(m_currentRow, e.getChangedColumn());
				field.setValue(value, m_mTable.isInserting());
			}
		}
		else    //  Redistribute Info with current row info
			fireDataStatusChanged(m_DataStatusEvent);
	//	log.fine("dataStatusChanged #" + m_vo.TabNo + "- fini", e.toString());
	}	//	dataStatusChanged

	/**
	 *	Inform Listeners and build WHO info
	 *  @param e event
	 */
	private void fireDataStatusChanged (DataStatusEvent e)
	{
		DataStatusListener[] listeners = m_listenerList.getListeners(DataStatusListener.class);
		if (listeners.length == 0)
			return;
		log.fine(e.toString());
		//  WHO Info
		if (e.getCurrentRow() >= 0)
		{
			e.Created = (Timestamp)getValue("Created");
			e.CreatedBy = (Integer)getValue("CreatedBy");
			e.Updated = (Timestamp)getValue("Updated");
			e.UpdatedBy = (Integer)getValue("UpdatedBy");
			e.Record_ID = getValue(m_keyColumnName);
			//  Info
			StringBuffer info = new StringBuffer(getTableName());
			//  We have a key column
			if (m_keyColumnName != null && m_keyColumnName.length() > 0)
			{
				info.append(" - ")
					.append(m_keyColumnName).append("=").append(e.Record_ID);
			}
			else    //  we have multiple parents
			{
				for (int i = 0; i < m_parents.size(); i++)
				{
					String keyCol = (String)m_parents.get(i);
					info.append(" - ")
						.append(keyCol).append("=").append(getValue(keyCol));
				}
			}
			e.Info = info.toString();
		}
		e.setInserting(m_mTable.isInserting());
		//  Distribute/fire it
        for (int i = 0; i < listeners.length; i++) 
        	listeners[i].dataStatusChanged(e);
	//	log.fine("fini - " + e.toString());
	}	//	fireDataStatusChanged

	/**
	 *  Create and fire Data Status Error Event
	 *  @param AD_Message message
	 *  @param info info
	 *  @param isError if not true, it is a Warning
	 */
	protected void fireDataStatusEEvent(String AD_Message, String info, boolean isError)
	{
		m_mTable.fireDataStatusEEvent(AD_Message, info, isError);
	}   //  fireDataStatusEvent

	/**
	 *  Create and fire Data Status Error Event (from Error Log)
	 *  @param errorLog log
	 */
	protected void fireDataStatusEEvent (ValueNamePair errorLog)
	{
		if (errorLog != null)
			m_mTable.fireDataStatusEEvent(errorLog);
	}   //  fireDataStatusEvent

	/**
	 *  Get Current Row
	 *  @return current row
	 */
	public int getCurrentRow()
	{
		if (m_currentRow != verifyRow(m_currentRow))
			setCurrentRow(m_mTable.getRowCount()-1, true);
		return m_currentRow;
	}   //  getCurrentRow

	/**
	 *  Get Current Table Key ID
	 *  @return Record_ID
	 */
	public int getRecord_ID()
	{
		return m_mTable.getKeyID(m_currentRow);
	}   //  getRecord_ID

	/**
	 *  Get Key ID of row
	 *  @param  row row number
	 *  @return The Key ID of the row or -1 if not found
	 */
	public int getKeyID (int row)
	{
		return m_mTable.getKeyID (row);
	}   //  getCurrentKeyID

	/**
	 *  Navigate absolute - goto Row - (zero based).
	 *  - does nothing, if in current row
	 *  - saves old row if required
	 *  @param targetRow target row
	 *  @return current row
	 */
	public int navigate (int targetRow)
	{
		//  nothing to do
		if (targetRow == m_currentRow)
			return m_currentRow;
		log.info ("Row=" + targetRow);

		//  Row range check
		int newRow = verifyRow(targetRow);

		//  Check, if we have old uncommitted data
		m_mTable.dataSave(newRow, false);

		//  new position
		int row =  setCurrentRow(newRow, true);
		return row;
	}   //  navigate

	/**
	 *  Navigate relatively - i.e. plus/minus from current position
	 *  @param rowChange row change
	 *  @return current row
	 */
	public int navigateRelative (int rowChange)
	{
		return navigate (m_currentRow + rowChange);
	}   //  navigateRelative

	/**
	 *  Navigate to current now (reload)
	 *  @return current row
	 */
	public int navigateCurrent()
	{
		log.info("Row=" + m_currentRow);
		return setCurrentRow(m_currentRow, true);
	}   //  navigateCurrent

	/**
	 *  Row Range check
	 *  @param targetRow target row
	 *  @return checked row
	 */
	private int verifyRow (int targetRow)
	{
		int newRow = targetRow;
		//  Table Open?
		if (!m_mTable.isOpen())
		{
			log.severe ("Table not open");
			return -1;
		}
		//  Row Count
		int rows = getRowCount();
		if (rows == 0)
		{
			log.fine("No Rows");
			return -1;
		}
		if (newRow >= rows)
		{
			newRow = rows-1;
			log.fine("Set to max Row: " + newRow);
		}
		else if (newRow < 0)
		{
			newRow = 0;
			log.fine("Set to first Row");
		}
		return newRow;
	}   //  verifyRow

	/**
	 *  Set current row and load data into fields.
	 *  If there is no row - load nulls
	 *  @param newCurrentRow new current row
	 *  @param fireEvents fire events
	 *  @return current row
	 */
	private int setCurrentRow (int newCurrentRow, boolean fireEvents)
	{
		int oldCurrentRow = m_currentRow;
		m_currentRow = verifyRow (newCurrentRow);
		log.fine("Row=" + m_currentRow + " - fire=" + fireEvents);

		//  Update Field Values
		int size = m_mTable.getColumnCount();
		for (int i = 0; i < size; i++)
		{
			GridField mField = m_mTable.getField(i);
			//  get Value from Table
			if (m_currentRow >= 0)
			{
				Object value = m_mTable.getValueAt(m_currentRow, i);
				mField.setValue(value, m_mTable.isInserting());
				
				// gwu: now always validated, not just when inserting
				mField.validateValue();
			}
			else
			{   //  no rows - set to a reasonable value - not updateable
//				Object value = null;
//				if (mField.isKey() || mField.isParent() || mField.getColumnName().equals(m_linkColumnName))
//					value = mField.getDefault();
				mField.setValue();
			}
		}
		loadDependentInfo();

		if (!fireEvents)    //  prevents informing twice
			return m_currentRow;

		//  inform VTable/..    -> rowChanged
		m_propertyChangeSupport.firePropertyChange(PROPERTY, oldCurrentRow, m_currentRow);

		//  inform APanel/..    -> dataStatus with row updated
		if (m_DataStatusEvent == null)
			m_DataStatusEvent = new DataStatusEvent(this, getRowCount(), 
				m_mTable.isInserting(),		//	changed 
				Env.getCtx().isAutoCommit(m_vo.WindowNo), m_mTable.isInserting());
		//
		m_DataStatusEvent.setCurrentRow(m_currentRow);
		String status = m_DataStatusEvent.getAD_Message();
		if (status == null || status.length() == 0)
			 m_DataStatusEvent.setInfo("NavigateOrUpdate", null, false,false);
		fireDataStatusChanged(m_DataStatusEvent);
		return m_currentRow;
	}   //  setCurrentRow

	
	/**************************************************************************
	 *  Get RowCount
	 *  @return row count
	 */
	public int getRowCount()
	{
		int count = m_mTable.getRowCount();
		//  Wait a bit if currently loading
		if (count == 0 && m_mTable.isLoading())
		{
			try
			{
				Thread.sleep(100);      //  .1 sec
			}
			catch (Exception e) {}
			count = m_mTable.getRowCount();
		}
		return count;
	}   //  getRowCount

	/**
	 *  Get Column/Field Count
	 *  @return field count
	 */
	public int getFieldCount()
	{
		return m_mTable.getColumnCount();
	}   //  getFieldCount

	/**
	 *  Get Field by index
	 *  @param index index
	 *  @return MField
	 */
	public GridField getField (int index)
	{
		return m_mTable.getField(index);
	}   //  getField

	/**
	 * 	Find Column
	 *	@param columnName column
	 *	@return index of column/field
	 */
	public int findColumn (String columnName)
	{
		return m_mTable.findColumn(columnName);
	}	//	findColumn
	
	/**
	 *  Get Field by DB column name
	 *  @param columnName column name
	 *  @return MField
	 */
	public GridField getField (String columnName)
	{
		return m_mTable.getField(columnName);
	}   //  getField

	/**
	 *  Get all Fields
	 *  @return MFields
	 */
	public GridField[] getFields ()
	{
		return m_mTable.getFields();
	}   //  getField

	/**
	 *  Set New Value & call Callout
	 *  @param columnName database column name
	 *  @param value value
	 *  @return error message or ""
	 */
	public String setValue (String columnName, Object value)
	{
		if (columnName == null)
			return "NoColumn";
		return setValue(m_mTable.getField(columnName), value);
	}   //  setValue

	/**
	 *  Set New Value & call Callout
	 *  @param field field
	 *  @param value value
	 *  @return error message or ""
	 */
	public String setValue (GridField field, Object value)
	{
		if (field == null)
			return "NoField";

		log.fine(field.getColumnName() + "=" + value + " - Row=" + m_currentRow);

		int col = m_mTable.findColumn(field.getColumnName());
		m_mTable.setValueAt(value, m_currentRow, col, false);
		//
		return processFieldChange (field);
	}   //  setValue

	/**
	 * 	Is Processed
	 *	@return true if current record is processed
	 */
	public boolean isProcessed()
	{
		int index = m_mTable.findColumn("Processed");
		if (index != -1)
		{
			Object oo = m_mTable.getValueAt(m_currentRow, index);
			if (oo instanceof String)
				return "Y".equals(oo);
			if (oo instanceof Boolean)
				return ((Boolean)oo).booleanValue();
		}
		return "Y".equals(m_vo.ctx.getContext( m_vo.WindowNo, "Processed"));
	}	//	isProcessed
	
	/**
	 *  Process Field Change - evaluate Dependencies and process Callouts.
	 *
	 *  called from MTab.setValue or GridController.dataStatusChanged
	 *  @param changedField changed field
	 *  @return error message or ""
	 */
	public String processFieldChange (GridField changedField)
	{
		processDependencies (changedField);
		return processCallout (changedField);
	}   //  processFieldChange

	/**
	 *  Evaluate Dependencies
	 *  @param changedField changed field
	 */
	private void processDependencies (GridField changedField)
	{
		String columnName = changedField.getColumnName();
	//	log.trace(log.l4_Data, "Changed Column", columnName);

		//  when column name is not in list of DependentOn fields - fini
		if (!hasDependants(columnName))
			return;

		//  Get dependent MFields (may be because of display or dynamic lookup)
		ArrayList<GridField> list = getDependantFields(columnName);
		for (int i = 0; i < list.size(); i++)
		{
			GridField dependentField = (GridField)list.get(i);
		//	log.trace(log.l5_DData, "Dependent Field", dependentField==null ? "null" : dependentField.getColumnName());
			//  if the field has a lookup
			if (dependentField != null && dependentField.getLookup() instanceof MLookup)
			{
				MLookup mLookup = (MLookup)dependentField.getLookup();
			//	log.trace(log.l6_Database, "Lookup Validation", mLookup.getValidation());
				//  if the lookup is dynamic (i.e. contains this columnName as variable)
				if (mLookup.getValidation().indexOf("@"+columnName+"@") != -1)
				{
					log.fine(columnName + " changed - " 
						+ dependentField.getColumnName() + " set to null");
					//  invalidate current selection
					setValue(dependentField, null);
				}
			}
			if (dependentField != null && dependentField.getLookup() instanceof MLocatorLookup)
			{
				// gwu: invalidate currently selected locator if any dependent fields changed
				MLocatorLookup locLookup = (MLocatorLookup) dependentField.getLookup();
				int valueAsInt = 0;
				if( changedField.getValue() != null && changedField.getValue() instanceof Number )
					valueAsInt = ((Number) changedField.getValue()).intValue();
				if( columnName.equals( "M_Warehouse_ID" ) )
				{
					locLookup.setOnly_Warehouse_ID( valueAsInt );
				}
				if( columnName.equals( "M_Product_ID" ) )
				{
					locLookup.setOnly_Product_ID( valueAsInt );
				}
				locLookup.setOnly_Outgoing(Env.getCtx().isSOTrx(m_vo.WindowNo ));
				locLookup.refresh();
				if( !locLookup.isValid( dependentField.getValue() ) )
					setValue(dependentField, null);
			}
		}   //  for all dependent fields
	}   //  processDependencies

	
	/**************************************************************************
	 *  Process Callout(s).
	 *  <p>
	 *  The Callout is in the string of
	 *  "class.method;class.method;"
	 * If there is no class name, i.e. only a method name, the class is regarded
	 * as CalloutSystem.
	 * The class needs to comply with the Interface Callout.
	 *
	 * For a limited time, the old notation of Sx_matheod / Ux_menthod is maintained.
	 *
	 * @param field field
	 * @return error message or ""
	 * @see org.compiere.model.Callout
	 */
	private String processCallout (GridField field)
	{
		String callout = field.getCallout();
		if (callout.length() == 0)
			return "";
		//
		if (isProcessed())		//	only active records
			return "";			//	"DocProcessed";

		Object value = field.getValue();
		Object oldValue = field.getOldValue();
		log.fine(field.getColumnName() + "=" + value
			+ " (" + callout + ") - old=" + oldValue);

		StringTokenizer st = new StringTokenizer(callout, ";,", false);
		while (st.hasMoreTokens())      //  for each callout
		{
			String cmd = st.nextToken().trim();
			Callout call = null;
			String method = null;
			int methodStart = cmd.lastIndexOf(".");
			try
			{
				if (methodStart != -1)      //  no class
				{
					Class<?> cClass = Class.forName(cmd.substring(0,methodStart));
					call = (Callout)cClass.newInstance();
					method = cmd.substring(methodStart+1);
				}
			}
			catch (Exception e)
			{
				log.log(Level.SEVERE, "class", e);
				return "Callout Invalid: " + cmd + " (" + e.toString() + ")";
			}

			if (call == null || method == null || method.length() == 0)
				return "Callout Invalid: " + method;
				
			String retValue = "";
			try
			{	
				retValue = call.start(m_vo.ctx, method, m_vo.WindowNo, this, field, value, oldValue);
			}
			catch (Exception e)
			{
				log.log(Level.SEVERE, "start", e);
				retValue = 	"Callout Invalid: " + e.toString();
				return retValue;
			}
			if (!retValue.equals(""))		//	interrupt on first error
			{
				log.warning (retValue);
				return retValue;
			}
		}   //  for each callout
		return "";
	}	//	processCallout


	/**
	 *  Get Value of Field with columnName
	 *  @param columnName column name
	 *  @return value
	 */
	public Object getValue (String columnName)
	{
		if (columnName == null)
			return null;
		GridField field = m_mTable.getField(columnName);
		return getValue(field);
	}   //  getValue

	/**
	 *  Get Value of Field
	 *  @param field field
	 *  @return value
	 */
	public Object getValue (GridField field)
	{
		if (field == null)
			return null;
		return field.getValue();
	}   //  getValue

	/**
	 *  Get Value of Field in row
	 *  @param row row
	 *  @param columnName column name
	 *  @return value
	 */
	public Object getValue (int row, String columnName)
	{
		int col = m_mTable.findColumn(columnName);
		if (col == -1)
			return null;
		return m_mTable.getValueAt(row, col);
	}   //  getValue

	/**
	 *  toString
	 *  @return String representation
	 */
	public String toString()
	{
		String retValue = "MTab #" + m_vo.TabNo;
		if (m_vo != null)
		{
			retValue += " " + m_vo.Name + " (" + m_vo.AD_Tab_ID 
				+ ") QueryActive=" + (m_query != null && m_query.isActive())
				+ ", CurrentDays=" + m_vo.onlyCurrentDays;
		}
		return retValue;
	}   //  toString

	
	/**************************************************************************
	 *  @param l listener
	 */
	public synchronized void removePropertyChangeListener(PropertyChangeListener l)
	{
		m_propertyChangeSupport.removePropertyChangeListener(l);
	}
	/**
	 *  @param l listener
	 */
	public synchronized void addPropertyChangeListener(PropertyChangeListener l)
	{
		m_propertyChangeSupport.addPropertyChangeListener(l);
	}

	/**
	 *  @param l listener
	 */
	public synchronized void removeDataStatusListener(DataStatusListener l)
	{
		m_listenerList.remove(DataStatusListener.class, l);
	}
	/**
	 *  @param l listener
	 */
	public synchronized void addDataStatusListener(DataStatusListener l)
	{
		m_listenerList.add(DataStatusListener.class, l);
	}

}	//	MTab
