/******************************************************************************
 * 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           *
 *****************************************************************************/
package org.compiere.wf;
import static org.compiere.model.SystemIDs.MESSAGE_WORKFLOWRESULT;
import java.io.File;
import java.math.BigDecimal;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.logging.Level;
import org.adempiere.exceptions.AdempiereException;
import org.compiere.model.MAttachment;
import org.compiere.model.MBPartner;
import org.compiere.model.MClient;
import org.compiere.model.MColumn;
import org.compiere.model.MConversionRate;
import org.compiere.model.MMailText;
import org.compiere.model.MNote;
import org.compiere.model.MOrg;
import org.compiere.model.MOrgInfo;
import org.compiere.model.MPInstance;
import org.compiere.model.MPInstancePara;
import org.compiere.model.MProcess;
import org.compiere.model.MProcessPara;
import org.compiere.model.MRefList;
import org.compiere.model.MRole;
import org.compiere.model.MTable;
import org.compiere.model.MUser;
import org.compiere.model.MUserRoles;
import org.compiere.model.MWFActivityApprover;
import org.compiere.model.PO;
import org.compiere.model.Query;
import org.compiere.model.X_AD_WF_Activity;
import org.compiere.print.ReportEngine;
import org.compiere.process.DocAction;
import org.compiere.process.ProcessInfo;
import org.compiere.process.StateEngine;
import org.compiere.util.CLogger;
import org.compiere.util.DB;
import org.compiere.util.DisplayType;
import org.compiere.util.Env;
import org.compiere.util.Msg;
import org.compiere.util.Trace;
import org.compiere.util.Trx;
import org.compiere.util.TrxEventListener;
import org.compiere.util.Util;
/**
 *	Extended Workflow Activity Model for AD_WF_Activity. 
IDEMPIERE-3209 added process-aware resultset-based constructor
 *  @version $Id: MWFActivity.java,v 1.4 2006/07/30 00:51:05 jjanke Exp $
 */
public class MWFActivity extends X_AD_WF_Activity implements Runnable
{	
	/**
	 * generated serial id
	 */
	private static final long serialVersionUID = -9119089506977887142L;
	private static final String CURRENT_WORKFLOW_PROCESS_INFO_ATTR = "Workflow.ProcessInfo";
	
