Skip to main content

Invoice Approval System

This scenario demonstrates a complete invoice approval workflow with custom records, scripts, and workflow configuration.


Business Requirements

┌─────────────────────────────────────────────────────────────────────────────┐
│ INVOICE APPROVAL REQUIREMENTS │
└─────────────────────────────────────────────────────────────────────────────┘

✓ Invoices over $1,000 require approval
✓ Route to different approvers based on amount thresholds
✓ Track approval history with audit trail
✓ Send email notifications at each stage
✓ Allow rejection with reason
✓ Provide approval dashboard for approvers
✓ Lock invoice during approval process

APPROVAL THRESHOLDS:
┌─────────────────────────────────────────────────────────────────────┐
│ Amount Range │ Approver │
├─────────────────────────┼──────────────────────────────────────────┤
│ $1,000 - $5,000 │ Manager │
│ $5,001 - $25,000 │ Director │
│ $25,001+ │ CFO │
└─────────────────────────┴──────────────────────────────────────────┘

Solution Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│ SOLUTION COMPONENTS │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ CUSTOM OBJECTS │
│ ────────────────────────────────────────────────────────────────│
│ • customlist_approval_status - Status values │
│ • customrecord_approval_request - Approval tracking │
│ • customrecord_approval_config - Threshold configuration │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ CUSTOM FIELDS │
│ ────────────────────────────────────────────────────────────────│
│ • custbody_approval_status - Current status │
│ • custbody_approver - Assigned approver │
│ • custbody_approved_by - Who approved │
│ • custbody_approved_date - When approved │
│ • custbody_rejection_reason - Why rejected │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ SCRIPTS │
│ ────────────────────────────────────────────────────────────────│
│ • User Event - Auto-submit for approval on save │
│ • Client Script - Validation and UI control │
│ • Suitelet - Approval dashboard │
│ • Workflow Action - Custom approval processing │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ WORKFLOW │
│ ────────────────────────────────────────────────────────────────│
│ • States: Draft, Pending, Approved, Rejected │
│ • Transitions: Submit, Approve, Reject, Resubmit │
└──────────────────────────────────────────────────────────────────┘

Implementation Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│ APPROVAL PROCESS FLOW │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────────┐
│ Invoice Created │
│ (Amount > $1K) │
└────────┬─────────┘


┌──────────────────────────────────────────────────────────────────┐
│ USER EVENT (afterSubmit) │
│ ────────────────────────────────────────────────────────────────│
│ 1. Check if amount > threshold │
│ 2. Find appropriate approver based on amount │
│ 3. Create approval request record │
│ 4. Set status to "Pending Approval" │
│ 5. Send notification email │
└──────────────────────────────┬───────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ WORKFLOW ENTERS: Pending Approval State │
│ ────────────────────────────────────────────────────────────────│
│ • Add Approve/Reject buttons │
│ • Lock record for editing │
│ • Display approval instructions │
└──────────────────────────────┬───────────────────────────────────┘

┌────────────────┼────────────────┐
│ │
[Approve] [Reject]
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ WORKFLOW ACTION │ │ WORKFLOW ACTION │
│ ──────────────────── │ │ ──────────────────── │
│ • Set approved by │ │ • Set rejection reason │
│ • Set approved date │ │ • Notify creator │
│ • Update request │ │ • Unlock record │
│ • Notify creator │ │ │
│ • Unlock record │ │ │
└────────────┬────────────┘ └────────────┬────────────┘
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ STATE: Approved │ │ STATE: Rejected │
│ (Final) │ │ (Allow Resubmit) │
└─────────────────────────┘ └─────────────────────────┘

Step 1: Custom List - Approval Status

src/Objects/customlist_approval_status.xml

<?xml version="1.0" encoding="UTF-8"?>
<customlist scriptid="customlist_approval_status">
<name>Approval Status</name>
<description>Status values for invoice approval</description>
<isordered>T</isordered>

<customvalues>
<customvalue scriptid="val_draft">
<value>Draft</value>
<abbreviation>D</abbreviation>
</customvalue>
<customvalue scriptid="val_pending">
<value>Pending Approval</value>
<abbreviation>P</abbreviation>
</customvalue>
<customvalue scriptid="val_approved">
<value>Approved</value>
<abbreviation>A</abbreviation>
</customvalue>
<customvalue scriptid="val_rejected">
<value>Rejected</value>
<abbreviation>R</abbreviation>
</customvalue>
</customvalues>
</customlist>

