Sign Up

Sign Up to our social questions and Answers Engine to ask questions, answer people’s questions, and connect with other people.

Have an account? Sign In

Have an account? Sign In Now

Sign In

Login to our social questions & Answers Engine to ask questions answer people’s questions & connect with other people.

Sign Up Here

Forgot Password?

Don't have account, Sign Up Here

Forgot Password

Lost your password? Please enter your email address. You will receive a link and will create a new password via email.

Have an account? Sign In Now

You must login to ask a question.

Forgot Password?

Need An Account, Sign Up Here

Please briefly explain why you feel this question should be reported.

Please briefly explain why you feel this answer should be reported.

Please briefly explain why you feel this user should be reported.

Sign InSign Up

The NetSuite Pro

The NetSuite Pro Logo The NetSuite Pro Logo

The NetSuite Pro Navigation

  • Home
  • About Us
  • Tutorials
    • NetSuite Scripting
    • NetSuite Customization
    • NetSuite Integration
    • NetSuite Advanced PDF Templates
    • NetSuite Reporting & Analytics Guide
    • Real-World NetSuite Examples
  • Blog
  • Contact Us
Search
Ask A Question

Mobile menu

Close
Ask A Question
  • Home
  • About Us
  • Tutorials
    • NetSuite Scripting
    • NetSuite Customization
    • NetSuite Integration
    • NetSuite Advanced PDF Templates
    • NetSuite Reporting & Analytics Guide
    • Real-World NetSuite Examples
  • Blog
  • Contact Us
Home/ Real-World NetSuite Examples/Sales Order Edit Guard (Price Drops & Discount Caps)

Sales Order Edit Guard (Price Drops & Discount Caps)

πŸ’Ό Business problem

Edits to approved Sales Orders can silently reduce revenue: a rep changes a line rate or adds a big header discount. Finance only finds out after invoicing. You want automatic guardrails with clear error messages and a role-based bypass for managers.


🧠 Approach

  • User Event (beforeSubmit) on Sales Order for EDIT/COPY.
  • Compare old vs. new totals and line rates (using context.oldRecord).
  • Enforce 3 rules:
    1. Max total decrease % (e.g., no more than 5% drop vs. saved SO).
    2. Max per-line rate decrease % (e.g., no more than 10% cut on any item).
    3. Max header discount % (e.g., body discountrate ≀ 15%).
  • Bypass for specific roles (e.g., Sales Manager, Admin).
  • Optional: show a single consolidated error listing all violations.

πŸ”§ Script parameters (Deployment)

  • custscript_guard_max_total_drop_pct (float, e.g., 5)
  • custscript_guard_max_line_drop_pct (float, e.g., 10)
  • custscript_guard_max_body_disc_pct (float, e.g., 15)
  • custscript_guard_bypass_roles (text CSV of role IDs, e.g., 3,1013)

Tip: start conservative (higher thresholds), monitor, then tighten.


🧩 User Event (SuiteScript 2.1) β€” block edits that exceed caps

/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 * Title: UE | Sales Order Edit Guard (price/discount caps)
 * Author: The NetSuite Pro
 */
