/******************************************************************************
 * 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 java.io.*;
import java.math.*;
import java.rmi.*;
import java.sql.*;
import java.text.*;
import java.util.*;
import java.util.logging.*;
import org.compiere.db.*;
import org.compiere.interfaces.*;
import org.compiere.model.*;
import org.compiere.print.*;
import org.compiere.process.*;
import org.compiere.util.*;

/**
 *	Workflow Activity Model.
 *	Controlled by WF Process: 
 *		set Node - startWork 
 *	
 *  @author Jorg Janke
 *  @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
{
	/**
	 * 	Get Activities for table/tecord 
	 *	@param ctx context
	 *	@param AD_Table_ID table
	 *	@param Record_ID record
	 *	@param activeOnly if true only not processed records are returned
	 *	@return activity
	 */
	public static MWFActivity[] get (Properties ctx, int AD_Table_ID, int Record_ID, boolean activeOnly)
	{
		ArrayList<MWFActivity> list = new ArrayList<MWFActivity>();
		PreparedStatement pstmt = null;
		String sql = "SELECT * FROM AD_WF_Activity WHERE AD_Table_ID=? AND Record_ID=?";
		if (activeOnly)
			sql += " AND Processed<>'Y'";
		sql += " ORDER BY AD_WF_Activity_ID";
		try
		{
			pstmt = DB.prepareStatement (sql, null);
			pstmt.setInt (1, AD_Table_ID);
			pstmt.setInt (2, Record_ID);
			ResultSet rs = pstmt.executeQuery ();
			while (rs.next ())
				list.add(new MWFActivity (ctx, rs, null));
			rs.close ();
			pstmt.close ();
			pstmt = null;
		}
		catch (Exception e)
		{
			s_log.log(Level.SEVERE, sql, e);
		}
		try
		{
			if (pstmt != null)
				pstmt.close ();
			pstmt = null;
		}
		catch (Exception e)
		{
			pstmt = null;
		}
		MWFActivity[] retValue = new MWFActivity[list.size ()];
		list.toArray (retValue);
		return retValue;
	}	//	get

	/**
	 * 	Get Active Info
	 * 	@param ctx context
	 *	@param AD_Table_ID table
	 *	@param Record_ID record
	 *	@return activity summary
	 */
	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;
		//
		StringBuffer sb = new StringBuffer();
		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

	/**	Static Logger	*/
	private static CLogger	s_log	= CLogger.getCLogger (MWFActivity.class);

	
	/**************************************************************************
	 * 	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 Contructor
	 *	@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());
		//	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);
		save();
		//
		m_audit = new MWFEventAudit(this);
		m_audit.save();
		//
		m_process = process;
	}	//	MWFActivity

	/**	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;
	/** Post Immediate Candidate	*/
	private DocAction			m_postImmediate = null;
	/** List of email recipients	*/
	private ArrayList<String> 	m_emails = new ArrayList<String>();

	/**************************************************************************
	 * 	Get State
	 *	@return state
	 */
	public StateEngine getState()
	{
		return m_state;
	}	//	getState

	/**
	 * 	Set Activity State
	 *	@param WFState
	 */
	public void setWFState (String WFState)
	{
		if (m_state == null)
			m_state = new StateEngine (getWFState());
		if (m_state.isClosed())
			return;
		if (getWFState().equals(WFState))
			return;
		//
		if (m_state.isValidNewState(WFState))
		{
			String oldState = getWFState();
			log.fine(oldState + "->"+ WFState + ", Msg=" + getTextMsg()); 
			super.setWFState (WFState);
			m_state = new StateEngine (getWFState());
			save();			//	closed in MWFProcess.checkActivities()
			updateEventAudit();			
			
			//	Inform Process
			if (m_process == null)
				m_process = new MWFProcess (getCtx(), getAD_WF_Process_ID(), null);
			m_process.checkActivities(m_trx == null ? null : m_trx.getTrxName());
		}
		else
		{
			String msg = "Set WFState - Ignored Invalid Transformation - New=" 
				+ WFState + ", Current=" + getWFState(); 
			log.log(Level.SEVERE, msg);
			Trace.printStack();
			setTextMsg(msg);
			save();
		}
	}	//	setWFState
	
	/**
	 * 	Is Activity closed
	 * 	@return true if closed
	 */
	public boolean isClosed()
	{
		return m_state.isClosed();
	}	//	isClosed
	
	
	/**************************************************************************
	 * 	Update Event Audit
	 */
	private void updateEventAudit()
	{
	//	log.fine("");
		getEventAudit();
		m_audit.setTextMsg(getTextMsg());
		m_audit.setWFState(getWFState());
		if (m_newValue != null)
			m_audit.setNewValue(m_newValue);
		if (m_state.isClosed())
		{
			m_audit.setEventType(MWFEventAudit.EVENTTYPE_ProcessCompleted);
			long ms = System.currentTimeMillis() - m_audit.getCreated().getTime();
			m_audit.setElapsedTimeMS(new BigDecimal(ms));
		}
		else
			m_audit.setEventType(MWFEventAudit.EVENTTYPE_StateChanged);
		m_audit.save();
	}	//	updateEventAudit

	/**
	 * 	Get/Create Event Audit
	 * 	@return event
	 */
	public MWFEventAudit getEventAudit()
	{
		if (m_audit != null)
			return m_audit;
		MWFEventAudit[] events = MWFEventAudit.get(getCtx(), getAD_WF_Process_ID(), getAD_WF_Node_ID());
		if (events == null || events.length == 0)
			m_audit = new MWFEventAudit(this);
		else
			m_audit = events[events.length-1];		//	last event
		return m_audit;
	}	//	getEventAudit
	
	
	/**************************************************************************
	 * 	Get Persistent Object in Transaction
	 * 	@param trx transaction
	 *	@return po
	 */
	public PO getPO (Trx trx)
	{
		if (m_po != null)
			return m_po;
		
		MTable table = MTable.get (getCtx(), getAD_Table_ID());
		if (trx != null)
			m_po = table.getPO(getRecord_ID(), trx.getTrxName());
		else
			m_po = table.getPO(getRecord_ID(), null);
		return m_po;
	}	//	getPO
	
	/**
	 * 	Get Persistent Object
	 *	@return po
	 */
	public PO getPO()
	{
		return getPO(null);
	}	//	getPO
	
	/**
	 * 	Get PO AD_Client_ID
	 *	@return client of PO
	 */
	public int getPO_AD_Client_ID()
	{
		if (m_po == null)
			getPO(null);
		if (m_po != null)
			return m_po.getAD_Client_ID();
		return 0;
	}	//	getPO_AD_Client_ID
	
	/**
	 * 	Get Attribute Value (based on Node) of PO
	 *	@return Attribute Value or null
	 */
	public Object getAttributeValue()
	{
		MWFNode node = getNode();
		if (node == null)
			return null;
		int AD_Column_ID = node.getAD_Column_ID();
		if (AD_Column_ID == 0)
			return null;
		PO po = getPO();
		if (po.get_ID() == 0)
			return null;
		return po.get_ValueOfColumn(AD_Column_ID);
	}	//	getAttributeValue
	
	/**
	 * 	Is SO Trx
	 *	@return SO Trx or of not found true
	 */
	public boolean isSOTrx()
	{
		PO po = getPO();
		if (po.get_ID() == 0)
			return true;
		//	Is there a Column?
		int index = po.get_ColumnIndex("IsSOTrx");
		if (index < 0)
		{
			if (po.get_TableName().startsWith("M_"))
				return false;
			return true;
		}
		//	we have a column
		try
		{
			Boolean IsSOTrx = (Boolean)po.get_Value(index);
			return IsSOTrx.booleanValue();
		}
		catch (Exception e)
		{
			log.log(Level.SEVERE, "", e);
		}
		return true;
	}	//	isSOTrx
	
	
	/**************************************************************************
	 * 	Set AD_WF_Node_ID.
	 * 	(Re)Set to Not Started
	 *	@param AD_WF_Node_ID now node
	 */
	public void setAD_WF_Node_ID (int AD_WF_Node_ID)
	{
		if (AD_WF_Node_ID == 0)
			throw new IllegalArgumentException("Workflow Node is not defined");
		super.setAD_WF_Node_ID (AD_WF_Node_ID);
		//
		if (!WFSTATE_NotStarted.equals(getWFState()))
		{
			super.setWFState(WFSTATE_NotStarted);
			m_state = new StateEngine (getWFState());
		}
		if (isProcessed())
			setProcessed (false);
	}	//	setAD_WF_Node_ID
	
	/**
	 * 	Get WF Node
	 *	@return node
	 */
	public MWFNode getNode()
	{
		if (m_node == null)
			m_node = MWFNode.get (getCtx(), getAD_WF_Node_ID());
		return m_node;
	}	//	getNode
	
	/**
	 * 	Get WF Node Name
	 *	@return translated node name
	 */
	public String getNodeName()
	{
		return getNode().getName(true);
	}	//	getNodeName

	/**
	 * 	Get Node Description
	 *	@return translated node description
	 */
	public String getNodeDescription()
	{
		return getNode().getDescription(true);
	}	//	getNodeDescription
	
	/**
	 * 	Get Node Help
	 *	@return translated node help
	 */
	public String getNodeHelp()
	{
		return getNode().getHelp(true);
	}	//	getNodeHelp
	
	
	/**
	 * 	Is this an user Approval step?
	 *	@return true if User Approval
	 */
	public boolean isUserApproval()
	{
		return getNode().isUserApproval();
	}	//	isNodeApproval

	/**
	 * 	Is this a Manual user step?
	 *	@return true if Window/Form/..
	 */
	public boolean isUserManual()
	{
		return getNode().isUserManual();
	}	//	isUserManual

	/**
	 * 	Is this a user choice step?
	 *	@return true if User Choice
	 */
	public boolean isUserChoice()
	{
		return getNode().isUserChoice();
	}	//	isUserChoice

	
	/**
	 * 	Set Text Msg (add to existing)
	 *	@param TextMsg
	 */
	public void setTextMsg (String TextMsg)
	{
		if (TextMsg == null || TextMsg.length() == 0)
			return;
		String oldText = getTextMsg();
		if (oldText == null || oldText.length() == 0)
			super.setTextMsg (Util.trimSize(TextMsg,1000));
		else if (TextMsg != null && TextMsg.length() > 0)
			super.setTextMsg (Util.trimSize(oldText + "\n - " + TextMsg,1000));
	}	//	setTextMsg	
	
	/**
	 * 	Add to Text Msg
	 *	@param obj some object
	 */
	public void addTextMsg (Object obj)
	{
		if (obj == null)
			return;
		//
		StringBuffer TextMsg = new StringBuffer (obj.toString());
		if (obj instanceof Exception)
		{
			Exception ex = (Exception)obj;
			while (ex != null)
			{
				StackTraceElement[] st = ex.getStackTrace();
				for (int i = 0; i < st.length; i++)
				{
					StackTraceElement ste = st[i];
					if (i == 0 || ste.getClassName().startsWith("org.compiere"))
						TextMsg.append(" (").append(i).append("): ")
							.append(ste.toString())
							.append("\n");
				}
				if (ex.getCause() instanceof Exception)
					ex = (Exception)ex.getCause();
				else
					ex = null;
			}
		}
		//
		String oldText = getTextMsg();
		if (oldText == null || oldText.length() == 0)
			super.setTextMsg(Util.trimSize(TextMsg.toString(),1000));
		else if (TextMsg != null && TextMsg.length() > 0)
			super.setTextMsg(Util.trimSize(oldText + "\n - " + TextMsg.toString(),1000));
	}	//	setTextMsg	
	
	/**
	 * 	Get WF State text
	 *	@return state text
	 */
	public String getWFStateText ()
	{
		return MRefList.getListName(getCtx(), WFSTATE_AD_Reference_ID, getWFState());
	}	//	getWFStateText
	
	/**
	 * 	Set Responsible and User from Process / Node
	 *	@param process process
	 */
	private void setResponsible (MWFProcess process)
	{
		//	Responsible
		int AD_WF_Responsible_ID = getNode().getAD_WF_Responsible_ID();
		if (AD_WF_Responsible_ID == 0)	//	not defined on Node Level
			AD_WF_Responsible_ID = process.getAD_WF_Responsible_ID();
		setAD_WF_Responsible_ID (AD_WF_Responsible_ID);
		MWFResponsible resp = getResponsible();
		
		//	User - Directly responsible
		int AD_User_ID = resp.getAD_User_ID();
		//	Invoker - get Sales Rep or last updater of document
		if (AD_User_ID == 0 && resp.isInvoker())
			AD_User_ID = process.getAD_User_ID();
		//
		setAD_User_ID(AD_User_ID);
	}	//	setResponsible
	
	/**
	 * 	Get Responsible
	 *	@return responsible
	 */
	public MWFResponsible getResponsible()
	{
		MWFResponsible resp = MWFResponsible.get(getCtx(), getAD_WF_Responsible_ID());
		return resp;
	}	//	isInvoker

	/**
	 * 	Is Invoker (no user & no role)
	 *	@return true if invoker
	 */
	public boolean isInvoker()
	{
		return getResponsible().isInvoker();
	}	//	isInvoker

	/**
	 * 	Get Approval User.
	 * 	If the returned user is the same, the document is approved.
	 *	@param AD_User_ID starting User
	 *	@param C_Currency_ID currency
	 *	@param amount amount
	 *	@param AD_Org_ID document organization
	 *	@param ownDocument the document is owned by AD_User_ID
	 *	@return AD_User_ID - if -1 no Approver
	 */
	public int getApprovalUser (int AD_User_ID, 
			int C_Currency_ID, BigDecimal amount, 
			int AD_Org_ID, boolean ownDocument)
	{
		//	Nothing to approve
		if (amount == null 
			|| amount.signum() == 0)
			return AD_User_ID;
		
		//	Starting user
		MUser user = MUser.get(getCtx(), AD_User_ID);
		log.info("For User=" + user 
			+ ", Amt=" + amount 
			+ ", Own=" + ownDocument);

		MUser oldUser = null;
		while (user != null)
		{
			if (user.equals(oldUser))
			{
				log.info("Loop - " + user.getName());
				return -1;
			}
			oldUser = user;
			log.fine("User=" + user.getName());
			//	Get Roles of User
			MRole[] roles = user.getRoles(AD_Org_ID);
			for (int i = 0; i < roles.length; i++)
			{
				MRole role = roles[i];
				if (ownDocument && !role.isCanApproveOwnDoc())
					continue;	//	find a role with allows them to approve own
				BigDecimal roleAmt = role.getAmtApproval();
				if (roleAmt == null || roleAmt.signum() == 0)
					continue;
				if (C_Currency_ID != role.getC_Currency_ID()	
					&& role.getC_Currency_ID() != 0)			//	No currency = amt only
				{
					roleAmt =  MConversionRate.convert(getCtx(),//	today & default rate 
						roleAmt, role.getC_Currency_ID(), 
						C_Currency_ID, getAD_Client_ID(), AD_Org_ID);
					if (roleAmt == null || roleAmt.signum() == 0)
						continue;
				}
				boolean approved = amount.compareTo(roleAmt) <= 0;
				log.fine("Approved=" + approved 
					+ " - User=" + user.getName() + ", Role=" + role.getName()
					+ ", ApprovalAmt=" + roleAmt);
				if (approved)
					return user.getAD_User_ID();
			}
			
			//	**** Find next User 
			//	Get Supervisor
			if (user.getSupervisor_ID() != 0)
			{
				user = MUser.get(getCtx(), user.getSupervisor_ID());
				log.fine("Supervisor: " + user.getName()); 
			}
			else
			{
				log.fine("No Supervisor"); 
				MOrg org = MOrg.get (getCtx(), AD_Org_ID);
				MOrgInfo orgInfo = org.getInfo();
				//	Get Org Supervisor
				if (orgInfo.getSupervisor_ID() != 0)
				{
					user = MUser.get(getCtx(), orgInfo.getSupervisor_ID());
					log.fine("Org=" + org.getName() + ",Supervisor: " + user.getName()); 
				}
				else
				{
					log.fine("No Org Supervisor"); 
					//	Get Parent Org Supervisor
					if (orgInfo.getParent_Org_ID() != 0)
					{
						org = MOrg.get (getCtx(), orgInfo.getParent_Org_ID());
						orgInfo = org.getInfo();
						if (orgInfo.getSupervisor_ID() != 0)
						{
							user = MUser.get(getCtx(), orgInfo.getSupervisor_ID());
							log.fine("Parent Org Supervisor: " + user.getName()); 
						}
					}
				}
			}	//	No Supervisor
			
		}	//	while there is a user to approve
		
		log.fine("No user found"); 
		return -1;
	}	//	getApproval

	
	/**************************************************************************
	 * 	Execute Work.
	 * 	Called from MWFProcess.startNext
	 * 	Feedback to Process via setWFState -> checkActivities
	 */
	public void run()
	{
		log.info ("Node=" + getNode());
		m_newValue = null;
		if (!m_state.isValidAction(StateEngine.ACTION_Start))
		{
			setTextMsg("State=" + getWFState() + " - cannot start");
			setWFState(StateEngine.STATE_Terminated);
			return;
		}
		//
		setWFState(StateEngine.STATE_Running);
		m_trx = Trx.get(Trx.createTrxName("WF"), true);
		//
		try
		{
			if (getNode().get_ID() == 0)
			{
				setTextMsg("Node not found - AD_WF_Node_ID=" + getAD_WF_Node_ID());
				setWFState(StateEngine.STATE_Aborted);
				return;
			}
			//	Do Work
			/****	Trx Start	****/
			boolean done = performWork(m_trx);
			            //begin vpj-cd e-evolution 03/08/2005 PostgreSQL
			// Reason: When is execute setWFState create a transaction for same table is generate a clicle Trx 			
			//         is cause that PostgreSQL wait into transaction idle
			//setWFState (done ? StateEngine.STATE_Completed : StateEngine.STATE_Suspended);
			//end vpj-cd e-evolution 03/08/2005 PostgreSQL
			/****	Trx End		****/
			m_trx.commit(true);
			m_trx.close();
			m_trx = null;
                        //begin vpj-cd e-evolution 03/08/2005 PostgreSQL
                        //Reason: setWFState moved for the first trx be finished
			setWFState (done ? StateEngine.STATE_Completed : StateEngine.STATE_Suspended);
			//end vpj-cd e-evolution 03/08/2005 PostgreSQL
			if (m_postImmediate != null)
				postImmediate();
		}
		catch (Exception e)
		{
			log.log(Level.WARNING, "" + getNode(), e);
			/****	Trx Rollback	****/
			m_trx.rollback();
			m_trx.close();
			m_trx = null;
			//
			if (e.getCause() != null)
				log.log(Level.WARNING, "Cause", e.getCause());
			String processMsg = e.getLocalizedMessage();
			if (processMsg == null || processMsg.length() == 0)
				processMsg = e.getMessage();
			setTextMsg(processMsg);
			addTextMsg(e);
			setWFState (StateEngine.STATE_Terminated);	//	unlocks
			//	Set Document Status 
			if (m_po != null && m_docStatus != null)
			{
				m_po.load(null);
				DocAction doc = (DocAction)m_po;
				doc.setDocStatus(m_docStatus);
				m_po.save();
			}
		}
		m_trx = null;
	}	//	run
	
	
	/**
	 * 	Perform Work.
	 * 	Set Text Msg.
	 * 	@param trx transaction
	 *	@return true if completed, false otherwise
	 *	@throws Exception if error
	 */
	private boolean performWork (Trx trx) throws Exception
	{
		log.info (m_node + " [" + trx.getTrxName() + "]");
		m_postImmediate = null;
		m_docStatus = null;
		if (m_node.getPriority() != 0)		//	overwrite priority if defined
			setPriority(m_node.getPriority());
		String action = m_node.getAction();
		
		/******	Sleep (Start/End)			******/
		if (MWFNode.ACTION_WaitSleep.equals(action))
		{
			log.fine("Sleep:WaitTime=" + m_node.getWaitTime());
			if (m_node.getWaitingTime() == 0)
				return true;	//	done
			Calendar cal = Calendar.getInstance();
			cal.add(m_node.getDurationCalendarField(), m_node.getWaitTime());
			setEndWaitTime(new Timestamp(cal.getTimeInMillis()));
			return false;		//	not done
		}
		
		/******	Document Action				******/
		else if (MWFNode.ACTION_DocumentAction.equals(action))
		{
			log.fine("DocumentAction=" + m_node.getDocAction());
			getPO(trx);
			if (m_po == null)
				throw new Exception("Persistent Object not found - AD_Table_ID=" 
					+ getAD_Table_ID() + ", Record_ID=" + getRecord_ID());
			m_po.set_TrxName(trx.getTrxName());
			boolean success = false;
			String processMsg = null;
			DocAction doc = null;
			if (m_po instanceof DocAction)
			{
				doc = (DocAction)m_po;
				//
				success = doc.processIt (m_node.getDocAction());	//	** Do the work
				setTextMsg(doc.getSummary());
				processMsg = doc.getProcessMsg();
				m_docStatus = doc.getDocStatus();
				//	Post Immediate
				if (success && DocAction.ACTION_Complete.equals(m_node.getDocAction()))
				{
					MClient client = MClient.get(doc.getCtx(), doc.getAD_Client_ID());
					if (client.isPostImmediate())
						m_postImmediate = doc;
				}
				//
				if (m_process != null)
					m_process.setProcessMsg(processMsg);
			}
			else
				throw new IllegalStateException("Persistent Object not DocAction - "
					+ m_po.getClass().getName()
					+ " - AD_Table_ID=" + getAD_Table_ID() + ", Record_ID=" + getRecord_ID());
			//
			if (!m_po.save())
			{
				success = false;
				processMsg = "SaveError";
			}
			if (!success)
			{
				if (processMsg == null || processMsg.length() == 0)
				{
					processMsg = "PerformWork Error - " + m_node.toStringX();
					if (doc != null)	//	problem: status will be rolled back
						processMsg += " - DocStatus=" + doc.getDocStatus();
				}
				throw new Exception(processMsg);
			}
			return success;
		}	//	DocumentAction
		
		/******	Report						******/
		else if (MWFNode.ACTION_AppsReport.equals(action))
		{
			log.fine("Report:AD_Process_ID=" + m_node.getAD_Process_ID());
			//	Process
			MProcess process = MProcess.get(getCtx(), m_node.getAD_Process_ID());
			if (!process.isReport() || process.getAD_ReportView_ID() == 0)
				throw new IllegalStateException("Not a Report AD_Process_ID=" + m_node.getAD_Process_ID());
			//
			ProcessInfo pi = new ProcessInfo (m_node.getName(true), m_node.getAD_Process_ID(),
				getAD_Table_ID(), getRecord_ID());
			pi.setAD_User_ID(getAD_User_ID());
			pi.setAD_Client_ID(getAD_Client_ID());
			MPInstance pInstance = new MPInstance(process, getRecord_ID());
			fillParameter(pInstance, trx);
			pi.setAD_PInstance_ID(pInstance.getAD_PInstance_ID());
			//	Report
			ReportEngine re = ReportEngine.get(getCtx(), pi);
			if (re == null)
				throw new IllegalStateException("Cannot create Report AD_Process_ID=" + m_node.getAD_Process_ID());
			File report = re.getPDF();
			//	Notice
			int AD_Message_ID = 753;		//	HARDCODED WorkflowResult
			MNote note = new MNote(getCtx(), AD_Message_ID, getAD_User_ID(), trx.getTrxName());
			note.setTextMsg(m_node.getName(true));
			note.setDescription(m_node.getDescription(true));
			note.setRecord(getAD_Table_ID(), getRecord_ID());
			note.save();
			//	Attachment
			MAttachment attachment = new MAttachment (getCtx(), MNote.Table_ID, note.getAD_Note_ID(), get_TrxName());
			attachment.addEntry(report);
			attachment.setTextMsg(m_node.getName(true));
			attachment.save();
			return true;
		}
		
		/******	Process						******/
		else if (MWFNode.ACTION_AppsProcess.equals(action))
		{
			log.fine("Process:AD_Process_ID=" + m_node.getAD_Process_ID());
			//	Process
			MProcess process = MProcess.get(getCtx(), m_node.getAD_Process_ID());
			MPInstance pInstance = new MPInstance(process, getRecord_ID());
			fillParameter(pInstance, trx);
			//
			ProcessInfo pi = new ProcessInfo (m_node.getName(true), m_node.getAD_Process_ID(),
				getAD_Table_ID(), getRecord_ID());
			pi.setAD_User_ID(getAD_User_ID());
			pi.setAD_Client_ID(getAD_Client_ID());
			pi.setAD_PInstance_ID(pInstance.getAD_PInstance_ID());
			return process.processIt(pi, trx);
		}
		
		/******	TODO Start Task				******/
		else if (MWFNode.ACTION_AppsTask.equals(action))
		{
			log.warning ("Task:AD_Task_ID=" + m_node.getAD_Task_ID());
		}
		
		/******	EMail						******/
		else if (MWFNode.ACTION_EMail.equals(action))
		{
			log.fine ("EMail:EMailRecipient=" + m_node.getEMailRecipient());
			getPO(trx);
			if (m_po == null)
				throw new Exception("Persistent Object not found - AD_Table_ID=" 
					+ getAD_Table_ID() + ", Record_ID=" + getRecord_ID());
			if (m_po instanceof DocAction)
			{
				m_emails = new ArrayList<String>();
				sendEMail();
				setTextMsg(m_emails.toString());
			}
			return true;	//	done
		}	//	EMail
		
		/******	Set Variable				******/
		else if (MWFNode.ACTION_SetVariable.equals(action))
		{
			String value = m_node.getAttributeValue();
			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);
		}	//	SetVariable
		
		/******	TODO Start WF Instance		******/
		else if (MWFNode.ACTION_SubWorkflow.equals(action))
		{
			log.warning ("Workflow:AD_Workflow_ID=" + m_node.getAD_Workflow_ID());
		}
		
		/******	User Choice					******/
		else if (MWFNode.ACTION_UserChoice.equals(action))
		{
			log.fine("UserChoice:AD_Column_ID=" + m_node.getAD_Column_ID());
			//	Approval
			if (m_node.isUserApproval() 
				&& getPO() instanceof DocAction)
			{
				DocAction doc = (DocAction)m_po;
				boolean autoApproval = false;
				//	Approval Hierarchy
				if (isInvoker())
				{
					//	Set Approver
					int startAD_User_ID = getAD_User_ID();
					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
					//	same user = approved
					autoApproval = startAD_User_ID == nextAD_User_ID;
					if (!autoApproval)
						setAD_User_ID(nextAD_User_ID);
				}
				else	//	fixed Approver
				{
					MWFResponsible resp = getResponsible();
					autoApproval = resp.getAD_User_ID() == getAD_User_ID();
					if (!autoApproval && resp.getAD_User_ID() != 0)
						setAD_User_ID(resp.getAD_User_ID());
				}
				if (autoApproval
					&& doc.processIt(DocAction.ACTION_Approve)
					&& doc.save())
					return true;	//	done
			}	//	approval
			return false;	//	wait for user
		}
		/******	User Workbench				******/
		else if (MWFNode.ACTION_UserWorkbench.equals(action))
		{
			log.fine("Workbench:?");
			return false;
		}
		/******	User Form					******/
		else if (MWFNode.ACTION_UserForm.equals(action))
		{
			log.fine("Form:AD_Form_ID=" + m_node.getAD_Form_ID());
			return false;
		}
		/******	User Window					******/
		else if (MWFNode.ACTION_UserWindow.equals(action))
		{
			log.fine("Window:AD_Window_ID=" + m_node.getAD_Window_ID());
			return false;
		}
		//
		throw new IllegalArgumentException("Invalid Action (Not Implemented) =" + action);
	}	//	performWork
	
	/**
	 * 	Set Variable
	 *	@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) throws Exception
	{
		m_newValue = null;
		getPO();
		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 = new Boolean("Y".equals(value));
		else if (DisplayType.isNumeric(displayType))
			dbValue = new BigDecimal (value);
		else
			dbValue = value;
		m_po.set_ValueOfColumn(getNode().getAD_Column_ID(), dbValue);
		m_po.save();
		if (!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
	{
		//	Check if user approves own document when a role is reponsible
		if (getNode().isUserApproval() && getPO() instanceof DocAction)
		{
			DocAction doc = (DocAction)m_po;
			MUser user = new MUser (getCtx(), AD_User_ID, null);
			MRole[] roles = user.getRoles(m_po.getAD_Org_ID());
			boolean canApproveOwnDoc = false;
			for (int r = 0; r < roles.length; r++)
			{
				if (roles[r].isCanApproveOwnDoc())
				{
					canApproveOwnDoc = true;
					break;
				}	//	found a role which allows to approve own document
			}
			if (!canApproveOwnDoc)
			{
				String info = user.getName() + " cannot approve own document " + doc;
				addTextMsg(info);
				log.fine(info);
				return false;		//	ignore
			}
		}
		
		setWFState (StateEngine.STATE_Running);
		setAD_User_ID(AD_User_ID);
		boolean ok = setVariable (value, displayType, textMsg);
		if (!ok)
			return false;

		String newState = StateEngine.STATE_Completed;
		//	Approval
		if (getNode().isUserApproval() && getPO() instanceof DocAction)
		{
			DocAction doc = (DocAction)m_po;
			try
			{
				//	Not pproved
				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 = getAD_User_ID();
						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 ("Cannot Approve - No Approver");
							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.save();
			}
			catch (Exception e)
			{
				newState = StateEngine.STATE_Terminated;
				setTextMsg ("User Choice: " + e.toString());
				log.log(Level.WARNING, "", e);
			}
			// Send Approval Notification
			if (newState.equals(StateEngine.STATE_Aborted)) {
				MUser to = new MUser(getCtx(), doc.getDoc_User_ID(), null);
				String NotificationType = to.getNotificationType();
				
				// send email
				if (MUser.NOTIFICATIONTYPE_EMail.equals(NotificationType)
						|| MUser.NOTIFICATIONTYPE_EMailPlusNotice.equals(NotificationType)) {
					MClient client = MClient.get(getCtx(), doc.getAD_Client_ID());
					client.sendEMail(doc.getDoc_User_ID(), Msg.getMsg(getCtx(), "NotApproved")
							+ ": " + doc.getDocumentNo(), doc.getSummary() + "\n"
							+ doc.getProcessMsg() + "\n" + getTextMsg(), null);
				}

				// Send Note
				if (MUser.NOTIFICATIONTYPE_Notice.equals(NotificationType)
						|| MUser.NOTIFICATIONTYPE_EMailPlusNotice.equals(NotificationType)) {
					MNote note = new MNote(getCtx(), "NotApproved", doc.getDoc_User_ID(), null);
					note.setTextMsg(doc.getSummary() + "\n" + doc.getProcessMsg() + "\n"
							+ getTextMsg());
					note.save();
				}
			}
		}
		setWFState (newState);
		return ok;
	}	//	setUserChoice
	
	/**
	 * 	Forward To
	 *	@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);
		save();
		//	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.save();
		//	Create new one
		m_audit = new MWFEventAudit(this);
		m_audit.save();
		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 Parameter
	 *	@param pInstance process instance
	 * 	@param trx transaction
	 */
	private void fillParameter(MPInstance pInstance, Trx trx)
	{
		getPO(trx);
		//
		MWFNodePara[] nParams = m_node.getParameters();
		MPInstancePara[] iParams = pInstance.getParameters();
		for (int pi = 0; pi < iParams.length; pi++)
		{
			MPInstancePara iPara = iParams[pi];
			for (int np = 0; np < nParams.length; np++)
			{
				MWFNodePara nPara = nParams[np];
				if (iPara.getParameterName().equals(nPara.getAttributeName()))
				{
					String variableName = nPara.getAttributeValue();
					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);
							break;
						}
						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 + ")");
								break;
							}
							else
								value = env;
						}
					}	//	@variable@
					
					//	No Value
					if (value == null)
					{
						if (nPara.isMandatory())
							log.warning(nPara.getAttributeName() 
								+ " - empty - mandatory!");
						else
							log.fine(nPara.getAttributeName() 
								+ " - empty");
						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);
							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);
							log.fine(nPara.getAttributeName()
								+ " = " + variableName + " (=" + ts + "=)");
						}
						else
						{
							iPara.setP_String(value.toString());
							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

	/**
	 * 	Post Immediate
	 */
	private void postImmediate()
	{
		if (CConnection.get().isAppsServerOK(false))
		{
			try
			{
				Server server = CConnection.get().getServer();
				if (server != null)
				{
					String error = server.postImmediate(Env.getCtx(), 
						m_postImmediate.getAD_Client_ID(),
						m_postImmediate.get_Table_ID(), m_postImmediate.get_ID(), 
						true, null);
					m_postImmediate.get_Logger().config("Server: " + error == null ? "OK" : error);
					return;
				}
				else
					m_postImmediate.get_Logger().config("NoAppsServer");
			}
			catch (RemoteException e)
			{
				m_postImmediate.get_Logger().config("(RE) " + e.getMessage());
			}
			catch (Exception e)
			{
				m_postImmediate.get_Logger().config("(ex) " + e.getMessage());
			}
		}
	}	//	PostImmediate
	
	
	/*********************************
	 * 	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 = doc.getDocumentInfo()  
			+ ": " + text.getMailHeader();
		String message = text.getMailText(true)
			+ "\n-----\n" + doc.getDocumentInfo()
			+ "\n" + doc.getSummary();
		File pdf = doc.createPDF();
		//
		MClient client = MClient.get(doc.getCtx(), doc.getAD_Client_ID());
		
		//	Explicit EMail
		sendEMail(client, 0, m_node.getEMail(), subject, message, pdf);
		//	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); 
		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);
					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); 
		else if (recipient.equals(MWFNode.EMAILRECIPIENT_WFResponsible))
		{
			MWFResponsible resp = getResponsible();
			if (resp.isInvoker())
				sendEMail(client, doc.getDoc_User_ID(), null, subject, message, pdf); 
			else if (resp.isHuman())
				sendEMail(client, resp.getAD_User_ID(), null, subject, message, pdf); 
			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); 
				}
			}
			else if (resp.isOrganization())
			{
				MOrgInfo org = MOrgInfo.get(getCtx(), m_po.getAD_Org_ID());
				if (org.getSupervisor_ID() == 0)
					log.fine("No Supervisor for AD_Org_ID=" + m_po.getAD_Org_ID());
				else
					sendEMail(client, org.getSupervisor_ID(), null, subject, message, pdf); 
			}
		}
	}	//	sendEMail
	
	/**
	 * 	Send actual EMail
	 *	@param client client
	 *	@param AD_User_ID user
	 *	@param email email string
	 *	@param subject subject
	 *	@param message message
	 *	@param pdf attachment
	 */
	private void sendEMail (MClient client, int AD_User_ID, String email,
		String subject, String message, File pdf)
	{
		if (AD_User_ID != 0)
		{
			MUser user = MUser.get(getCtx(), AD_User_ID);
			email = user.getEMail();
			if (email != null && email.length() > 0)
			{
				email = email.trim();
				if (!m_emails.contains(email))
				{
					client.sendEMail(null, user, subject, message, pdf);
					m_emails.add(email);
				}
			}
			else
				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);
					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);
					m_emails.add(email1);
				}
			}
		}
	}	//	sendEMail
	
	/**************************************************************************
	 * 	Get Process Activity (Event) History
	 *	@return history
	 */
	public String getHistoryHTML()
	{
		SimpleDateFormat format = DisplayType.getDateFormat(DisplayType.DateTime);
		StringBuffer sb = new StringBuffer();
		MWFEventAudit[] events = MWFEventAudit.get(getCtx(), getAD_WF_Process_ID());
		for (int i = 0; i < events.length; i++)
		{
			MWFEventAudit audit = events[i];
		//	sb.append("<p style=\"width:400\">");
			sb.append("<p>");
			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("</p>");
		}
		return sb.toString();
	}	//	getHistory
	
	/**
	 * 	Get HTML part
	 *	@param tag HTML tag
	 *	@param content content
	 *	@return <tag>content</tag>
	 */
	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 have 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
	 */
	public String toString ()
	{
		StringBuffer sb = new StringBuffer ("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 ()
	{
		StringBuffer sb = new StringBuffer();
		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
	
}	//	MWFActivity