Step 2: Custom Record - Approval Request

src/Objects/customrecord_approval_request.xml

<?xml version="1.0" encoding="UTF-8"?>
<customrecordtype scriptid="customrecord_approval_request">
<recordname>Approval Request</recordname>
<description>Tracks invoice approval requests</description>

<includename>T</includename>
<showid>T</showid>
<shownotes>T</shownotes>
<accesstype>USEPERMISSIONLIST</accesstype>

<customrecordcustomfields>
<customrecordcustomfield scriptid="custrecord_ar_invoice">
<label>Invoice</label>
<fieldtype>SELECT</fieldtype>
<selectrecordtype>-30</selectrecordtype>
<ismandatory>T</ismandatory>
</customrecordcustomfield>

<customrecordcustomfield scriptid="custrecord_ar_amount">
<label>Amount</label>
<fieldtype>CURRENCY</fieldtype>
</customrecordcustomfield>

<customrecordcustomfield scriptid="custrecord_ar_status">
<label>Status</label>
<fieldtype>SELECT</fieldtype>
<selectrecordtype>[customlist_approval_status]</selectrecordtype>
<defaultvalue>2</defaultvalue>
</customrecordcustomfield>

<customrecordcustomfield scriptid="custrecord_ar_approver">
<label>Assigned Approver</label>
<fieldtype>SELECT</fieldtype>
<selectrecordtype>-4</selectrecordtype>
</customrecordcustomfield>

<customrecordcustomfield scriptid="custrecord_ar_submitted_by">
<label>Submitted By</label>
<fieldtype>SELECT</fieldtype>
<selectrecordtype>-4</selectrecordtype>
</customrecordcustomfield>

<customrecordcustomfield scriptid="custrecord_ar_submitted_date">
<label>Submitted Date</label>
<fieldtype>DATETIME</fieldtype>
</customrecordcustomfield>

<customrecordcustomfield scriptid="custrecord_ar_actioned_by">
<label>Actioned By</label>
<fieldtype>SELECT</fieldtype>
<selectrecordtype>-4</selectrecordtype>
</customrecordcustomfield>

<customrecordcustomfield scriptid="custrecord_ar_actioned_date">
<label>Actioned Date</label>
<fieldtype>DATETIME</fieldtype>
</customrecordcustomfield>

<customrecordcustomfield scriptid="custrecord_ar_notes">
<label>Notes</label>
<fieldtype>TEXTAREA</fieldtype>
</customrecordcustomfield>
</customrecordcustomfields>

<permissions>
<permission>
<permittedlevel>FULL</permittedlevel>
<permittedrole>ADMINISTRATOR</permittedrole>
</permission>
</permissions>
</customrecordtype>

Step 3: Custom Fields on Invoice

src/Objects/custbody_approval_status.xml

<?xml version="1.0" encoding="UTF-8"?>
<transactionbodycustomfield scriptid="custbody_approval_status">
<label>Approval Status</label>
<fieldtype>SELECT</fieldtype>
<selectrecordtype>[customlist_approval_status]</selectrecordtype>
<displaytype>INLINE</displaytype>
<showinlist>T</showinlist>
<appliestosale>F</appliestosale>
<appliestopurchase>T</appliestopurchase>
<appliestovendorbill>T</appliestovendorbill>
<storevalue>T</storevalue>
</transactionbodycustomfield>

Step 4: User Event Script

src/FileCabinet/SuiteScripts/UserEvents/invoice_approval_ue.js