	/**
	 * 	Get Activities for table/record
	 *	@param ctx context
	 *	@param AD_Table_ID table
	 *	@param Record_ID record
	 *	@param activeOnly if true only not processed records are returned
	 *	@return activities
	 */
	public static MWFActivity[] get (Properties ctx, int AD_Table_ID, int Record_ID, boolean activeOnly)
	{
		ArrayList params = new ArrayList();
		StringBuilder whereClause = new StringBuilder("AD_Table_ID=? AND Record_ID=?");
		params.add(AD_Table_ID);
		params.add(Record_ID);
		if (activeOnly)
		{
			whereClause.append(" AND Processed<>?");
			params.add(true);
		}
		List list = new Query(ctx, Table_Name, whereClause.toString(), null)
					.setParameters(params)
					.setOrderBy(COLUMNNAME_AD_WF_Activity_ID)
					.list();
		MWFActivity[] retValue = new MWFActivity[list.size ()];
		list.toArray (retValue);
		return retValue;
	}	//	get
	/**
	 * 	Get info of active activities
	 * 	@param ctx context
	 *	@param AD_Table_ID table
	 *	@param Record_ID record
	 *	@return info of active activities (separated by new line character)
	 */
	public static String getActiveInfo (Properties ctx, int AD_Table_ID, int Record_ID)
	{
		MWFActivity[] acts = get (ctx, AD_Table_ID, Record_ID, true);
		if (acts == null || acts.length == 0)
			return null;
		//
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < acts.length; i++)
		{
			if (i > 0)
				sb.append("\n");
			MWFActivity activity = acts[i];
			sb.append(activity.toStringX());
		}
		return sb.toString();
	}	//	getActivityInfo
    /**
     * UUID based Constructor
     * @param ctx  Context
     * @param AD_WF_Activity_UU  UUID key
     * @param trxName Transaction
     */
    public MWFActivity(Properties ctx, String AD_WF_Activity_UU, String trxName) {
        super(ctx, AD_WF_Activity_UU, trxName);
		if (Util.isEmpty(AD_WF_Activity_UU))
			throw new IllegalArgumentException ("Cannot create new WF Activity directly");
		m_state = new StateEngine (getWFState());
    }
	/**
	 * 	Standard Constructor
	 *	@param ctx context
	 *	@param AD_WF_Activity_ID id
	 *	@param trxName transaction
	 */
	public MWFActivity (Properties ctx, int AD_WF_Activity_ID, String trxName)
	{
		super (ctx, AD_WF_Activity_ID, trxName);
		if (AD_WF_Activity_ID == 0)
			throw new IllegalArgumentException ("Cannot create new WF Activity directly");
		m_state = new StateEngine (getWFState());
	}	//	MWFActivity
	/**
	 * 	Load Constructor
	 *	@param ctx context
	 *	@param rs result set
	 *	@param trxName transaction
	 */
	public MWFActivity (Properties ctx, ResultSet rs, String trxName)
	{
		super(ctx, rs, trxName);
		m_state = new StateEngine (getWFState());
	}	//	MWFActivity
	/**
	 * 	Parent Constructor
	 *	@param process process
	 *	@param AD_WF_Node_ID start node
	 */
	public MWFActivity (MWFProcess process, int AD_WF_Node_ID)
	{
		super (process.getCtx(), 0, process.get_TrxName());
		setAD_WF_Process_ID (process.getAD_WF_Process_ID());
		setPriority(process.getPriority());
		//	Document Link
		setAD_Table_ID(process.getAD_Table_ID());
		setRecord_ID(process.getRecord_ID());
		if(process.getPO() != null)
        	m_po = process.getPO();
		//modified by Rob Klein
		setAD_Client_ID(process.getAD_Client_ID());
		setAD_Org_ID(process.getAD_Org_ID());
		//	Status
		super.setWFState(WFSTATE_NotStarted);
		m_state = new StateEngine (getWFState());
		setProcessed (false);
		//	Set Workflow Node
		setAD_Workflow_ID (process.getAD_Workflow_ID());
		setAD_WF_Node_ID (AD_WF_Node_ID);
		//	Node Priority & End Duration
		MWFNode node = MWFNode.get(getCtx(), AD_WF_Node_ID);
		int priority = node.getPriority();
		if (priority != 0 && priority != getPriority())
			setPriority (priority);
		long limitMS = node.getLimitMS();
		if (limitMS != 0)
			setEndWaitTime(new Timestamp(limitMS + System.currentTimeMillis()));
		//	Responsible
		setResponsible(process);
		saveCrossTenantSafeEx();
		//
		m_audit = new MWFEventAudit(this);
		m_audit.setAD_Org_ID(getAD_Org_ID());//Add by Hideaki Hagiwara
		m_audit.saveEx();
		//
		m_process = process;
	}	//	MWFActivity
	
	/**
	 * 	Process-aware Parent Constructor
	 *	@param process process
	 *	@param ctx context
	 *	@param rs record to load
	 *  @param trxName transaction name
	 */
	public MWFActivity (MWFProcess process, Properties ctx, ResultSet rs, String trxName)
	{
		super(ctx, rs, trxName);
		m_process = process;
	}
	
	/**
	 * 	Parent Constructor
	 *	@param process process
	 *	@param next_ID start node
	 *	@param lastPO PO from the previously executed node
	 */
	public MWFActivity(MWFProcess process, int next_ID, PO lastPO) {
		this(process, next_ID);
		if (lastPO != null) {
			// Compare if the last PO is the same type and record needed here, if yes, use it
			if (lastPO.get_Table_ID() == getAD_Table_ID() && lastPO.get_ID() == getRecord_ID()) {
				m_po = lastPO;
			}
		}
	}
	/**	State Machine				*/
	private StateEngine			m_state = null;
	/**	Workflow Node				*/
	private MWFNode				m_node = null;
	/** Transaction					*/
	//private Trx 				m_trx = null;
	/**	Audit						*/
	private MWFEventAudit		m_audit = null;
	/** Persistent Object			*/
	private PO					m_po = null;
	/** Document Status				*/
	private String				m_docStatus = null;
	/**	New Value to save in audit	*/
	private String				m_newValue = null;
	/** Process						*/
	private MWFProcess 			m_process = null;
	/** List of email recipients	*/
	private ArrayList 	m_emails = new ArrayList();
	/**
	 * 	Get State
	 *	@return state
	 */
	public StateEngine getState()
	{
		return m_state;
	}	//	getState
	/**
	 * Set Activity State.();
				sendEMail();
				setTextMsg(m_emails.toString());
			} else
			{
				MClient client = MClient.get(getCtx(), getAD_Client_ID());
				MMailText mailtext = new MMailText(getCtx(),getNode().getR_MailText_ID(),null);
				mailtext.setPO(m_po);
				String subject = getNode().getDescription()
				+ ": " + mailtext.getMailHeader();
				String message = mailtext.getMailText(true)
				+ "\n-----\n" + getNodeHelp();
				String to = getNode().getEMail();
				client.sendEMail(to, subject, message, null);
			}
			return true;	//	done
		}	//	EMail
		/******	Set Variable				******/
		else if (MWFNode.ACTION_SetVariable.equals(action))
		{
			String value = m_node.getAttributeValue();
			if (log.isLoggable(Level.FINE)) log.fine("SetVariable:AD_Column_ID=" + m_node.getAD_Column_ID()
				+ " to " +  value);
			MColumn column = m_node.getColumn();
			int dt = column.getAD_Reference_ID();
			return setVariable (value, dt, null, trx);
		}	//	SetVariable
		/******	TODO Start WF Instance		******/
		else if (MWFNode.ACTION_SubWorkflow.equals(action))
		{
			log.warning ("Workflow:AD_Workflow_ID=" + m_node.getAD_Workflow_ID());
			log.warning("Start WF Instance is not implemented yet");
		}
		/******	User Choice					******/
		else if (MWFNode.ACTION_UserChoice.equals(action))
		{
			if (log.isLoggable(Level.FINE)) log.fine("UserChoice:AD_Column_ID=" + m_node.getAD_Column_ID());
			//	Approval
			if (m_node.isUserApproval()
				&& getPO(trx) instanceof DocAction)
			{
				DocAction doc = (DocAction)m_po;
				boolean autoApproval = false;
				//	Approval Hierarchy
				if (isInvoker())
				{
					//	Set Approver
					int startAD_User_ID = Env.getAD_User_ID(getCtx());
					if (startAD_User_ID == 0)
						startAD_User_ID = doc.getDoc_User_ID();
					int nextAD_User_ID = getApprovalUser(startAD_User_ID,
						doc.getC_Currency_ID(), doc.getApprovalAmt(),
						doc.getAD_Org_ID(),
						startAD_User_ID == doc.getDoc_User_ID());	//	own doc
                   if (nextAD_User_ID<=0) {
                	   m_docStatus = DocAction.STATUS_Invalid;
                	   throw new AdempiereException(Msg.getMsg(getCtx(), "NoApprover"));
                   }
					//	same user = approved
					autoApproval = startAD_User_ID == nextAD_User_ID;
					if (!autoApproval)
						setAD_User_ID(nextAD_User_ID);
				}
				else	//	fixed Approver
				{
					MWFResponsible resp = getResponsible();
					// MZ Goodwill
					// [ 1742751 ] Workflow: User Choice is not working
					if (resp.isHuman())
					{
						autoApproval = resp.getAD_User_ID() == Env.getAD_User_ID(getCtx());
						if (!autoApproval && resp.getAD_User_ID() != 0)
							setAD_User_ID(resp.getAD_User_ID());
					}
					else if(resp.isRole())
					{
						MUserRoles[] urs = MUserRoles.getOfRole(getCtx(), resp.getAD_Role_ID());
						for (int i = 0; i < urs.length; i++)
						{
							if(urs[i].getAD_User_ID() == Env.getAD_User_ID(getCtx()) && urs[i].isActive())
							{
								autoApproval = true;
								break;
							}
						}
					}
					else if(resp.isManual()) {
					    MWFActivityApprover[] approvers = MWFActivityApprover.getOfActivity(getCtx(), getAD_WF_Activity_ID(), get_TrxName());
                        for (int i = 0; i < approvers.length; i++)
                        {
                            if(approvers[i].getAD_User_ID() == Env.getAD_User_ID(getCtx()))
                            {
                                autoApproval = true;
                                break;
                            }
                        }
					}
					else if(resp.isOrganization())
					{
						throw new AdempiereException("Support not implemented for "+resp);
					}
					else
					{
						throw new AdempiereException("@NotSupported@ "+resp);
					}
					// end MZ
				}
				if (autoApproval
					&& doc.processIt(DocAction.ACTION_Approve)
					&& doc.save())
					return true;	//	done
			}	//	approval
			return false;	//	wait for user
		}
		/******	User Form					******/
		else if (MWFNode.ACTION_UserForm.equals(action))
		{
			if (log.isLoggable(Level.FINE)) log.fine("Form:AD_Form_ID=" + m_node.getAD_Form_ID());
			return false;
		}
		/******	User Window					******/
		else if (MWFNode.ACTION_UserWindow.equals(action))
		{
			if (log.isLoggable(Level.FINE)) log.fine("Window:AD_Window_ID=" + m_node.getAD_Window_ID());
			return false;
		}
		/******	User Info					******/
		else if (MWFNode.ACTION_UserInfo.equals(action))
		{
			if (log.isLoggable(Level.FINE)) log.fine("InfoWindow:AD_InfoWindow_ID=" + m_node.getAD_InfoWindow_ID());
			return false;
		}
		//
		throw new IllegalArgumentException("Invalid Action (Not Implemented) =" + action);
	}	//	performWork
	/**
	 * 	Set value to PO
	 *	@param value new Value
	 *	@param displayType display type
	 *	@param textMsg optional Message
	 *	@return true if set
	 *	@throws Exception if error
	 */
	private boolean setVariable(String value, int displayType, String textMsg, Trx trx) throws Exception
	{
		m_newValue = null;
		getPO(trx);
		if (m_po == null)
			throw new Exception("Persistent Object not found - AD_Table_ID="
				+ getAD_Table_ID() + ", Record_ID=" + getRecord_ID());
		//	Set Value
		Object dbValue = null;
		if (value == null)
			;
		else if (displayType == DisplayType.YesNo)
			dbValue = Boolean.valueOf("Y".equals(value));
		else if (DisplayType.isNumeric(displayType))
			dbValue = new BigDecimal (value);
		else if (DisplayType.isID(displayType)) {
			MColumn column = MColumn.get(Env.getCtx(), getNode().getAD_Column_ID());
			String referenceTableName = column.getReferenceTableName();
			if (referenceTableName != null) {
				MTable refTable = MTable.get(Env.getCtx(), referenceTableName);
				dbValue = Integer.valueOf(value);
				boolean validValue = true;
				PO po = refTable.getPO((Integer)dbValue, trx.getTrxName());
				if (po == null || po.get_ID() == 0) {
					// foreign key does not exist
					validValue = false;
				}
				if (validValue && po.getAD_Client_ID() != Env.getAD_Client_ID(Env.getCtx())) {
					validValue = false;
					if (po.getAD_Client_ID() == 0) {
						String accessLevel = refTable.getAccessLevel();
						if (   MTable.ACCESSLEVEL_All.equals(accessLevel)
							|| MTable.ACCESSLEVEL_SystemPlusClient.equals(accessLevel)) {
							// client foreign keys are OK if the table has reference All or System+Client
							validValue = true;
						}
					}
				}
				if (! validValue) {
					throw new Exception("Persistent Object not updated - AD_Table_ID="
							+ getAD_Table_ID() + ", Record_ID=" + getRecord_ID()
							+ " - Value=" + value + " is not valid for a foreign key");
				}
			}
		}
		else
			dbValue = value;
		if (!m_po.set_ValueOfColumnReturningBoolean(getNode().getAD_Column_ID(), dbValue)) {
			throw new Exception("Persistent Object not updated - AD_Table_ID="
					+ getAD_Table_ID() + ", Record_ID=" + getRecord_ID()
					+ " - Value=" + value + " error : " + CLogger.retrieveErrorString("check logs"));
		}
		m_po.saveEx();
		if (dbValue != null && !dbValue.equals(m_po.get_ValueOfColumn(getNode().getAD_Column_ID())))
			throw new Exception("Persistent Object not updated - AD_Table_ID="
				+ getAD_Table_ID() + ", Record_ID=" + getRecord_ID()
				+ " - Should=" + value + ", Is=" + m_po.get_ValueOfColumn(m_node.getAD_Column_ID()));
		//	Info
		String msg = getNode().getAttributeName() + "=" + value;
		if (textMsg != null && textMsg.length() > 0)
			msg += " - " + textMsg;
		setTextMsg (msg);
		m_newValue = value;
		return true;
	}	//	setVariable
	/**
	 * 	Set User Choice
	 * 	@param AD_User_ID user
	 *	@param value new Value
	 *	@param displayType display type
	 *	@param textMsg optional Message
	 *	@return true if set
	 *	@throws Exception if error
	 */
	public boolean setUserChoice (int AD_User_ID, String value, int displayType,
		String textMsg) throws Exception
	{
		setWFState (StateEngine.STATE_Running);
		setAD_User_ID(AD_User_ID);
		Trx trx = ( get_TrxName() != null ) ? Trx.get(get_TrxName(), false) : null;
		boolean ok = setVariable (value, displayType, textMsg, trx);
		if (!ok)
			return false;
		String newState = StateEngine.STATE_Completed;
		//	Approval
		if (getNode().isUserApproval() && getPO(trx) instanceof DocAction)
		{
			DocAction doc = (DocAction)m_po;
			try
			{
				//	Not approved
				if (!"Y".equals(value))
				{
					newState = StateEngine.STATE_Aborted;
					if (!(doc.processIt (DocAction.ACTION_Reject)))
						setTextMsg ("Cannot Reject - Document Status: " + doc.getDocStatus());
				}
				else
				{
					if (isInvoker())
					{
						int startAD_User_ID = Env.getAD_User_ID(getCtx());
						if (startAD_User_ID == 0)
							startAD_User_ID = doc.getDoc_User_ID();
						int nextAD_User_ID = getApprovalUser(startAD_User_ID,
							doc.getC_Currency_ID(), doc.getApprovalAmt(),
							doc.getAD_Org_ID(),
							startAD_User_ID == doc.getDoc_User_ID());	//	own doc
						//	No Approver
						if (nextAD_User_ID <= 0)
						{
							newState = StateEngine.STATE_Aborted;
							setTextMsg (Msg.getMsg(getCtx(), "NoApprover"));
							doc.processIt (DocAction.ACTION_Reject);
						}
						else if (startAD_User_ID != nextAD_User_ID)
						{
							forwardTo(nextAD_User_ID, "Next Approver");
							newState = StateEngine.STATE_Suspended;
						}
						else	//	Approve
						{
							if (!(doc.processIt (DocAction.ACTION_Approve)))
							{
								newState = StateEngine.STATE_Aborted;
								setTextMsg ("Cannot Approve - Document Status: " + doc.getDocStatus());
							}
						}
					}
					//	No Invoker - Approve
					else if (!(doc.processIt (DocAction.ACTION_Approve)))
					{
						newState = StateEngine.STATE_Aborted;
						setTextMsg ("Cannot Approve - Document Status: " + doc.getDocStatus());
					}
				}
				doc.saveEx();
			}
			catch (Exception e)
			{
				newState = StateEngine.STATE_Terminated;
				setTextMsg ("User Choice: " + e.toString());
				addTextMsg(e);
				log.log(Level.WARNING, "", e);
			}
			// Send Approval Notification
			if (newState.equals(StateEngine.STATE_Aborted)) {
				MUser to = new MUser(getCtx(), doc.getDoc_User_ID(), null);
				// send email
				if (to.isNotificationEMail()) {
					MClient client = MClient.get(getCtx(), doc.getAD_Client_ID());
					client.sendEMail(doc.getDoc_User_ID(), Msg.getMsg(getCtx(), "NotApproved")
							+ ": " + doc.getDocumentNo(),
							(doc.getSummary() != null ? doc.getSummary() + "\n" : "" )
							+ (doc.getProcessMsg() != null ? doc.getProcessMsg() + "\n" : "")
							+ (getTextMsg() != null ? getTextMsg() : ""), null);
				}
				// Send Note
				if (to.isNotificationNote()) {
					MNote note = new MNote(getCtx(), "NotApproved", doc.getDoc_User_ID(), null);
					note.setTextMsg((doc.getSummary() != null ? doc.getSummary() + "\n" : "" )
							+ (doc.getProcessMsg() != null ? doc.getProcessMsg() + "\n" : "")
							+ (getTextMsg() != null ? getTextMsg() : ""));
					// 2007-06-08, matthiasO.
					// Add record information to the note, so that the user receiving the
					// note can jump to the doc easily
					note.setRecord(m_po.get_Table_ID(), m_po.get_ID());
					note.saveEx();
				}
			}
		}
		setWFState (newState);
		return ok;
	}	//	setUserChoice
	/**
	 * 	Forward to user, usually for approval
	 *	@param AD_User_ID user
	 *	@param textMsg text message
	 *	@return true if forwarded
	 */
	public boolean forwardTo (int AD_User_ID, String textMsg)
	{
		if (AD_User_ID == getAD_User_ID())
		{
			log.log(Level.WARNING, "Same User - AD_User_ID=" + AD_User_ID);
			return false;
		}
		//
		MUser oldUser = MUser.get(getCtx(), getAD_User_ID());
		MUser user = MUser.get(getCtx(), AD_User_ID);
		if (user == null || user.get_ID() == 0)
		{
			log.log(Level.WARNING, "Does not exist - AD_User_ID=" + AD_User_ID);
			return false;
		}
		//	Update
		setAD_User_ID (user.getAD_User_ID());
		setTextMsg(textMsg);
		saveEx();
		//	Close up Old Event
		getEventAudit();
		m_audit.setAD_User_ID(oldUser.getAD_User_ID());
		m_audit.setTextMsg(getTextMsg());
		m_audit.setAttributeName("AD_User_ID");
		m_audit.setOldValue(oldUser.getName()+ "("+oldUser.getAD_User_ID()+")");
		m_audit.setNewValue(user.getName()+ "("+user.getAD_User_ID()+")");
		//
		m_audit.setWFState(getWFState());
		m_audit.setEventType(MWFEventAudit.EVENTTYPE_StateChanged);
		long ms = System.currentTimeMillis() - m_audit.getCreated().getTime();
		m_audit.setElapsedTimeMS(new BigDecimal(ms));
		m_audit.saveEx();
		//	Create new one
		m_audit = new MWFEventAudit(this);
		m_audit.saveEx();
		return true;
	}	//	forwardTo
	/**
	 * 	Set User Confirmation
	 * 	@param AD_User_ID user
	 *	@param textMsg optional message
	 */
	public void setUserConfirmation (int AD_User_ID, String textMsg)
	{
		log.fine(textMsg);
		setWFState (StateEngine.STATE_Running);
		setAD_User_ID(AD_User_ID);
		if (textMsg != null)
			setTextMsg (textMsg);
		setWFState (StateEngine.STATE_Completed);
	}	//	setUserConfirmation
	/**
	 * 	Fill Report/Process Parameters
	 *	@param pInstance process instance
	 * 	@param trx transaction
	 */
	private void fillParameter(MPInstance pInstance, Trx trx)
	{
		getPO(trx);
		//
		MWFNodePara[] nParams = m_node.getParameters();
		MProcessPara[] processParams = pInstance.getProcessParameters();
		for (int pi = 0; pi < processParams.length; pi++)
		{
			MPInstancePara iPara = new MPInstancePara (pInstance, processParams[pi].getSeqNo());
			iPara.setParameterName(processParams[pi].getColumnName());
			iPara.setInfo(processParams[pi].getName());
			iPara.setParameterName(processParams[pi].getColumnName());
			iPara.setInfo(processParams[pi].getName());
			for (int np = 0; np < nParams.length; np++)
			{
				MWFNodePara nPara = nParams[np];				
				if (iPara.getParameterName().equals(nPara.getAttributeName()))
				{
					String variableName = nPara.getAttributeValue();
					Object value = parseNodeParaAttribute(nPara);
					if (value == nPara)
						break;
					//	No Value
					if (value == null)
					{
						if (nPara.isMandatory())
							log.warning(nPara.getAttributeName()
								+ " - empty - mandatory!");
						else
							if (log.isLoggable(Level.FINE)) log.fine(nPara.getAttributeName()
								+ " - empty");
						break;
					}
					if( DisplayType.isText(nPara.getDisplayType())
							&& Util.isEmpty(String.valueOf(value))) {
						if (log.isLoggable(Level.FINE)) log.fine(nPara.getAttributeName() + " - empty string");
							break;
					}
					//	Convert to Type
					try
					{
						if (DisplayType.isNumeric(nPara.getDisplayType())
							|| DisplayType.isID(nPara.getDisplayType()))
						{
							BigDecimal bd = null;
							if (value instanceof BigDecimal)
								bd = (BigDecimal)value;
							else if (value instanceof Integer)
								bd = new BigDecimal (((Integer)value).intValue());
							else
								bd = new BigDecimal (value.toString());
							iPara.setP_Number(bd);
							if (log.isLoggable(Level.FINE)) log.fine(nPara.getAttributeName()
								+ " = " + variableName + " (=" + bd + "=)");
						}
						else if (DisplayType.isDate(nPara.getDisplayType()))
						{
							Timestamp ts = null;
							if (value instanceof Timestamp)
								ts = (Timestamp)value;
							else
								ts = Timestamp.valueOf(value.toString());
							iPara.setP_Date(ts);
							if (log.isLoggable(Level.FINE)) log.fine(nPara.getAttributeName()
								+ " = " + variableName + " (=" + ts + "=)");
						}
						else
						{
							iPara.setP_String(value.toString());
							if (log.isLoggable(Level.FINE)) log.fine(nPara.getAttributeName()
								+ " = " + variableName
								+ " (=" + value + "=) " + value.getClass().getName());
						}
						if (!iPara.save())
							log.warning("Not Saved - " + nPara.getAttributeName());
					}
					catch (Exception e)
					{
						log.warning(nPara.getAttributeName()
							+ " = " + variableName + " (" + value
							+ ") " + value.getClass().getName()
							+ " - " + e.getLocalizedMessage());
					}
					break;
				}
			}	//	node parameter loop
		}	//	instance parameter loop
	}	//	fillParameter
	/**
	 * Parse attribute value of node parameter
	 * @param nPara node parameter
	 * @return parsed value
	 */
	private Object parseNodeParaAttribute(MWFNodePara nPara)
	{
		String variableName = nPara.getAttributeValue();
		if (log.isLoggable(Level.FINE)) log.fine(nPara.getAttributeName()
			+ " = " + variableName);
		//	Value - Constant/Variable
		Object value = variableName;
		if (variableName == null
			|| (variableName != null && variableName.length() == 0))
			value = null;
		else if (variableName.indexOf('@') != -1 && m_po != null)	//	we have a variable
		{
			//	Strip
			int index = variableName.indexOf('@');
			String columnName = variableName.substring(index+1);
			index = columnName.indexOf('@');
			if (index == -1)
			{
				log.warning(nPara.getAttributeName()
					+ " - cannot evaluate=" + variableName);
				return nPara;
			}
			columnName = columnName.substring(0, index);
			index = m_po.get_ColumnIndex(columnName);
			if (index != -1)
			{
				value = m_po.get_Value(index);
			}
			else	//	not a column
			{
				//	try Env
				String env = Env.getContext(getCtx(), columnName);
				if (env.length() == 0)
				{
					log.warning(nPara.getAttributeName()
						+ " - not column nor environment =" + columnName
						+ "(" + variableName + ")");
					return nPara;
				}
				else
					value = env;
			}
		}	//	@variable@
		return value;
	}
	
	/**
	 * 	Send EMail
	 */
	private void sendEMail()
	{
		DocAction doc = (DocAction)m_po;
		MMailText text = new MMailText (getCtx(), m_node.getR_MailText_ID(), null);
		text.setPO(m_po, true);
		//
		String subject = null;
		String raw = text.getMailHeader(false);
		if (raw != null && raw.contains("@_noDocInfo_@"))
			subject = text.getMailHeader().replaceAll("@_noDocInfo_@", "");
		else
			subject = doc.getDocumentInfo() + ": " + text.getMailHeader();
		String message = null;
		raw = text.getMailText(true, false);
		if (raw != null && (raw.contains("@=DocumentInfo") || raw.contains("@=documentInfo")
				|| raw.contains("@=Summary") || raw.contains("@=summary") || raw.contains("@_noDocInfo_@")))
			message = text.getMailText(true).replaceAll("@_noDocInfo_@", "");
		else
			message = text.getMailText(true)
				+ "\n-----\n" + doc.getDocumentInfo()
				+ "\n" + doc.getSummary();
		File pdf = doc != null && m_node.isAttachedDocumentToEmail() ? doc.createPDF() : null;
		//
		MClient client = MClient.get(doc.getCtx(), doc.getAD_Client_ID());
		//	Explicit EMail
		sendEMail(client, 0, m_node.getEMail(), subject, message, pdf, text.isHtml());
		//	Recipient Type
		String recipient = m_node.getEMailRecipient();
		//	email to document user
		if (recipient == null || recipient.length() == 0)
			sendEMail(client, doc.getDoc_User_ID(), null, subject, message, pdf, text.isHtml());
		else if (recipient.equals(MWFNode.EMAILRECIPIENT_DocumentBusinessPartner))
		{
			int index = m_po.get_ColumnIndex("AD_User_ID");
			if (index > 0)
			{
				Object oo = m_po.get_Value(index);
				if (oo instanceof Integer)
				{
					int AD_User_ID = ((Integer)oo).intValue();
					if (AD_User_ID != 0)
						sendEMail(client, AD_User_ID, null, subject, message, pdf, text.isHtml());
					else
						log.fine("No User in Document");
				}
				else
					log.fine("Empty User in Document");
			}
			else
				log.fine("No User Field in Document");
		}
		else if (recipient.equals(MWFNode.EMAILRECIPIENT_DocumentOwner))
			sendEMail(client, doc.getDoc_User_ID(), null, subject, message, pdf, text.isHtml());
		else if (recipient.equals(MWFNode.EMAILRECIPIENT_WFResponsible))
		{
			MWFResponsible resp = getResponsible();
			if (resp.isInvoker())
				sendEMail(client, doc.getDoc_User_ID(), null, subject, message, pdf, text.isHtml());
			else if (resp.isHuman())
				sendEMail(client, resp.getAD_User_ID(), null, subject, message, pdf, text.isHtml());
			else if (resp.isRole())
			{
				MRole role = resp.getRole();
				if (role != null)
				{
					MUser[] users = MUser.getWithRole(role);
					for (int i = 0; i < users.length; i++)
						sendEMail(client, users[i].getAD_User_ID(), null, subject, message, pdf, text.isHtml());
				}
			}
			else if (resp.isOrganization())
			{
				MOrgInfo org = MOrgInfo.get(getCtx(), m_po.getAD_Org_ID(), get_TrxName());
				if (org.getSupervisor_ID() == 0) {
					if (log.isLoggable(Level.FINE)) log.fine("No Supervisor for AD_Org_ID=" + m_po.getAD_Org_ID());
				} else {
					sendEMail(client, org.getSupervisor_ID(), null, subject, message, pdf, text.isHtml());
				}
			}
		}
	}	//	sendEMail
	/**
	 * 	Send EMail
	 *	@param client client
	 *	@param AD_User_ID user
	 *	@param email email string
	 *	@param subject subject
	 *	@param message message
	 *	@param pdf attachment
	 *  @param  isHtml isHtml
	 */
	private void sendEMail (MClient client, int AD_User_ID, String email,
		String subject, String message, File pdf, boolean isHtml)
	{
		if (AD_User_ID != 0)
		{
			MUser user = new MUser(getCtx(), AD_User_ID, get_TrxName());
			email = user.getEMail();
			if (email != null && email.length() > 0)
			{
				email = email.trim();
				if (!m_emails.contains(email))
				{
					client.sendEMail(null, user, subject, message, pdf,isHtml);
					m_emails.add(email);
				}
			}
			else
				if (log.isLoggable(Level.INFO)) log.info("No EMail for User " + user.getName());
		}
		else if (email != null && email.length() > 0)
		{
			//	Just one
			if (email.indexOf(';') == -1)
			{
				email = email.trim();
				if (!m_emails.contains(email))
				{
					client.sendEMail(email, subject, message, pdf, isHtml);
					m_emails.add(email);
				}
				return;
			}
			//	Multiple EMail
			StringTokenizer st = new StringTokenizer(email, ";");
			while (st.hasMoreTokens())
			{
				String email1 = st.nextToken().trim();
				if (email1.length() == 0)
					continue;
				if (!m_emails.contains(email1))
				{
					client.sendEMail(email1, subject, message, pdf, isHtml);
					m_emails.add(email1);
				}
			}
		}
	}	//	sendEMail
	/**
	 * 	Get Process Activity (Event) History
	 *	@return history in html format
	 */
	public String getHistoryHTML()
	{
		SimpleDateFormat format = DisplayType.getDateFormat(DisplayType.DateTime);
		StringBuilder sb = new StringBuilder();
		MWFEventAudit[] events = MWFEventAudit.get(getCtx(), getAD_WF_Process_ID(), get_TrxName());
		for (int i = 0; i < events.length; i++)
		{
			MWFEventAudit audit = events[i];
			sb.append("");
			sb.append(format.format(audit.getCreated()))
				.append(" ")
				.append(getHTMLpart("b", audit.getNodeName()))
				.append(": ")
				.append(getHTMLpart(null, audit.getDescription()))
				.append(getHTMLpart("i", audit.getTextMsg()));
			sb.append("
");
		}
		return sb.toString();
	}	//	getHistory
	/**
	 * 	Get HTML part
	 *	@param tag HTML tag
	 *	@param content content
	 *	@return content 
	 */
	private StringBuffer getHTMLpart (String tag, String content)
	{
		StringBuffer sb = new StringBuffer();
		if (content == null || content.length() == 0)
			return sb;
		if (tag != null && tag.length() > 0)
			sb.append("<").append(tag).append(">");
		sb.append(content);
		if (tag != null && tag.length() > 0)
			sb.append("").append(tag).append(">");
		return sb;
	}	//	getHTMLpart
	/**
	 * 	Does the underlying PO object has a PDF Attachment
	 * 	@return true if there is a pdf attachment
	 */
	public boolean isPdfAttachment()
	{
		if (getPO() == null)
			return false;
		return m_po.isPdfAttachment();
	}	//	isPDFAttachment
	/**
	 * 	Get PDF Attachment of underlying PO object
	 *	@return pdf data or null
	 */
	public byte[] getPdfAttachment()
	{
		if (getPO() == null)
			return null;
		return m_po.getPdfAttachment();
	}	//	getPdfAttachment
	/**
	 * 	String Representation
	 *	@return info
	 */
	@Override
	public String toString ()
	{
		StringBuilder sb = new StringBuilder ("MWFActivity[");
		sb.append(get_ID()).append(",Node=");
		if (m_node == null)
			sb.append(getAD_WF_Node_ID());
		else
			sb.append(m_node.getName());
		sb.append(",State=").append(getWFState())
			.append(",AD_User_ID=").append(getAD_User_ID())
			.append(",").append(getCreated())
			.append ("]");
		return sb.toString ();
	} 	//	toString
	/**
	 * 	User String Representation.
	 * 	Suspended: Approve it (Joe)
	 *	@return info
	 */
	public String toStringX ()
	{
		StringBuilder sb = new StringBuilder();
		sb.append(getWFStateText())
			.append(": ").append(getNode().getName());
		if (getAD_User_ID() > 0)
		{
			MUser user = MUser.get(getCtx(), getAD_User_ID());
			sb.append(" (").append(user.getName()).append(")");
		}
		return sb.toString();
	}	//	toStringX
	/**
	 * 	Get Document Summary
	 *	@return PO Summary
	 */
	public String getSummary()
	{
		PO po = getPO();
		if (po == null)
			return null;
		StringBuilder sb = new StringBuilder();
		String[] keyColumns = po.get_KeyColumns();
		if ((keyColumns != null) && (keyColumns.length > 0))
			sb.append(Msg.getElement(getCtx(), keyColumns[0])).append(" ");
		int index = po.get_ColumnIndex("DocumentNo");
		if (index != -1)
			sb.append(po.get_Value(index)).append(": ");
		index = po.get_ColumnIndex("SalesRep_ID");
		Integer sr = null;
		if (index != -1)
			sr = (Integer)po.get_Value(index);
		else
		{
			index = po.get_ColumnIndex("AD_User_ID");
			if (index != -1)
				sr = (Integer)po.get_Value(index);
		}
		if (sr != null)
		{
			MUser user = MUser.get(getCtx(), sr.intValue());
			if (user != null)
				sb.append(user.getName()).append(" ");
		}
		//
		index = po.get_ColumnIndex("C_BPartner_ID");
		if (index != -1)
		{
			Integer bp = (Integer)po.get_Value(index);
			if (bp != null)
			{
				MBPartner partner = MBPartner.get(getCtx(), bp.intValue());
				if (partner != null)
					sb.append(partner.getName()).append(" ");
			}
		}
		return sb.toString();
	}	//	getSummary
	/**
	 * Set up transaction event listener to set workflow state to StateEngine.STATE_Completed in the transaction
	 * after commit event. 
	 */
	private void prepareCommitEvent()
	{
		Trx trx = null;
		if (get_TrxName() == null)
		{
			return; // no transaction, nothing to commit
		}
		MWFActivity activity = new MWFActivity (getCtx(), get_ID(), get_TrxName());
		trx = Trx.get(get_TrxName(), true);
		trx.addTrxEventListener(new TrxListener(activity));		
	}
	/** Transaction event listener to update workflow activity in the after commit event */
	static class TrxListener implements TrxEventListener {
		private MWFActivity activity;
		protected TrxListener(MWFActivity activity) {
			this.activity = activity;
		}
		
		@Override
		public void afterRollback(Trx trx, boolean success) {
		}
		
		@Override
		public void afterCommit(Trx trx, boolean success) {
			if (success) {
				trx.removeTrxEventListener(this);
				activity.setWFState (StateEngine.STATE_Completed);
			}
		}
		
		@Override
		public void afterClose(Trx trx) {
			trx.removeTrxEventListener(this);
		}		
	}
	/**
	 * Get where clause to get the pending activities related to a User (unprocessed and suspended).