define(['N/runtime','N/error','N/format'], (runtime, error, format) => {

  const P_MAX_TOTAL_DROP = 'custscript_guard_max_total_drop_pct';
  const P_MAX_LINE_DROP  = 'custscript_guard_max_line_drop_pct';
  const P_MAX_HDR_DISC   = 'custscript_guard_max_body_disc_pct';
  const P_BYPASS_ROLES   = 'custscript_guard_bypass_roles';

  const beforeSubmit = (ctx) => {
    // Only apply on SO edit/copy
    if (ctx.newRecord.type !== 'salesorder') return;
    if (![ctx.UserEventType.EDIT, ctx.UserEventType.COPY].includes(ctx.type)) return;

    // Role-based bypass
    const roleId = runtime.getCurrentUser().role;
    const bypass = parseCsv(runtime.getCurrentScript().getParameter({ name: P_BYPASS_ROLES }));
    if (bypass.includes(String(roleId))) return;

    const newRec = ctx.newRecord;
    const oldRec = ctx.oldRecord;
    if (!oldRec) return; // No baseline to compare

    // Params
    const maxTotalDrop = toNum(runtime.getCurrentScript().getParameter({ name: P_MAX_TOTAL_DROP }), 0);
    const maxLineDrop  = toNum(runtime.getCurrentScript().getParameter({ name: P_MAX_LINE_DROP }), 0);
    const maxHdrDisc   = toNum(runtime.getCurrentScript().getParameter({ name: P_MAX_HDR_DISC }), 0);

    const violations = [];

    // --- Rule 1: Total decrease %
    const oldTotal = toNum(oldRec.getValue({ fieldId: 'total' }), 0);
    const newTotal = toNum(newRec.getValue({ fieldId: 'total' }), 0);
    if (oldTotal > 0 && newTotal < oldTotal) {
      const dropPct = ((oldTotal - newTotal) / oldTotal) * 100;
      if (dropPct > maxTotalDrop) {
        violations.push(`Order total decreased by ${round(dropPct)}% (old ${fmt(oldTotal)} β†’ new ${fmt(newTotal)}), exceeds ${maxTotalDrop}%.`);
      }
    }

    // --- Rule 2: Per-line rate decrease %
    const newCount = newRec.getLineCount({ sublistId: 'item' }) || 0;
    const oldCount = oldRec.getLineCount({ sublistId: 'item' }) || 0;
    const lineMax = Math.max(newCount, oldCount);

    for (let i = 0; i < lineMax; i++) {
      // Skip if line types mismatch or not an item line
      const newItem   = safe(newRec, i, 'item');
      const newType   = safe(newRec, i, 'itemtype');
      const oldItem   = safe(oldRec, i, 'item');
      const oldType   = safe(oldRec, i, 'itemtype');

      // Only evaluate when both old & new are real item rows (ignore discount/memo lines)
      const isItemNew = !!newItem && (!newType || newType === 'InvtPart' || newType === 'NonInvtPart' || newType === 'Service' || newType === 'OtherCharge' || newType === 'Assembly');
      const isItemOld = !!oldItem && (!oldType || oldType === 'InvtPart' || oldType === 'NonInvtPart' || oldType === 'Service' || oldType === 'OtherCharge' || oldType === 'Assembly');
      if (!(isItemNew && isItemOld)) continue;

      const oldRate = toNum(oldRec.getSublistValue({ sublistId:'item', fieldId:'rate', line:i }), null);
      const newRate = toNum(newRec.getSublistValue({ sublistId:'item', fieldId:'rate', line:i }), null);
      if (oldRate == null || newRate == null) continue;
      if (newRate < oldRate && oldRate > 0) {
        const dropPct = ((oldRate - newRate) / oldRate) * 100;
        if (dropPct > maxLineDrop) {
          const itemName = newRec.getSublistText({ sublistId:'item', fieldId:'item', line:i }) || `line ${i+1}`;
          violations.push(`Line "${itemName}" rate dropped ${round(dropPct)}% (old ${fmt(oldRate)} β†’ new ${fmt(newRate)}), exceeds ${maxLineDrop}%.`);
        }
      }
    }

    // --- Rule 3: Header discount cap (discountrate like "-10%")
    const dr = (newRec.getValue({ fieldId: 'discountrate' }) || '').toString().trim();
    const pct = parsePercent(dr); // returns positive number, e.g., 10 for "-10%"
    if (pct != null && pct > maxHdrDisc) {
      violations.push(`Header discount ${pct}% exceeds allowed ${maxHdrDisc}%.`);
    }

    if (violations.length) {
      throw error.create({
        name: 'SO_EDIT_GUARD',
        message:
          'Edits exceed allowed limits:\nβ€’ ' + violations.join('\nβ€’ ') +
          '\n\nIf this is intentional, have a manager with bypass role save the change or adjust the caps.',
        notifyOff: false
      });
    }
  };

  // ---------- helpers ----------
  function safe(rec, line, fieldId) {
    try { return rec.getSublistValue({ sublistId:'item', fieldId, line }); } catch (_e) { return null; }
  }
  function toNum(v, dflt) {
    const n = Number(String(v || '').toString().replace(/[^\d.-]/g, ''));
    return isFinite(n) ? n : dflt;
  }
  function round(n) { return Math.round(n * 10) / 10; }
  function fmt(n) { try { return format.format({ value: n, type: format.Type.CURRENCY }); } catch (_e) { return String(n); } }
  function parseCsv(s) {
    return (s || '').split(',').map(x => x.trim()).filter(Boolean);
  }
  function parsePercent(s) {
    if (!s) return null;
    // Accept "-10%", "10%", "-0.1", "0.10", etc. Return absolute percent number
    const str = String(s).replace(/\s/g,'');
    if (str.endsWith('%')) {
      const n = Number(str.slice(0, -1));
      return isFinite(n) ? Math.abs(n) : null;
    }
    const n = Number(str);
    if (!isFinite(n)) return null;
    // If between -1 and 1, assume ratio (e.g., -0.1 => 10%)
    return Math.abs(Math.abs(n) < 1 ? n * 100 : n);
  }

  return { beforeSubmit };
});