/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
* @NModuleScope SameAccount
* @description Handles invoice approval routing
*/
define(['N/record', 'N/search', 'N/email', 'N/runtime', 'N/url', 'N/log'],
(record, search, email, runtime, url, log) => {

// Configuration
const CONFIG = {
thresholdAmount: 1000,
statusFieldId: 'custbody_approval_status',
approverFieldId: 'custbody_approver',
approvalRecordType: 'customrecord_approval_request',
statusList: {
draft: 1,
pending: 2,
approved: 3,
rejected: 4
}
};

/**
* After Submit - Check if approval needed
*/
const afterSubmit = (context) => {
if (context.type !== context.UserEventType.CREATE &&
context.type !== context.UserEventType.EDIT) {
return;
}

try {
const invoice = context.newRecord;
const invoiceId = invoice.id;
const total = parseFloat(invoice.getValue({ fieldId: 'total' })) || 0;

// Check if approval is needed
if (total < CONFIG.thresholdAmount) {
log.debug('Approval Check', 'Amount below threshold, no approval needed');
return;
}

// Check current status
const currentStatus = invoice.getValue({ fieldId: CONFIG.statusFieldId });
if (currentStatus === CONFIG.statusList.pending ||
currentStatus === CONFIG.statusList.approved) {
log.debug('Approval Check', 'Already in approval or approved');
return;
}

// Find appropriate approver
const approver = findApprover(total, invoice);

if (!approver) {
log.error('Approval Error', 'No approver found for amount: ' + total);
return;
}

// Create approval request
const requestId = createApprovalRequest(invoice, approver, total);

// Update invoice
record.submitFields({
type: invoice.type,
id: invoiceId,
values: {
[CONFIG.statusFieldId]: CONFIG.statusList.pending,
[CONFIG.approverFieldId]: approver.id
}
});

// Send notification
sendApprovalNotification(invoice, approver);

log.audit('Approval Submitted', `Invoice ${invoiceId} submitted for approval to ${approver.name}`);

} catch (e) {
log.error('afterSubmit Error', e.message);
}
};

/**
* Find approver based on amount thresholds
*/
const findApprover = (amount, invoice) => {
// Search for approval threshold configuration
const configSearch = search.create({
type: 'customrecord_approval_config',
filters: [
['custrecord_ac_min_amount', 'lessthanorequalto', amount],
'AND',
['isinactive', 'is', 'F']
],
columns: [
'custrecord_ac_approver',
search.createColumn({
name: 'custrecord_ac_min_amount',
sort: search.Sort.DESC
})
]
});

let approver = null;

configSearch.run().each((result) => {
approver = {
id: result.getValue('custrecord_ac_approver'),
name: result.getText('custrecord_ac_approver')
};
return false; // Get first (highest matching threshold)
});

// Fallback to default approver
if (!approver) {
const script = runtime.getCurrentScript();
const defaultApprover = script.getParameter({
name: 'custscript_default_approver'
});

if (defaultApprover) {
const empSearch = search.lookupFields({
type: search.Type.EMPLOYEE,
id: defaultApprover,
columns: ['entityid']
});

approver = {
id: defaultApprover,
name: empSearch.entityid
};
}
}

return approver;
};

/**
* Create approval request record
*/
const createApprovalRequest = (invoice, approver, amount) => {
const request = record.create({
type: CONFIG.approvalRecordType
});

request.setValue({ fieldId: 'custrecord_ar_invoice', value: invoice.id });
request.setValue({ fieldId: 'custrecord_ar_amount', value: amount });
request.setValue({ fieldId: 'custrecord_ar_status', value: CONFIG.statusList.pending });
request.setValue({ fieldId: 'custrecord_ar_approver', value: approver.id });
request.setValue({ fieldId: 'custrecord_ar_submitted_by', value: runtime.getCurrentUser().id });
request.setValue({ fieldId: 'custrecord_ar_submitted_date', value: new Date() });

return request.save();
};

/**
* Send approval notification email
*/
const sendApprovalNotification = (invoice, approver) => {
const invoiceUrl = url.resolveRecord({
recordType: invoice.type,
recordId: invoice.id,
isEditMode: false
});

const invoiceNumber = invoice.getValue({ fieldId: 'tranid' });
const vendor = invoice.getText({ fieldId: 'entity' });
const total = invoice.getValue({ fieldId: 'total' });

const emailBody = `
<html>
<body style="font-family: Arial, sans-serif;">
<h2>Invoice Approval Required</h2>
<p>An invoice requires your approval:</p>

<table style="border-collapse: collapse; margin: 20px 0;">
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Invoice #:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${invoiceNumber}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Vendor:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${vendor}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Amount:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">$${parseFloat(total).toFixed(2)}</td>
</tr>
</table>

<p>
<a href="${invoiceUrl}"
style="background: #4CAF50; color: white; padding: 10px 20px;
text-decoration: none; border-radius: 4px; display: inline-block;">
Review Invoice
</a>
</p>
</body>
</html>
`;

email.send({
author: runtime.getCurrentUser().id,
recipients: [approver.id],
subject: `Approval Required: Invoice ${invoiceNumber}`,
body: emailBody
});
};

return { afterSubmit };
});

