/****************************************************************************** * Product: Adempiere ERP & CRM Smart Business Solution * * Copyright (C) 1999-2006 ComPiere, Inc. All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * * under the terms version 2 of the GNU General Public License as published * * by the Free Software Foundation. This program is distributed in the hope * * that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * * with this program; if not, write to the Free Software Foundation, Inc., * * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. * * For the text or an alternative of this public license, you may reach us * * ComPiere, Inc., 2620 Augustine Dr. #245, Santa Clara, CA 95054, USA * * or via info@compiere.org or http://www.compiere.org/license.html * * @contributor Victor Perez , e-Evolution.SC FR [ 1757088 ] * * Teo Sarca, www.arhipac.ro * *****************************************************************************/ package org.compiere.model; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.Serializable; import java.math.BigDecimal; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.StringTokenizer; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.logging.Level; import javax.script.ScriptEngine; import javax.swing.event.EventListenerList; import org.adempiere.base.Core; import org.adempiere.base.IColumnCallout; import org.adempiere.base.IColumnCalloutFactory; import org.adempiere.model.MTabCustomization; import org.adempiere.util.ContextRunnable; import org.adempiere.util.ICalloutUI; import org.compiere.Adempiere; import org.compiere.util.CLogMgt; import org.compiere.util.CLogger; import org.compiere.util.DB; import org.compiere.util.DefaultEvaluatee; import org.compiere.util.DisplayType; import org.compiere.util.Env; import org.compiere.util.Evaluatee; import org.compiere.util.Evaluator; import org.compiere.util.Msg; import org.compiere.util.Util; import org.compiere.util.ValueNamePair; /** * Tab Model. * - a combination of AD_Tab (the display attributes) and AD_Table information. *
* The Tab owns also it's Table model * and listens to data changes to update the Field values. * *
* The Tab maintains the bound property: CurrentRow * *
* Event Hierarchies: * - dataChanged (from GridTable) * - setCurrentRow * - Update all Field Values * * - setValue * - Update Field Value * - Callout ** @author Jorg Janke * @version $Id: GridTab.java,v 1.10 2006/10/02 05:18:39 jjanke Exp $ * * @author Teo Sarca, SC ARHIPAC SERVICE SRL *
* MTab provides a property listener for changed rows and a * DataStatusListener for communicating changes of the underlying data * @param vo Value Object * @param w */ public GridTab(GridTabVO vo, GridWindow w) { this(vo, w, false); } /** * Create Tab Model from Value Object. *
	 *  GridTab provides a property listener for changed rows and a
	 *  DataStatusListener for communicating changes of the underlying data.
	 *  @param vo Value Object
	 *  @param w
	 *  @param virtual
	 */
	public GridTab(GridTabVO vo, GridWindow w, boolean virtual)
	{
		m_window = w;
		m_vo = vo;
		//  Create MTable
		m_mTable = new GridTable (m_vo.ctx, m_vo.AD_Table_ID, m_vo.TableName, m_vo.WindowNo, m_vo.TabNo, true, virtual);
		m_mTable.setReadOnly(m_vo.IsReadOnly || m_vo.IsView);
		m_mTable.setDeleteable(m_vo.IsDeleteable);
		
		selection = new ArrayList 
	 *  A tab can have attachment if it has single key column ending with _ID or _UU.
	 *  @return true if record can have attachment
	 */
	public boolean canHaveAttachment()
	{
		if (getKeyColumnName().endsWith("_ID") || getKeyColumnName().endsWith("_UU"))
			return true;
		return false;
	}   //	canHaveAttachment
	/**
	 *	Returns true, if current row has an Attachment
	 *  @return true if record has attachment
	 */
	public boolean hasAttachment()
	{
		return getAD_AttachmentID() > 0;
	}	//	hasAttachment
	/**
	 *	Get Attachment_ID for current record.
	 *	@return AD_AttachmentID or 0 if no attachment.
	 */
	public int getAD_AttachmentID()
	{
		if (!canHaveAttachment())
			return 0;
		String recordUU = m_mTable.getKeyUUID(m_currentRow);
		int recordID = m_mTable.getKeyID(m_currentRow);
		return MAttachment.getID(m_vo.AD_Table_ID, recordID, recordUU);
	}	//	getAttachmentID
	/**
	 *	Returns true, if current row has chat records.
	 *  @return true if current row has chat records.
	 */
	public boolean hasChat()
	{
		return getCM_ChatID() > 0;
	}	//	hasChat
	/**
	 *	Get Chat_ID for this record.
	 *	@return CM_Chat_ID or 0 if no chat records
	 */
	public int getCM_ChatID()
	{
		if (!canHaveAttachment())
			return 0;
		String recordUU = m_mTable.getKeyUUID(m_currentRow);
		int recordID = m_mTable.getKeyID(m_currentRow);
		return MChat.getID(m_vo.AD_Table_ID, recordID, recordUU);
	}	//	getCM_ChatID
	
	/**
	 * @return true if current row has post it note records.
	 */
	public boolean hasPostIt()
	{
		return getAD_PostIt_ID() > 0;
	}	//	hasChat
	/**
	 *	Get PostItID for this record.
	 *	@return AD_PostIt_ID or 0 if no post it note records.
	 */
	public int getAD_PostIt_ID()
	{
		if (!canHaveAttachment())
			return 0;
		String recordUU = m_mTable.getKeyUUID(m_currentRow);
		int recordID = m_mTable.getKeyID(m_currentRow);
		return MPostIt.getID(m_vo.AD_Table_ID, recordID, recordUU);
	}	//	getAD_PostIt_ID	
	
	/**
	 *  @return true if current row has Label records.
	 */
	public boolean hasLabel()
	{
		if (!canHaveAttachment())
			return false;
		String recordUU = m_mTable.getKeyUUID(m_currentRow);
		int recordID = m_mTable.getKeyID(m_currentRow);
		return MLabelAssignment.hasAnyAssignment(m_vo.AD_Table_ID, recordID, recordUU);
	}	//	hasLabel
	/**
	 *  @return true if this has import templates
	 */
	public boolean hasTemplate()
	{
		return MImportTemplate.getTemplates(Env.getAD_Role_ID(Env.getCtx()), getAD_Tab_ID()).size() > 0;
	}	//	hasChat
	/**
	 * 	Load Record Locks for Table and User
	 */
	public void loadLocks()
	{
		int AD_User_ID = Env.getContextAsInt(Env.getCtx(), Env.AD_USER_ID);
		if (log.isLoggable(Level.FINE)) 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";
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try
		{
			if (m_Lock == null)
				m_Lock = new ArrayList 
	 *  Usually called from UI side data status change listener.
	 *  @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();
		//  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)
		for (GridField dependentField : getDependantFields(columnName))
		{
			if (dependentField == null || dependentField.isLookupEditorSettingValue())
				continue;
			//  if the field has a lookup
			if (dependentField.getLookup() instanceof MLookup)
			{
				MLookup mLookup = (MLookup)dependentField.getLookup();
				//  if the lookup is dynamic (i.e. contains this columnName as variable)
				if (mLookup.getValidation().indexOf("@"+columnName+"@") != -1
						|| mLookup.getValidation().matches(".*[@]"+getTabNo()+"[|]"+columnName+"([:].+)?[@].*")
						|| mLookup.getValidation().matches(".*[@][~]?"+columnName+"([:].+)?[@].*"))
				{
					if (log.isLoggable(Level.FINE)) log.fine(columnName + " changed - "
						+ dependentField.getColumnName() + " set to null");
					mLookup.refresh();
					Object currentValue = dependentField.getValue();
					
					//  invalidate current selection
					setValue(dependentField, null);
					
					if (currentValue != null && mLookup.containsKeyNoDirect(currentValue))
						setValue(dependentField, currentValue);
				}
			}
			//  if the field is a Virtual UI Column
			if (dependentField.isVirtualUIColumn()) {
				dependentField.processUIVirtualColumn();
			}
		}   //  for all dependent fields
	}   //  processDependencies
	private List 
	 * The traditional column callout is in the format of "class.method;class.method;". 
	 * Newer callout implement the {@link IColumnCallout} interface and discover via {@link IColumnCalloutFactory} service.
	 * @param field field
	 * @return error message or ""
	 * @see org.compiere.model.Callout
	 * @see IColumnCallout
	 */
	public String processCallout (GridField field)
	{
		//
		if (isProcessed() && !field.isAlwaysUpdateable() && !field.isKey())		//	only active records
			return "";			//	"DocProcessed";
		Object value = field.getValue();
		Object oldValue = field.getOldValue();
		String callout = field.getCallout();
		if (log.isLoggable(Level.FINE)) log.fine(field.getColumnName() + "=" + value
			+ " (" + callout + ") - old=" + oldValue);
		if (callout.length() > 0)
		{
			StringTokenizer st = new StringTokenizer(callout, ";,", false);
			while (st.hasMoreTokens())      //  for each callout
			{
				String cmd = st.nextToken().trim();
	
				//detect infinite loop
				if (activeCallouts.contains(cmd)) continue;
	
				String retValue = "";
				// FR [1877902]
				// CarlosRuiz - globalqss - implement beanshell callout
				// Victor Perez  - vpj-cd implement JSR 223 Scripting
				if (cmd.toLowerCase().startsWith(MRule.SCRIPT_PREFIX)) {
	
					MRule rule = MRule.get(m_vo.ctx, cmd.substring(MRule.SCRIPT_PREFIX.length()));
					if (rule == null) {
						retValue = "Callout " + cmd + " not found";
						log.log(Level.SEVERE, retValue);
						return retValue;
					}
					if ( !  (rule.getEventType().equals(MRule.EVENTTYPE_Callout)
						  && rule.getRuleType().equals(MRule.RULETYPE_JSR223ScriptingAPIs))) {
						retValue = "Callout " + cmd
							+ " must be of type JSR 223 and event Callout";
						log.log(Level.SEVERE, retValue);
						return retValue;
					}
	
					ScriptEngine engine = rule.getScriptEngine();
					if (engine == null) {
						retValue = 	"Callout Invalid, engine not found: " + rule.getEngineName();
						log.log(Level.SEVERE, retValue);
						return retValue;
					}
	
					// Window context are    W_
					// Login context  are    G_
					MRule.setContext(engine, m_vo.ctx, m_vo.WindowNo);
					// now add the callout parameters windowNo, tab, field, value, oldValue to the engine
					// Method arguments context are A_
					engine.put(MRule.ARGUMENTS_PREFIX + "WindowNo", m_vo.WindowNo);
					engine.put(MRule.ARGUMENTS_PREFIX + "Tab", this);
					engine.put(MRule.ARGUMENTS_PREFIX + "Field", field);
					engine.put(MRule.ARGUMENTS_PREFIX + "Value", value);
					engine.put(MRule.ARGUMENTS_PREFIX + "OldValue", oldValue);
					engine.put(MRule.ARGUMENTS_PREFIX + "Ctx", m_vo.ctx);
	
					try
					{
						activeCallouts.add(cmd);
						retValue = engine.eval(rule.getScript()).toString();
					}
					catch (Exception e)
					{
						log.log(Level.SEVERE, "", e);
						retValue = 	"Callout Invalid: " + e.toString();
						return retValue;
					}
					finally
					{
						activeCallouts.remove(cmd);
					}
	
				} else {
	
					Callout call = null;
					String method = null;
					int methodStart = cmd.lastIndexOf('.');
					try
					{
						if (methodStart != -1)      //  has class name
						{
							String className = cmd.substring(0,methodStart);
							// IDEMPIERE-2732
							method = cmd.substring(methodStart+1);
							// get corresponding callout
							call = Core.getCallout(className, method);
							// end IDEMPIERE-2732
							if (call == null) {
								//no match from factory, check java classpath
								Class> cClass = Class.forName(className);
								call = (Callout)cClass.getDeclaredConstructor().newInstance();
							}
						}
					}
					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;
	
					try
					{
						activeCallouts.add(cmd);
						activeCalloutInstance.add(call);
						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;
					}
					finally
					{
						activeCallouts.remove(cmd);
						activeCalloutInstance.remove(call);
					}
	
				}
	
				if (!Util.isEmpty(retValue))		//	interrupt on first error
				{
					log.config(retValue); // no need to save an AD_Issue error on each callout
					return retValue;
				}
			}   //  for each callout
		}
		List
	 *  - for display logic
	 *  @return ArrayList
	 */
	public ArrayList
	 *		Scenarios:
	 *		- Never opened                  (full query)
	 *		- query changed                 (full query)
	 *		- Detail link value changed     (full query)
	 *		- otherwise                     (refreshAll)
	 *  
	 *  @param onlyCurrentRows only current rows (1 day)
	 */
	public void query (boolean onlyCurrentRows)
	{
		query (onlyCurrentRows, 0, 0);
	}	//	query
	/**
	 *	Assemble whereClause and query GridTable and position to row 0.
	 *  
	 *		Scenarios:
	 *		- Never opened                  (full query)
	 *		- query changed                 (full query)
	 *		- Detail link value changed     (full query)
	 *		- otherwise                     (refreshAll)
	 *  
	 *  @param onlyCurrentRows only current rows
	 *  @param onlyCurrentDays if only current row, how many days back
	 *  @param maxRows maximum rows or 0 for all
	 */
	public void query (boolean onlyCurrentRows, int onlyCurrentDays, int maxRows)
	{
		if (!m_loadComplete) initTab(false);
		Env.clearTabContext(m_vo.ctx, m_vo.WindowNo, m_vo.TabNo);
		
		selection.clear();
		
		if (log.isLoggable(Level.FINE)) log.fine("#" + m_vo.TabNo
			+ " - Only Current Rows=" + onlyCurrentRows
			+ ", Days=" + onlyCurrentDays + ", Detail=" + isDetail());
		m_oldQuery = m_query.getWhereClause();
		m_vo.onlyCurrentRows = onlyCurrentRows;
		m_vo.onlyCurrentDays = onlyCurrentDays;
		/**
		 *	Set Where Clause
		 */
		//	Tab Where Clause
		StringBuilder where = new StringBuilder(m_vo.WhereClause);
		if (m_vo.onlyCurrentDays > 0)
		{
			if (where.length() > 0)
				where.append(" AND ");
			where.append("Created >= ");
			where.append("getDate()-").append(m_vo.onlyCurrentDays);
		}
		//	Detail Query
		if (isDetail())
		{
			m_parentNeedSave = false;
			String lc = getLinkColumnName();
			if (lc.equals("")) {
				log.warning ("No link column");
				where.append (" 2=3");
			}
			else
			{
				String value = null;
				if ( m_parentColumnName.length() > 0 )
				{
					// explicit parent link defined
					value = Env.getContext(m_vo.ctx, m_vo.WindowNo, getParentTabNo(), m_parentColumnName, true);
					if (value == null || value.length() == 0)
						value = Env.getContext(m_vo.ctx, m_vo.WindowNo, m_parentColumnName, true); // back compatibility
				} else {
					value = Env.getContext(m_vo.ctx, m_vo.WindowNo, getParentTabNo(), lc, true);
					if (value == null || value.length() == 0)
						value = Env.getContext(m_vo.ctx, m_vo.WindowNo, lc, true); // back compatibility
				}
				//	Same link value?
				if (! m_linkValue.equals(value))
					setQuery(null);
				m_linkValue = value;
				//	Check validity
				if (value.length() == 0)
				{
					//parent is new, can't retrieve detail
					m_parentNeedSave = true;
					if (where.length() != 0)
						where.append(" AND ");
					where.append (" 2=3");
				}
				else
				{
					//	we have column and value
					if (where.length() != 0)
						where.append(" AND ");
					where.append(getTableName()).append(".").append(lc).append("=");
					if (lc.endsWith("_ID"))
						where.append(DB.TO_NUMBER(new BigDecimal(value), DisplayType.ID));
					else
						where.append(DB.TO_STRING(value));
				}
			}
		}	//	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(" (").append(q).append(")");
			}
		}
		m_currentRow = -1;
		/**
		 *	Query
		 */
		if (log.isLoggable(Level.FINE)) log.fine("#" + m_vo.TabNo + " - " + where);
		if (m_mTable.isOpen())
		{
			m_mTable.dataRequery(where.toString(), m_vo.onlyCurrentRows && !isDetail(), onlyCurrentDays);
		}
		else
		{
			m_mTable.setSelectWhereClause(where.toString(), m_vo.onlyCurrentRows && !isDetail(), onlyCurrentDays);
			m_mTable.open(maxRows);
		}
		//  Go to Record 0
		setCurrentRow(0, true);
		
		fireStateChangeEvent(new StateChangeEvent(this, StateChangeEvent.DATA_QUERY));
	}	//	query
	/**
	 * Reset detail GridTab when parent tab current record is new and not saved yet
	 */
	public void resetDetailForNewParentRecord() {
		if (m_mTable.isOpen())
		{
			String where = "2=3";
			m_extendedWhere = where;
			m_oldQuery = where;
			m_parentNeedSave = true;
			
			m_currentRow = -1;
			/**
			 *	Query
			 */
			if (log.isLoggable(Level.FINE)) log.fine("#" + m_vo.TabNo + " - " + where);		
			m_mTable.dataRequery(where, m_vo.onlyCurrentRows && !isDetail(), 0);
			
			// Go to Record 0
			setCurrentRow(0, true);
		}
	}
	
	/**
	 * 	Validate Query.
	 *  If query column is not a tab column, create EXISTS query.
	 * 	@param query query
	 * 	@return where clause
	 */
	private String validateQuery (MQuery query)
	{
		if (query == null || query.getRestrictionCount() == 0)
			return null;
		//	Check: only one restriction
		if (query.getRestrictionCount() != 1)
		{
			if (log.isLoggable(Level.FINE)) log.fine("Ignored(More than 1 Restriction): " + query);
			return query.getWhereClause(true);
		}
		String colName = query.getColumnName(0);
		if (colName == null)
		{
			if (log.isLoggable(Level.FINE)) log.fine("Ignored(No Column): " + query);
			return query.getWhereClause(true);
		}
		//	a '(' in the name = function - don't try to resolve
		if (colName.indexOf('(') != -1)
		{
			if (log.isLoggable(Level.FINE)) log.fine("Ignored(Function): " + colName);
			return query.getWhereClause(true);
		}
		//	OK - Query is valid
		//	Simple Query.
		if (getField(colName) != null)
		{
			if (log.isLoggable(Level.FINE)) log.fine("Field Found: " + colName);
			return query.getWhereClause(true);
		}
		//	Find Reference Column e.g. BillTo_ID -> C_BPartner_Location_ID
		final String sql1 = "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.AD_Key=cc.AD_Column_ID) "
			+ "WHERE c.AD_Reference_ID IN (?,?,?,?)"
			+ " AND c.ColumnName=?";
		String refColName = DB.getSQLValueStringEx(null, sql1, DisplayType.Table, DisplayType.Search, DisplayType.TableUU, DisplayType.SearchUU, colName);
		//	Reference Column found
		if (refColName != null)
		{
			query.setColumnName(0, refColName);
			if (getField(refColName) != null)
			{
				if (log.isLoggable(Level.FINE)) log.fine("Column " + colName + " replaced with " + refColName);
				return query.getWhereClause(true);
			}
			colName = refColName;
		}
		//	Column NOT in Tab - create EXISTS subquery
		String tabKeyColumn = getKeyColumnName();
		final String sql2 = "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
		String tableName = DB.getSQLValueStringEx(null, sql2, colName, tabKeyColumn);
		//	Special Reference Handling
		if (tabKeyColumn.equals("AD_Reference_ID"))
		{
			//	Column=AccessLevel, Key=AD_Reference_ID, Query=AccessLevel='6'
			final String sql3 = "SELECT AD_Reference_ID FROM AD_Column WHERE ColumnName=?";
			int AD_Reference_ID = DB.getSQLValueEx(null, sql3, 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)
		{
			if (log.isLoggable(Level.INFO)) log.info ("Not successful - Column="
				+ colName + ", Key=" + tabKeyColumn
				+ ", Query=" + query);
			return query.getWhereClause(true);
		}
		query.setTableName("xx");
		// use IN instead of EXISTS as subquery should be highly selective
		StringBuilder result = new StringBuilder (getTableName()).append(".").append(tabKeyColumn)
			.append(" IN (SELECT xx.").append(tabKeyColumn)
			.append(" FROM ")
			.append(tableName).append(" xx WHERE ")
			.append(query.getWhereClause(true))
			.append(")");
		if (log.isLoggable(Level.FINE)) log.fine(result.toString());
		return result.toString();
	}	//	validateQuery
	/**
	 *  Refresh all data
	 */
	public void dataRefreshAll ()
	{
		dataRefreshAll(true);
	}
	/**
	 *  Refresh all data
	 *  @param fireEvent
	 */
	public void dataRefreshAll (boolean fireEvent)
	{
		dataRefreshAll(fireEvent, false);
	}
	/**
	 *  Refresh all data
	 *  @param fireEvent
	 *  @param retainedCurrentRow
	 */
	public void dataRefreshAll (boolean fireEvent, boolean retainedCurrentRow)
	{
		if (log.isLoggable(Level.FINE)) log.fine("#" + m_vo.TabNo);
		selection.clear();
		int keyNo = m_mTable.getKeyID(m_currentRow);
		UUID uuid = null;
		if (keyNo == -1) {
			uuid = m_mTable.getUUID(m_currentRow);
		}
		m_mTable.dataRefreshAll(fireEvent, retainedCurrentRow ? m_currentRow : -1);
		if (keyNo != -1 || uuid != null)
		{
			if (   ( keyNo != -1 && keyNo != m_mTable.getKeyID(m_currentRow) ) 
				|| (uuid != null && m_mTable.getUUID(m_currentRow) == null) || ( uuid != null && uuid.compareTo(m_mTable.getUUID(m_currentRow)) != 0) )   //  something changed
			{
				int size = getRowCount();
				for (int i = 0; i < size; i++)
				{
					if (   ( keyNo != -1 && keyNo == m_mTable.getKeyID(i) )
						|| ( uuid != null && m_mTable.getUUID(i) != null && uuid.compareTo(m_mTable.getUUID(i)) == 0) )
					{
						m_currentRow = i;
						break;
					}
				}
			}
		}
		setCurrentRow(m_currentRow, fireEvent);
		if (fireEvent)
			fireStateChangeEvent(new StateChangeEvent(this, StateChangeEvent.DATA_REFRESH_ALL));
	}   //  dataRefreshAll
	/**
	 *  Refresh current row data
	 */
	public void dataRefresh ()
	{
		dataRefresh(true);
	}
	/**
	 *  Refresh current row data
	 *  @param fireEvent
	 */
	public void dataRefresh (boolean fireEvent)
	{
		dataRefresh (m_currentRow, fireEvent);
	}   //  dataRefresh
	/**
	 *  Refresh row data
	 *  @param row index
	 */
	public void dataRefresh (int row)
	{
		dataRefresh(row, true);
	}
	/**
	 *  Refresh row data
	 *  @param row index
	 *  @param fireEvent
	 */
	public void dataRefresh (int row, boolean fireEvent)
	{
		if (log.isLoggable(Level.FINE)) log.fine("#" + m_vo.TabNo + " - row=" + row);
		m_mTable.dataRefresh(row, fireEvent);
		setCurrentRow(row, fireEvent);
		if (fireEvent)
			fireStateChangeEvent(new StateChangeEvent(this, StateChangeEvent.DATA_REFRESH));
	}   //  dataRefresh
	/**
	 *  Save data
	 *  @param manualCmd if true, no vetoable PropertyChange event will be fired for save confirmation from GridTable
	 *  @return true if save complete (or not required)
	 */
	public boolean dataSave(boolean manualCmd)
	{
		if (log.isLoggable(Level.FINE)) log.fine("#" + m_vo.TabNo + " - row=" + m_currentRow);
		try
		{
			if (hasChangedCurrentTabAndParents())
				return false;
			boolean retValue = (m_mTable.dataSave(manualCmd) == GridTable.SAVE_OK);
			if (manualCmd)
			{
				setCurrentRow(m_currentRow, false);
				if (m_lastDataStatusEvent != null && m_lastDataStatusEvent.getCurrentRow() == m_currentRow
					&& ((m_lastDataStatusEvent.Record_ID != null && m_lastDataStatusEvent.Record_ID instanceof Integer
					&& (Integer) m_lastDataStatusEvent.Record_ID == 0) || m_lastDataStatusEvent.Record_ID == null))
				{
					updateDataStatusEventProperties(m_lastDataStatusEvent);
				}
			}
			fireStateChangeEvent(new StateChangeEvent(this, StateChangeEvent.DATA_SAVE));
			return retValue;
		}
		catch (Exception e)
		{
			log.log(Level.SEVERE, "#" + m_vo.TabNo + " - row=" + m_currentRow, e);
		}
		return false;
	}   //  dataSave
	/**
	 * 
	 * @return true if need save and all mandatory field has value
	 */
	public boolean isNeedSaveAndMandatoryFill()
	{
		return m_mTable.isNeedSaveAndMandatoryFill();
	}
	
	/**
	 * Validate if current tab or parent tab record has changed in database 
	 * @return true if if there are changes
	 */
	public boolean hasChangedCurrentTabAndParents() {
		String msg = null;
		// Carlos Ruiz / globalqss - [ adempiere-Bugs-1985481 ] Processed documents can be edited
		// Validate that current record has not changed and validate that every parent above has not changed
		if (m_mTable.hasChanged(m_currentRow)) {
			// return error stating that current record has changed and it cannot be saved
			msg = Msg.getMsg(Env.getCtx(), "CurrentRecordModified");
			log.saveError("CurrentRecordModified", msg, false);
			return true;
		}
		if (isDetail()) {
			// get parent tab
			// the parent tab is the first tab above with level = this_tab_level-1
			int level = m_vo.TabLevel;
			for (int i = m_window.getTabIndex(this) - 1; i >= 0; i--) {
				GridTab parentTab = m_window.getTab(i);
				if (parentTab.m_vo.TabLevel == level-1) {
					// this is parent tab
					if (parentTab.m_mTable.hasChanged(parentTab.m_currentRow)) {
						// return error stating that current record has changed and it cannot be saved
						msg = Msg.getMsg(Env.getCtx(), "ParentRecordModified") + ": " + parentTab.getName();
						log.saveError("ParentRecordModified", msg, false);
						return true;
					} else {
						// search for the next parent
						if (parentTab.isDetail()) {
							level = parentTab.m_vo.TabLevel;
						} else {
							break;
						}
					}
				}
			}
		}
		return false;
	}
	
	/**
	 * refresh current row of parent tabs
	 */
	public void refreshParentTabs() {
		refreshParentTabs(false);
	}
	/**
	 * refresh current row of parent tabs
	 * @param fireParentEvent
	 */
	public void refreshParentTabs(boolean fireParentEvent) {
		if (isDetail()) {
			// get parent tab
			// the parent tab is the first tab above with level = this_tab_level-1
			int level = m_vo.TabLevel;
			for (int i = m_window.getTabIndex(this) - 1; i >= 0; i--) {
				GridTab parentTab = m_window.getTab(i);
				if (parentTab.m_vo.TabLevel == level-1) {
					// this is parent tab
					parentTab.dataRefresh(fireParentEvent);
					// search for the next parent
					if (parentTab.isDetail()) {
						level = parentTab.m_vo.TabLevel;
					} else {
						break;
					}
				}
			}
		}
	}
	
	/**
	 *  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/undo data changes
	 */
	public void dataIgnore()
	{
		if (log.isLoggable(Level.FINE)) log.fine("#" + m_vo.TabNo);
		if (!m_mTable.isOpen())
			return;
		
		if (m_mTable.isInserting()) 
		{
			m_currentRow--;
		}
		m_mTable.dataIgnore();
		setCurrentRow(m_currentRow, false);    //  re-load data
		fireStateChangeEvent(new StateChangeEvent(this, StateChangeEvent.DATA_IGNORE));
		if (log.isLoggable(Level.FINE)) 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)
	{
		if (log.isLoggable(Level.FINE)) log.fine("#" + m_vo.TabNo);
		if (!isInsertRecord())
		{
			log.warning ("Insert Not allowed in TabNo=" + m_vo.TabNo);
			return false;
		}
		//hengsin, don't create new when parent is empty
		if (isDetail() && m_parentNeedSave)
			return false;
		if (!selection.isEmpty())
			clearSelection();
		
		/**
		 * temporary set currentrow to point to the new row to ensure even cause by m_mTable.dataNew
		 * is handle properly.
		 */
		int oldCurrentRow = m_currentRow;
		m_currentRow = m_currentRow + 1;
		boolean retValue = m_mTable.dataNew (oldCurrentRow, copy);
		m_currentRow = oldCurrentRow;
		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));
		m_mTable.setChanged(false);		
		fireStateChangeEvent(new StateChangeEvent(this, StateChangeEvent.DATA_NEW));
		return retValue;
	}   //  dataNew
	/**
	 *  Delete current Row
	 *  @return true if deleted
	 */
	public boolean dataDelete()
	{
		if (log.isLoggable(Level.FINE)) log.fine("#" + m_vo.TabNo + " - row=" + m_currentRow);
		boolean retValue = m_mTable.dataDelete(m_currentRow);
		setCurrentRow(m_currentRow, true);
		if (!selection.isEmpty()) 
		{
			List
	 *	Yes     - Table must be open
	 *	        - Query String is the same
	 *	        - Not Detail Tab
	 *	        - Old link column value is same as current one
	 *  
	 *  @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
		// IDEMPIERE-4799 Fix Check Parent Column name
		String columnName = Util.isEmpty(m_parentColumnName) ? getLinkColumnName() : m_parentColumnName;
		String value = Env.getContext(m_vo.ctx, m_vo.WindowNo, this.getParentTabNo(), columnName);
		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 Included in other Tab
	 *  @return true if included
	 *  @deprecated
	 */
	@Deprecated
	public boolean isIncluded()
	{
		if (! m_includedAlreadyCalc) {
			m_included = false;
			if (getParentTab() != null) {
				for (GridTab tab : getParentTab().getIncludedTabs()) {
					if (tab.equals(this)) {
						m_included = true;
						break;
					}
				}
			}
			m_includedAlreadyCalc = true;
		}		
		return m_included;
	}   //  isIncluded
	/**
	 *  Is Tab Included in other Tab
	 *  @param isIncluded true if included
	 *  @deprecated The method getIncluded now validate against the structure, this method is called nowhere
	*/
	@Deprecated
	public void setIncluded(boolean isIncluded)
	{
		m_included = isIncluded;
	}   //  setIncluded
	/**
	 *  Are Only Current Rows displayed
	 *  @return true if no history
	 */
	public boolean isOnlyCurrentRows()
	{
		return m_vo.onlyCurrentRows;
	}   //  isOnlyCurrentRows
	
	/**
	 *	Return Parent ArrayList
	 *  @return parent column names
	 */
	public ArrayList
");
				}
			}
			if (lines.length() > 0)
				return lines.toString();
		}
		return null;
	} // getWidgetLines
	/**
	 *  Load Dependent 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=?";
			PreparedStatement pstmt = null;
			ResultSet rs = null;
			try
			{
				pstmt = DB.prepareStatement(sql, null);
				pstmt.setInt(1, C_DocTyp_ID);
				rs = pstmt.executeQuery();
				if (rs.next())
					Env.setContext(m_vo.ctx, m_vo.WindowNo, "OrderType", rs.getString(1));
 			}
			catch (SQLException e)
			{
				log.log(Level.SEVERE, sql, e);
			}
			finally
			{
				DB.close(rs, pstmt);
				rs = null; pstmt = null;
			}
		}   //  loadOrderInfo
		// Set the Phone Format on BPartnerLocation based on Country
		if (m_vo.TableName.equals("C_BPartner_Location"))
		{
			Integer location_int = (Integer) getValue(X_C_BPartner_Location.COLUMNNAME_C_Location_ID);
			String phone_frm = null;
			if (location_int != null)
				// take the phone format from country
				phone_frm = DB.getSQLValueString(null, "SELECT ExpressionPhone FROM C_Country c, C_Location l WHERE c.C_Country_ID = l.C_Country_ID AND l.C_location_ID = ?", location_int);
			GridField fPhone = getField(X_C_BPartner_Location.COLUMNNAME_Phone);
			MColumn colPhone = null;
			if (fPhone != null)
				colPhone = MColumn.get(Env.getCtx(), fPhone.getAD_Column_ID());
			GridField fPhone2 = getField(X_C_BPartner_Location.COLUMNNAME_Phone2);
			MColumn colPhone2 = null;
			if (fPhone2 != null)
				colPhone2 = MColumn.get(Env.getCtx(), fPhone2.getAD_Column_ID());
			GridField fFax = getField(X_C_BPartner_Location.COLUMNNAME_Fax);
			MColumn colFax = null;
			if (fFax != null)
				colFax = MColumn.get(Env.getCtx(), fFax.getAD_Column_ID());
			// Apply the country format if the column doesn't have format
			if (colPhone != null && (colPhone.getVFormat() == null || colPhone.getVFormat().length() == 0))
				fPhone.setVFormat(phone_frm);
			if (colPhone2 != null && (colPhone2.getVFormat() == null || colPhone2.getVFormat().length() == 0))
				fPhone2.setVFormat(phone_frm);
			if (colFax != null && (colFax.getVFormat() == null || colFax.getVFormat().length() == 0))
				fFax.setVFormat(phone_frm);
		}
		// Load virtual UI columns
		for (GridField field : getFields()) {
			if (field.isVirtualUIColumn())
				field.processUIVirtualColumn();
		}
	}   //  loadDependentInfo
	/**
	 *	Can this tab have Attachments?.
	 *  
	 *  Data Status event from GridTable.
	 *  - get raw info and add current row information
	 *  - update current row
	 *  - redistribute (fire) Data Status event
	 *  
	 *  @param e event
	 */
	@Override
	public void dataStatusChanged (DataStatusEvent e)
	{		
		if (log.isLoggable(Level.FINE)) 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(GridTable.SORTED_DSE_EVENT))
		{
			oldCurrentRow = m_currentRow;
			if (e.getCurrentRow() >= 0)
				setCurrentRow(e.getCurrentRow());
			else
				setCurrentRow(0, true);
		}
		//  set current row
		m_DataStatusEvent = e;          //  setCurrentRow clear it, need to save again
		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
			//  Avoid firing of duplicate event
			boolean fire = true;
			if (m_lastDataStatusEvent != null)
			{
				if (System.currentTimeMillis() - m_lastDataStatusEventTime < 200)
				{
					if (m_lastDataStatusEvent.isEqual(m_DataStatusEvent))
					{
						fire = false;
					}
				}
			}
			
			if (fire)
				fireDataStatusChanged(m_DataStatusEvent);
		}
		//reset
		m_lastDataStatusEventTime = System.currentTimeMillis();
		m_lastDataStatusEvent = m_DataStatusEvent;
		m_DataStatusEvent = null;
	}	//	dataStatusChanged
	/**
	 *	Fire data status change event
	 *  @param e event
	 */
	private void fireDataStatusChanged (DataStatusEvent e)
	{
		if (e == null) {
			log.warning("IDEMPIERE-2449 - event must not arrive null here -> " + Thread.currentThread().getStackTrace());
			return;  // avoid NPE below
		}
		DataStatusListener[] listeners = m_listenerList.getListeners(DataStatusListener.class);
		if (listeners.length == 0 || e == null)
			return;
		if (log.isLoggable(Level.FINE)) log.fine(e.toString());
		//  WHO Info
		if (e.getCurrentRow() >= 0)
		{
			updateDataStatusEventProperties(e);
		}
		e.setInserting(m_mTable.isInserting());
		//  Distribute/fire it
        for (int i = 0; i < listeners.length; i++)
        	listeners[i].dataStatusChanged(e);
	}	//	fireDataStatusChanged
	/**
	 * Update {@link DataStatusEvent} properties from gridTab
	 * @param e
	 */
	public void updateDataStatusEventProperties(DataStatusEvent e) {
		e.Created = (Timestamp)getValue("Created");
		e.CreatedBy = (Integer)getValue("CreatedBy");
		e.Updated = (Timestamp)getValue("Updated");
		e.UpdatedBy = (Integer)getValue("UpdatedBy");
		if (   e.AD_Table_ID == I_AD_OrgInfo.Table_ID
			|| e.AD_Table_ID == I_AD_ClientInfo.Table_ID
			|| e.AD_Table_ID == I_AD_Ref_Table.Table_ID
			|| e.AD_Table_ID == I_C_AcctSchema_Default.Table_ID
			|| e.AD_Table_ID == I_C_AcctSchema_GL.Table_ID) {
			e.Record_ID = getValue(m_parents.get(0));
		} else {
			e.Record_ID = getValue(m_keyColumnName);
		}
		//  Info
		StringBuilder info = new StringBuilder(getTableName());
		
		//  We have a key column
		if (m_keyColumnName != null && m_keyColumnName.length() > 0)
		{
			info.append("\n ")
				.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(i == 0 ? "\n " : " - ")
					.append(keyCol).append("=").append(getValue(keyCol));
			}
		}
		e.Info = info.toString();
	}
	/**
	 *  Create and fire Data Status Error Event
	 *  @param AD_Message message
	 *  @param info info
	 *  @param isError true for Error, false for Warning
	 */
	public 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
	 */
	public void fireDataStatusEEvent (ValueNamePair errorLog)
	{
		if (errorLog != null)
			m_mTable.fireDataStatusEEvent(errorLog);
	}   //  fireDataStatusEvent
	/**
	 *  Get Current Row
	 *  @return current row index
	 */
	public int getCurrentRow()
	{
		return m_currentRow;
	}   //  getCurrentRow
	/**
	 *  @return Key id of current row (for e.g C_Order_ID value)
	 */
	public int getRecord_ID()
	{
		return m_mTable.getKeyID(m_currentRow);
	}   //  getRecord_ID
	/**
	 *  Get Current Table UUID
	 *  @return UUID value of current row (for e.g C_Order_UU value)
	 */
	public String getRecord_UU()
	{
		UUID uuid = m_mTable.getUUID(m_currentRow);
		return (uuid == null ? null : uuid.toString());
	}   //  getRecord_UU
	/**
	 *  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);
	}   //  getKeyID
	/**
	 *  Get Key UUID of row
	 *  @param  row row number
	 *  @return The Key UUID of the row or -1 if not found
	 */
	public String getKeyUUID (int row)
	{
		return m_mTable.getKeyUUID (row);
	}   //  getKeyUUID
	/**
	 *  
	 *  Navigate absolute - goto Row - (zero based).
	 *  - does nothing, if target row = current row
	 *  - saves current row if required
	 *  
	 *  @param targetRow target row
	 *  @return new current row index
	 */
	public int navigate (int targetRow)
	{
		//  nothing to do
		if (targetRow == m_currentRow)
			return m_currentRow;
		if (log.isLoggable(Level.INFO)) log.info ("Row=" + targetRow);
		//  Row range check
		int newRow = verifyRow(targetRow);
		//  Check, if we have old uncommitted data
		if (m_mTable.dataSave(newRow, false) == false)
			return m_currentRow;
		//remove/ignore new and unchange row
		if (m_mTable.isInserting())
		{
			if (newRow > m_currentRow)
				newRow--;
			dataIgnore();
		}
		//  new position
		return setCurrentRow(newRow, true);
	}   //  navigate
	/**
	 *  Navigate relatively - i.e. plus/minus from current position
	 *  @param rowChange offset from current position
	 *  @return new current row index
	 */
	public int navigateRelative (int rowChange)
	{
		return navigate (m_currentRow + rowChange);
	}   //  navigateRelative
	/**
	 *  Navigate to current now (reload)
	 *  @return current row index
	 */
	public int navigateCurrent()
	{
		if (log.isLoggable(Level.INFO)) log.info("Row=" + m_currentRow);
		return setCurrentRow(m_currentRow, true);
	}   //  navigateCurrent
	/**
	 *  Row Range check
	 *  @param targetRow target row index
	 *  @return checked row index
	 */
	private int verifyRow (int targetRow)
	{
		int newRow = targetRow;
		//  Table Open?
		if (!m_mTable.isOpen())
		{
			if (targetRow >= 0)
				log.log (Level.SEVERE, "Table not open", new Exception("Table not open"));
			
			return -1;
		}
		//  Row Count
		int rows = getRowCount();
		if (rows == 0 && !m_mTable.isLoading())
		{
			log.fine("No Rows");
			return -1;
		}
		if (newRow >= rows)
		{
			if (!m_mTable.isLoading())
			{
				newRow = rows-1;
				if (log.isLoggable(Level.FINE)) log.fine("Set to max Row: " + newRow);
			}
		}
		else if (newRow < 0)
		{
			newRow = 0;
			if (log.isLoggable(Level.FINE)) log.fine("Set to first Row");
		}
		
		m_mTable.waitLoadingForRow(newRow);
		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 index
	 */
	public int setCurrentRow (int newCurrentRow, boolean fireEvents)
	{
		boolean changingRow = (m_currentRow != newCurrentRow);
		int oldCurrentRow = m_currentRow;
		m_currentRow = verifyRow (newCurrentRow);
		if (log.isLoggable(Level.FINE)) log.fine("Row=" + m_currentRow + " - fire=" + fireEvents);
		//  Update Field Values
		int size = m_mTable.getColumnCount();
		GridField keyCalloutDelayed = null;
		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());
				if (mField.isKey())
					keyCalloutDelayed = mField;
			}
			else
			{   //  no rows - set to a reasonable value - not updateable
				// CarlosRuiz - globalqss [ 1881480 ] Navigation problem between tabs
				// the implementation of linking with window context variables is very weak
				// you must be careful when defining a column in a detail tab with a field
				// with the same column name as some of the links of the tabs above
				// this can cause bad behavior of linking
				if (mField.isKey())
					mField.setValueAndUpdateContext();
				else
					mField.setValue();
			}
		}
		if (changingRow && keyCalloutDelayed != null)
			processCallout(keyCalloutDelayed);
		
		//set isSOTrx context
		if (changingRow) {
			setIsSOTrxContext();
		}
		
		loadDependentInfo();
		if (!fireEvents)    //  prevents informing twice
			return m_currentRow;
		//  inform VTable/..    -> rowChanged
		m_propertyChangeSupport.firePropertyChange(PROPERTY, oldCurrentRow, m_currentRow);
		//check last data status event
		long since = System.currentTimeMillis() - m_lastDataStatusEventTime;
		if (since <= 500 && m_lastDataStatusEvent != null)
		{
			m_DataStatusEvent = m_lastDataStatusEvent;
		}
		//  inform APanel/..    -> dataStatus with row updated
		if (m_DataStatusEvent == null) {
			m_DataStatusEvent = new DataStatusEvent(this, getRowCount(),
				m_mTable.isInserting(),		//	changed
				Env.isAutoCommit(Env.getCtx(), m_vo.WindowNo), m_mTable.isInserting());
			m_DataStatusEvent.AD_Table_ID = m_vo.AD_Table_ID;
		}
		//
		m_DataStatusEvent.setCurrentRow(m_currentRow);
		String status = m_DataStatusEvent.getAD_Message();
		if (status == null || status.length() == 0)
			 m_DataStatusEvent.setInfo(DEFAULT_STATUS_MESSAGE, null, false,false);
		fireDataStatusChanged(m_DataStatusEvent);
		//reset
		m_DataStatusEvent = null;
		m_mTable.setCurrentRow(m_currentRow);
		
		return m_currentRow;
	}   //  setCurrentRow
	/**
	 * Update window context's IsSOTrx value.
	 */
	private void setIsSOTrxContext() {
		final String IsSOTrx = "IsSOTrx";
		final String C_DocType_ID = "C_DocType_ID";
		final String C_DocTypeTarget_ID = "C_DocTypeTarget_ID";
		if (getField(IsSOTrx) != null || getField(C_DocType_ID) != null || getField(C_DocTypeTarget_ID) != null) {
			String isSOTrx = null;
			GridField field = getField(IsSOTrx);
			if (field != null && field.getValue() != null) {
				Object value = field.getValue();
				if (value instanceof Boolean) {
					isSOTrx = ((Boolean) value).booleanValue() ? "Y" : "N";
				} else if (value instanceof String) {
					isSOTrx = (String) value;
				}
			}
			if (isSOTrx == null) {
				field = getField(C_DocType_ID);
				if (field != null && field.getValue() != null) {
					int docTypeId = ((Number)field.getValue()).intValue();
					if (docTypeId > 0) {
						isSOTrx = MDocType.get(docTypeId).isSOTrx() ? "Y" : "N";
					}
				}
			}
			if (isSOTrx == null) {
				field = getField(C_DocTypeTarget_ID);
				if (field != null && field.getValue() != null) {
					int docTypeId = ((Number)field.getValue()).intValue();
					if (docTypeId > 0) {
						isSOTrx = MDocType.get(docTypeId).isSOTrx() ? "Y" : "N";
					}
				}
			}
			if (isSOTrx != null) {
				Env.setContext(Env.getCtx(), getWindowNo(), getTabNo(), IsSOTrx, isSOTrx);
				if (m_vo.TabNo == 0) {
					Env.setContext(Env.getCtx(), getWindowNo(), IsSOTrx, isSOTrx);
				}
			}
		}
	}
	/**
	 *  Set current row - used for deleteSelection
	 *  @param row
	 */
	public void setCurrentRow(int row){
			setCurrentRow(row, false);
	}
	/**
	 *  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
			{
				m_mTable.waitLoading(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 field index
	 *  @return GridField
	 */
	public GridField getField (int index)
	{
		return (index >= 0 ? m_mTable.getField(index) : null);
	}   //  getField
	/**
	 *  Get Field by DB column name
	 *  @param columnName column name
	 *  @return GridField
	 */
	public GridField getField (String columnName)
	{
		return m_mTable.getField(columnName);
	}   //  getField
	/**
	 *  Get all Fields
	 *  @return GridFields
	 */
	public GridField[] getFields ()
	{
		return m_mTable.getFields();
	}   //  getField
	/**
	 *  Set New Value and 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 and call Callout
	 *  @param field field
	 *  @param value value
	 *  @return error message or ""
	 */
	public String setValue (GridField field, Object value)
	{
		if (field == null)
			return "NoField";
		if (log.isLoggable(Level.FINE)) log.fine(field.getColumnName() + "=" + value + " - Row=" + m_currentRow);
		if (value instanceof Integer) {
			if (((Integer)value).intValue() < 0 && DisplayType.isID(field.getDisplayType())) {
				value = null;
			} else if (((Integer)value).intValue() == 0 && field.isLookup()) {
				MColumn column = MColumn.get(field.getAD_Column_ID());
				if (! MTable.isZeroIDTable(column.getReferenceTableName()))
					value = null;
			}
		}
		int col = m_mTable.findColumn(field.getColumnName());
		//will trigger processFieldChange through data status change event
		m_mTable.setValueAt(value, m_currentRow, col, false);
		//
		return "";
	}   //  setValue
	/**
	 * 	Is Processed
	 *	@return true if current record is processed
	 */
	public boolean isProcessed()
	{
		return getValueAsBoolean("Processed");
	}	//	isProcessed
	/**
	 * Is the current record active
	 * @return true if current record is active
	 * author Teo Sarca - BF [ 1742159 ]
	 */
	public boolean isActive()
	{
		return getValueAsBoolean("IsActive");
	}	//	isProcessed
	/**
	 *  Process Field Change - evaluate Dependencies and process Callouts.
	 *  
	 * The class needs to comply with the Interface Callout.
	 * 
	 * If there is no column with the given name, the context for current window will be checked.
	 * @param columnName column name
	 * @return boolean value or false if the field was not found
	 * author Teo Sarca
	 */
	public boolean getValueAsBoolean(String columnName)
	{
		int index = m_mTable.findColumn(columnName);
		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(Env.getContext(m_vo.ctx, m_vo.WindowNo, columnName));
	}	//	isProcessed
	/**
	 *  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 for a row.
	 *  @param row row index
	 *  @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 #";
		if (m_vo != null)
			retValue += m_vo.TabNo + " " + m_vo.Name + " (" + m_vo.AD_Tab_ID + ")";
		else
			retValue += "???";
		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);
	}
	/**
	 * @param l
	 */
	public synchronized void addStateChangeListener(StateChangeListener l)
	{
		m_listenerList.add(StateChangeListener.class, l);
	}
	/**
	 * @param l
	 */
	public synchronized void removeStateChangeListener(StateChangeListener l)
	{
		m_listenerList.remove(StateChangeListener.class, l);
	}
	/**
	 * Feature Request [1707462]
	 * Enable runtime change of VFormat
	 * @param identifier column name
	 * @param strNewFormat new input mask
	 * author fer_luck
	 */
	public void setFieldVFormat (String identifier, String strNewFormat)
	{
		m_mTable.setFieldVFormat(identifier, strNewFormat);
	}	//	setFieldVFormat
	/**
	 * Switches the line/seqNo of the two rows
	 * @param from row index
	 * @param to row index
	 * @param sortColumn column index of sort column
	 * @param ascending sorting modus
	 */
	public void switchRows(int from, int to, int sortColumn, boolean ascending) {
		if (log.isLoggable(Level.FINE)) log.fine(from + " - " + to + " - " + sortColumn + " - " + ascending);
		// nothing to do
		if (from == to) {
			log.finest("nothing to do - from == to");
			return;
		}
		//check if lines are editable
		if(!(m_mTable.isRowEditable(from)&& m_mTable.isRowEditable(to))){
			log.finest("row not editable - return");
			return;
		}
		// Row range check
		to = verifyRow(to);
		if (to == -1) {
			log.finest("Row range check - return");
			return;
		}
		// Check, if we have old uncommitted data
		m_mTable.dataSave(to, false);
		//find the line column
		int lineCol = m_mTable.findColumn("Line");
		if (lineCol == -1) {
			lineCol = m_mTable.findColumn("SeqNo");
		}
		if(lineCol == -1){
			//no Line, no SeqNo
			return;
		}
		//get the line/seq numbers
		Integer lineNoCurrentRow = null;
		Integer lineNoNextRow = null;
		if (m_mTable.getValueAt(from, lineCol) instanceof Integer) {
			lineNoCurrentRow = (Integer) m_mTable.getValueAt(from, lineCol);
			lineNoNextRow = (Integer) m_mTable.getValueAt(to, lineCol);
		} else if (m_mTable.getValueAt(from, lineCol) instanceof BigDecimal) {
			lineNoCurrentRow = Integer.valueOf(((BigDecimal) m_mTable.getValueAt(from, lineCol))
					.intValue());
			lineNoNextRow = Integer.valueOf(((BigDecimal) m_mTable.getValueAt(to, lineCol))
					.intValue());
		} else {
			log.fine("unknown value format - return");
			return;
		}
		//don't sort special lines like taxes
		if (lineNoCurrentRow >= 9900
				|| lineNoNextRow >= 9900) {
			log.fine("don't sort - might be special lines");
			return;
		}
		// switch the line numbers and save new values
		m_mTable.setValueAt(lineNoCurrentRow, to, lineCol);
		setCurrentRow(to, false);
		m_mTable.dataSave(true);
		m_mTable.setValueAt(lineNoNextRow, from, lineCol);
		setCurrentRow(from, false);
		m_mTable.dataSave(true);
		//resort
		if(sortColumn != -1) {
			m_mTable.sort(sortColumn, ascending);
		} else {
			m_mTable.sort(lineCol, true);
		}
		navigate(to);
	}
	/**
	 * Fire state change event
	 * @param e
	 */
	private void fireStateChangeEvent(StateChangeEvent e)
	{
		StateChangeListener[] listeners = m_listenerList.getListeners(StateChangeListener.class);
		if (listeners.length == 0)
			return;
		for(int i = 0; i < listeners.length; i++) {
			listeners[i].stateChange(e);
		}
	}
	/**
	 * @return list of all tabs included in this tab
	 */
	public List
	 * If defined, you cannot query more records as defined - the query criteria needs to be changed to query less records.
	 * @return Max Query Records
     */
	public int getMaxQueryRecords() {
		// minimum between AD_Tab.MaxQueryRecords and AD_Role.MaxQueryRecords
		int roleMaxQueryRecords = MRole.getDefault().getMaxQueryRecords();
		int tabMaxQueryRecords = m_vo.MaxQueryRecords;
		if (roleMaxQueryRecords > 0 && (roleMaxQueryRecords < tabMaxQueryRecords || tabMaxQueryRecords == 0))
			tabMaxQueryRecords = roleMaxQueryRecords;
		if (tabMaxQueryRecords == 0)
			tabMaxQueryRecords = MSysConfig.getIntValue(MSysConfig.GLOBAL_MAX_QUERY_RECORDS, 
	        		DEFAULT_GLOBAL_MAX_QUERY_RECORDS, Env.getAD_Client_ID(Env.getCtx()));
		return tabMaxQueryRecords;
	}
	/**
	 * 	Require Query
	 *	@param noRecords records
	 *	@return true if query is required
	 */
	public boolean isQueryRequire (int noRecords)
	{
		if (noRecords < 2)
			return false;
		int max = getMaxQueryRecords();
		if (max > 0 && noRecords > max)
			return true;
		int qu = MRole.getDefault().getConfirmQueryRecords();
		return (noRecords > qu);
	}	//	isQueryRequire
	/**
	 * 	Over max Query
	 *	@param noRecords records
	 *	@return true if over max query records
	 */
	public boolean isQueryMax (int noRecords)
	{
		int max = getMaxQueryRecords();
		return max > 0 && noRecords > max;
	}	//	isQueryMax
	/**
	 * reset to empty
	 */
	public void reset() {
		m_mTable.reset();
		setCurrentRow(0, true);
	}
}	//	GridTab