What this catches (examples)

  • Rep lowers a line rate by 25% when the cap is 10% β†’ blocked.
  • Someone adds a -30% header discount while the max allowed is 15% β†’ blocked.
  • Order total drops by 12% when the cap is 5% β†’ blocked.
  • Managers on a bypass role can still save the change.

βœ… Testing checklist

  1. Set parameters: 5 / 10 / 15 and add your bypass role IDs.
  2. Edit an SO: reduce a line rate by 20% β†’ expect a readable error with the line name.
  3. Add a header discount -25% β†’ error.
  4. Lower several lines so that total falls > 5% even if each line is within 10% β†’ error shows total rule.
  5. Login as a bypass role β†’ the same edits should save.

πŸ”’ Enhancements (optional)

  • Whitelist fields: allow address/notes changes without triggering total rule (track via a hidden β€œprior total” body field and compare only when price fields changed).
  • Warning mode: convert thrown error to a soft block (set custbody_needs_mgr_approval = T and stop fulfillment via workflow).
  • Email alert: when blocked, email Sales Manager with the attempted changes.
  • Price level guard: block when price is set to Custom unless role is in bypass.
  • CSV/Integration flag: if createdfrom or an integration checkbox is set, relax rules or route for approval.

πŸ“Œ Summary

ControlResult
Per-line rate drop capPrevents deep, line-level underpricing
Total decrease capCatches many small changes that add up
Header discount capStops one-click heavy discounts
Role bypassKeeps managers flexible without slowing reps
Share
  • Facebook

Leave a ReplyCancel reply

Sidebar

Ask A Question

Stats

  • Questions 6
  • Answers 6
  • Best Answers 0
  • Users 2
  • Popular
  • Answers
  • Rocky

    Issue in running a client script in NetSuite SuiteScript 2.0 ...

    • 1 Answer
  • admin

    How can I send an email with an attachment in ...

    • 1 Answer
  • admin

    How do I avoid SSS_USAGE_LIMIT_EXCEEDED in a Map/Reduce script?

    • 1 Answer
  • admin
    admin added an answer The issue is usually caused by following Wrong script file… September 14, 2025 at 10:33 pm
  • admin
    admin added an answer Steps to send an Invoice PDF by email: define(['N/email', 'N/render',… August 28, 2025 at 3:05 am
  • admin
    admin added an answer This error means your script hit NetSuite’s governance usage limit… August 28, 2025 at 3:02 am

Top Members

Rocky

Rocky

  • 1 Question
  • 22 Points
Begginer
admin

admin

  • 5 Questions
  • 2 Points

Trending Tags

clientscript netsuite scripting suitescript

Explore

  • Home
  • Add group
  • Groups page
  • Communities
  • Questions
    • New Questions
    • Trending Questions
    • Must read Questions
    • Hot Questions
  • Polls
  • Tags
  • Badges
  • Users
  • Help

Footer

© 2025 The NetSuite Pro. All Rights Reserved