Step 5: Client Script

src/FileCabinet/SuiteScripts/ClientScripts/invoice_approval_cs.js

/**
* @NApiVersion 2.1
* @NScriptType ClientScript
* @NModuleScope SameAccount
* @description Client-side validation for invoice approval
*/
define(['N/currentRecord', 'N/dialog', 'N/log'],
(currentRecord, dialog, log) => {

const STATUS = {
draft: 1,
pending: 2,
approved: 3,
rejected: 4
};

/**
* Page initialization
*/
const pageInit = (context) => {
const rec = context.currentRecord;
const status = rec.getValue({ fieldId: 'custbody_approval_status' });

// Disable editing if pending approval
if (status === STATUS.pending) {
disableEditableFields(rec);
showApprovalMessage();
}
};

/**
* Save record validation
*/
const saveRecord = (context) => {
const rec = context.currentRecord;
const status = rec.getValue({ fieldId: 'custbody_approval_status' });

// Prevent saving if pending (unless through workflow)
if (status === STATUS.pending) {
dialog.alert({
title: 'Cannot Save',
message: 'This invoice is pending approval and cannot be edited.'
});
return false;
}

// Validate amount if changed
const total = parseFloat(rec.getValue({ fieldId: 'total' })) || 0;
const originalTotal = parseFloat(rec.getValue({ fieldId: 'custbody_original_amount' })) || 0;

if (status === STATUS.approved && Math.abs(total - originalTotal) > 0.01) {
const proceed = confirm(
'Changing the amount on an approved invoice will require re-approval. Continue?'
);

if (!proceed) {
return false;
}

// Reset status to draft for re-approval
rec.setValue({
fieldId: 'custbody_approval_status',
value: STATUS.draft
});
}

return true;
};

/**
* Disable editable fields during approval
*/
const disableEditableFields = (rec) => {
const fieldsToDisable = [
'entity',
'trandate',
'duedate',
'memo',
'account'
];

fieldsToDisable.forEach(fieldId => {
try {
const field = rec.getField({ fieldId: fieldId });
if (field) {
field.isDisabled = true;
}
} catch (e) {
// Field may not exist
}
});
};

/**
* Show approval status message
*/
const showApprovalMessage = () => {
// This would typically be done via a banner field
// set by beforeLoad user event
};

return {
pageInit,
saveRecord
};
});

Step 6: Approval Dashboard Suitelet

src/FileCabinet/SuiteScripts/Suitelets/approval_dashboard_sl.js

