πΌ Business problem
Finance spends time clicking Approve on lots of tiny, low-risk credit memos (e.g., shipping adjustments, price-match pennies). We want to auto-approve when safe, and leave the rest for manual review.
π§ Approach
- User Event (beforeSubmit) on Credit Memo:
- If memo total β€ threshold (e.g., $100) and created by/edited by an allowed role, set
approvalstatus = Approved
. - Optional: approve only when reason is in an allow-list (custom field or memo text match).
- If memo total β€ threshold (e.g., $100) and created by/edited by an allowed role, set
- Scheduled / Map-Reduce (optional): bulk-approve existing Pending Approval credit memos that meet the same criteria.
If your account doesnβt use Credit Memo Approval Routing, the field may be absent. Script will safely no-op.
π§ Prereqs & parameters
Create deployment parameters so Finance can tune without code changes:
custscript_cm_amount_threshold
(Number) β e.g.,100
custscript_cm_whitelist_roles
(Text CSV of role IDs) β e.g.,3,1042
custscript_cm_reason_field
(Text; body field ID or blank) β e.g.,custbody_credit_reason
custscript_cm_reason_allow_csv
(Text CSV of allowed reason values or keywords) β e.g.,Shipping Adj,Price Match,Rounding
custscript_cm_require_memo
(Checkbox) β require non-empty Memo to auto-approve (T/F)
Optional body field (nice to have):
custbody_cm_auto_approval_note
(Text) β store why it was approved.
π§© User Event (SuiteScript 2.1) β auto-approve on save
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
* Title: UE | Credit Memo Auto-Approval (role + amount + reason)
*/
define(['N/runtime','N/log'], (runtime, log) => {
const P_AMT = 'custscript_cm_amount_threshold';
const P_ROLES = 'custscript_cm_whitelist_roles';
const P_REASON_FIELD = 'custscript_cm_reason_field';
const P_REASON_ALLOW = 'custscript_cm_reason_allow_csv';
const P_REQUIRE_MEMO = 'custscript_cm_require_memo';
const F_APPROVAL = 'approvalstatus'; // 1=Pending Approval, 2=Approved, 3=Rejected (commonly)
const F_MEMO = 'memo';
const F_AUDIT = 'custbody_cm_auto_approval_note'; // optional
const beforeSubmit = (ctx) => {
const rec = ctx.newRecord;
if (rec.type !== 'creditmemo') return;
// Only on create/copy/edit; skip others
if (![ctx.UserEventType.CREATE, ctx.UserEventType.COPY, ctx.UserEventType.EDIT].includes(ctx.type)) return;
try {
// If approval field doesn't exist (no approval routing), gracefully skip
if (!hasField(rec, F_APPROVAL)) return;
// Already approved or rejected? Respect current state.
const currentStatus = safeGet(rec, F_APPROVAL);
if (String(currentStatus) === '2' || String(currentStatus) === '3') return;
const user = runtime.getCurrentUser();
const roleId = String(user.role);
const amtThreshold = toNum(param(P_AMT), 0);
const whitelist = parseCsv(param(P_ROLES)); // role IDs as strings
const reasonField = (param(P_REASON_FIELD) || '').trim();
const allowCsv = parseCsv(param(P_REASON_ALLOW)).map(x => x.toUpperCase());
const requireMemo = (param(P_REQUIRE_MEMO) === 'T');
const total = toNum(safeGet(rec, 'total'), 0);
const memo = (safeGet(rec, F_MEMO) || '').trim();
// Role check
const roleOk = whitelist.length === 0 ? true : whitelist.includes(roleId);
// Amount check
const amountOk = amtThreshold <= 0 ? true : total <= amtThreshold;
// Reason check (optional)
let reasonOk = true;
if (reasonField || allowCsv.length > 0) {
const reasonVal = ((reasonField && hasField(rec, reasonField)) ? safeGet(rec, reasonField) : memo) || '';
if (allowCsv.length > 0) {
const hay = reasonVal.toString().toUpperCase();
reasonOk = allowCsv.some(needle => hay.indexOf(needle) > -1);
}
}
// Memo required?
const memoOk = requireMemo ? memo.length > 0 : true;
if (roleOk && amountOk && reasonOk && memoOk) {
// Approve
safeSet(rec, F_APPROVAL, 2);
if (hasField(rec, F_AUDIT)) {
const note = `Auto-approved on save: role ${roleId}, total ${total}, reason OK: ${reasonOk}`;
safeSet(rec, F_AUDIT, note);
}
} else {
// Leave pending; optionally annotate
if (hasField(rec, F_AUDIT)) {
const why = [
roleOk ? null : 'role not allowed',
amountOk ? null : `amount>${amtThreshold}`,
reasonOk ? null : 'reason not allowed',
memoOk ? null : 'memo required'
].filter(Boolean).join(', ');
if (why) safeSet(rec, F_AUDIT, `Stayed Pending: ${why}`);
}
}
} catch (e) {
log.error('CM Auto-Approval UE error', e);
}
};
// ------------ helpers ------------
function param(name){ return runtime.getCurrentScript().getParameter({ name }) || ''; }
function hasField(rec, fieldId){
try { rec.getValue({ fieldId }); return true; } catch (_e){ return false; }
}
function safeGet(rec, fieldId){
try { return rec.getValue({ fieldId }); } catch (_e){ return null; }
}
function safeSet(rec, fieldId, value){
try { rec.setValue({ fieldId, value }); } catch (_e){}
}
function toNum(v, d){ const n = Number(String(v).replace(/[^\d.-]/g,'')); return isFinite(n) ? n : d; }
function parseCsv(s){ return (s||'').split(',').map(x=>x.trim()).filter(Boolean); }
return { beforeSubmit };
});
(Optional) Nightly bulk auto-approval (Saved Search + MR)
Use a saved search for Pending Approval credit memos under the threshold (and any other filters you like), then approve them in bulk.
Saved Search (Transaction)
- Type = Credit Memo
- Status = Pending Approval
- Amount (Net or Total) β€
{threshold}
- (Optional) Memo contains any of: Shipping Adj / Price Match / Rounding
- (Optional) Created By Role in (your whitelisted roles) β if you track via a custom field or system notes search
Map/Reduce (outline)
getInputData()
loads the search- In
reduce()
, callrecord.submitFields({ type: 'creditmemo', id, values: { approvalstatus: 2 } })
- Wrap with try/catch; log each approval
β Testing checklist
- Turn on approval routing for credit memos (if you use it).
- Set parameters: threshold
100
, roles3,1042
, reasonsShipping Adj,Price Match
. - Create a CM for $25 with memo βShipping adj β damaged boxβ as an allowed role β auto-approved.
- Create a CM for $250 (over threshold) β stays Pending Approval.
- Create a CM for $10 but memo is blank and βRequire Memoβ = T β stays Pending.
- Temporarily remove your role from whitelist β stays Pending.
- (Optional) Run the MR on historical items β confirms bulk approvals.
π Enhancements
- Maintain an Allowlist of Items (e.g., shipping charge item) by internal IDs; approve only when all lines are in the allowlist.
- Set an upper daily limit per role (track totals in a custom record).
- Email Finance when a CM is auto-approved above a smaller βnotifyβ threshold (e.g., > $50).
π Summary
Control | Effect |
---|---|
Role + Amount threshold | Auto-approve low-risk memos instantly |
Reason allow-list | Prevents accidental approvals |
Nightly MR | Cleans up existing pending queue |
Leave a Reply