/**
* @NApiVersion 2.1
* @NScriptType Suitelet
* @NModuleScope SameAccount
* @description Approval dashboard for approvers
*/
define(['N/ui/serverWidget', 'N/search', 'N/record', 'N/runtime', 'N/redirect', 'N/log'],
(serverWidget, search, record, runtime, redirect, log) => {

const onRequest = (context) => {
if (context.request.method === 'GET') {
showDashboard(context);
} else {
processAction(context);
}
};

/**
* Display approval dashboard
*/
const showDashboard = (context) => {
const form = serverWidget.createForm({
title: 'Invoice Approval Dashboard'
});

// Add action buttons
form.addSubmitButton({ label: 'Process Selected' });

form.addButton({
id: 'custpage_refresh',
label: 'Refresh',
functionName: 'location.reload()'
});

// Add action dropdown
const actionField = form.addField({
id: 'custpage_action',
type: serverWidget.FieldType.SELECT,
label: 'Action'
});
actionField.addSelectOption({ value: '', text: '- Select Action -' });
actionField.addSelectOption({ value: 'approve', text: 'Approve Selected' });
actionField.addSelectOption({ value: 'reject', text: 'Reject Selected' });

// Rejection reason (shown for reject action)
form.addField({
id: 'custpage_rejection_reason',
type: serverWidget.FieldType.TEXTAREA,
label: 'Rejection Reason (if rejecting)'
});

// Create sublist for pending approvals
const sublist = form.addSublist({
id: 'custpage_approvals',
type: serverWidget.SublistType.LIST,
label: 'Pending Approvals'
});

// Add checkbox for selection
sublist.addField({
id: 'custpage_select',
type: serverWidget.FieldType.CHECKBOX,
label: 'Select'
});

sublist.addField({
id: 'custpage_request_id',
type: serverWidget.FieldType.TEXT,
label: 'Request ID'
}).updateDisplayType({ displayType: serverWidget.FieldDisplayType.HIDDEN });

sublist.addField({
id: 'custpage_invoice_id',
type: serverWidget.FieldType.TEXT,
label: 'Invoice ID'
}).updateDisplayType({ displayType: serverWidget.FieldDisplayType.HIDDEN });

sublist.addField({
id: 'custpage_tranid',
type: serverWidget.FieldType.TEXT,
label: 'Invoice #'
});

sublist.addField({
id: 'custpage_vendor',
type: serverWidget.FieldType.TEXT,
label: 'Vendor'
});

sublist.addField({
id: 'custpage_amount',
type: serverWidget.FieldType.CURRENCY,
label: 'Amount'
});

sublist.addField({
id: 'custpage_date',
type: serverWidget.FieldType.DATE,
label: 'Submitted Date'
});

sublist.addField({
id: 'custpage_submitted_by',
type: serverWidget.FieldType.TEXT,
label: 'Submitted By'
});

// Populate pending approvals
populatePendingApprovals(sublist);

context.response.writePage(form);
};

/**
* Populate pending approvals for current user
*/
const populatePendingApprovals = (sublist) => {
const currentUser = runtime.getCurrentUser().id;

const approvalSearch = search.create({
type: 'customrecord_approval_request',
filters: [
['custrecord_ar_status', 'anyof', 2], // Pending
'AND',
['custrecord_ar_approver', 'anyof', currentUser]
],
columns: [
'internalid',
'custrecord_ar_invoice',
'custrecord_ar_amount',
'custrecord_ar_submitted_date',
'custrecord_ar_submitted_by'
]
});

let line = 0;

approvalSearch.run().each((result) => {
const invoiceId = result.getValue('custrecord_ar_invoice');

// Get invoice details
let invoiceDetails = {};
try {
invoiceDetails = search.lookupFields({
type: 'vendorbill',
id: invoiceId,
columns: ['tranid', 'entity']
});
} catch (e) {
log.error('Invoice Lookup', e.message);
}

sublist.setSublistValue({
id: 'custpage_request_id',
line: line,
value: result.id
});

sublist.setSublistValue({
id: 'custpage_invoice_id',
line: line,
value: invoiceId
});

sublist.setSublistValue({
id: 'custpage_tranid',
line: line,
value: invoiceDetails.tranid || ''
});

sublist.setSublistValue({
id: 'custpage_vendor',
line: line,
value: invoiceDetails.entity?.[0]?.text || ''
});

sublist.setSublistValue({
id: 'custpage_amount',
line: line,
value: result.getValue('custrecord_ar_amount') || 0
});

sublist.setSublistValue({
id: 'custpage_date',
line: line,
value: result.getValue('custrecord_ar_submitted_date') || ''
});

sublist.setSublistValue({
id: 'custpage_submitted_by',
line: line,
value: result.getText('custrecord_ar_submitted_by') || ''
});

line++;
return true;
});
};

/**
* Process approval/rejection action
*/
const processAction = (context) => {
const params = context.request.parameters;
const action = params.custpage_action;
const rejectionReason = params.custpage_rejection_reason || '';

if (!action) {
redirect.toSuitelet({
scriptId: runtime.getCurrentScript().id,
deploymentId: runtime.getCurrentScript().deploymentId
});
return;
}

let lineCount = 0;
let processed = 0;

// Count lines
while (params[`custpage_approvals_row_${lineCount}`] !== undefined ||
params[`custpage_select_${lineCount}`] !== undefined) {
lineCount++;
if (lineCount > 1000) break; // Safety limit
}

// Process selected items
for (let i = 0; i < lineCount; i++) {
const selected = params[`custpage_select_${i}`] === 'T';

if (selected) {
const requestId = params[`custpage_request_id_${i}`];
const invoiceId = params[`custpage_invoice_id_${i}`];

try {
if (action === 'approve') {
processApproval(requestId, invoiceId);
} else if (action === 'reject') {
processRejection(requestId, invoiceId, rejectionReason);
}
processed++;
} catch (e) {
log.error('Process Error', `Request ${requestId}: ${e.message}`);
}
}
}

// Show result
const form = serverWidget.createForm({
title: 'Processing Complete'
});

form.addField({
id: 'custpage_message',
type: serverWidget.FieldType.INLINEHTML,
label: ' '
}).defaultValue = `
<div style="padding: 20px; background: #e8f5e9; border-radius: 4px; margin: 20px 0;">
<h3 style="color: #2e7d32;">Success</h3>
<p>${processed} invoice(s) have been ${action === 'approve' ? 'approved' : 'rejected'}.</p>
<p><a href="#" onclick="history.back()">Return to Dashboard</a></p>
</div>
`;

context.response.writePage(form);
};

/**
* Process approval
*/
const processApproval = (requestId, invoiceId) => {
const currentUser = runtime.getCurrentUser().id;
const now = new Date();

// Update request record
record.submitFields({
type: 'customrecord_approval_request',
id: requestId,
values: {
'custrecord_ar_status': 3, // Approved
'custrecord_ar_actioned_by': currentUser,
'custrecord_ar_actioned_date': now
}
});

// Update invoice
record.submitFields({
type: 'vendorbill',
id: invoiceId,
values: {
'custbody_approval_status': 3, // Approved
'custbody_approved_by': currentUser,
'custbody_approved_date': now
}
});

log.audit('Invoice Approved', `Invoice ${invoiceId} approved by user ${currentUser}`);
};

/**
* Process rejection
*/
const processRejection = (requestId, invoiceId, reason) => {
const currentUser = runtime.getCurrentUser().id;
const now = new Date();

// Update request record
record.submitFields({
type: 'customrecord_approval_request',
id: requestId,
values: {
'custrecord_ar_status': 4, // Rejected
'custrecord_ar_actioned_by': currentUser,
'custrecord_ar_actioned_date': now,
'custrecord_ar_notes': reason
}
});

// Update invoice
record.submitFields({
type: 'vendorbill',
id: invoiceId,
values: {
'custbody_approval_status': 4, // Rejected
'custbody_rejection_reason': reason
}
});

log.audit('Invoice Rejected', `Invoice ${invoiceId} rejected by user ${currentUser}`);
};

return { onRequest };
});

Step 7: Script Deployment

src/Objects/customscript_invoice_approval_ue.xml

<?xml version="1.0" encoding="UTF-8"?>
<usereventscript scriptid="customscript_invoice_approval_ue">
<name>Invoice Approval User Event</name>
<scriptfile>[/SuiteScripts/UserEvents/invoice_approval_ue.js]</scriptfile>
<description>Handles invoice approval routing</description>
<isinactive>F</isinactive>
<notifyowner>T</notifyowner>

<scriptcustomfields>
<scriptcustomfield scriptid="custscript_default_approver">
<label>Default Approver</label>
<fieldtype>SELECT</fieldtype>
<selectrecordtype>-4</selectrecordtype>
</scriptcustomfield>
</scriptcustomfields>

<scriptdeployments>
<scriptdeployment scriptid="customdeploy_invoice_approval_ue">
<status>RELEASED</status>
<recordtype>vendorbill</recordtype>
<isdeployed>T</isdeployed>
<loglevel>DEBUG</loglevel>
</scriptdeployment>
</scriptdeployments>
</usereventscript>

Testing Checklist

┌─────────────────────────────────────────────────────────────────────────────┐
│ TESTING CHECKLIST │
└─────────────────────────────────────────────────────────────────────────────┘

DEPLOYMENT:
☐ Deploy all custom objects
☐ Deploy all scripts
☐ Configure approval thresholds
☐ Set default approver

FUNCTIONAL TESTS:
☐ Create invoice < $1,000 - No approval needed
☐ Create invoice $1,000-$5,000 - Routes to manager
☐ Create invoice $5,001-$25,000 - Routes to director
☐ Create invoice > $25,000 - Routes to CFO
☐ Approve invoice - Status updates correctly
☐ Reject invoice - Status updates, reason saved
☐ Resubmit rejected invoice
☐ Email notifications sent correctly
☐ Dashboard shows pending approvals
☐ Bulk approve/reject from dashboard

EDGE CASES:
☐ Edit approved invoice - Re-approval required
☐ No approver configured - Uses default
☐ Approver is inactive - Handles gracefully

Next Steps