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/Partial Fulfillment & Automatic Customer Notifications

Partial Fulfillment & Automatic Customer Notifications

💼 Business problem

Customers hate guessing what shipped and what’s delayed. CS teams spend hours replying to “where’s my order?” emails after a partial shipment.
Goal: When an Item Fulfillment posts and it’s not the full order, automatically email the customer with:

  • What shipped (qty, items, tracking)
  • What’s pending/backordered (qty remaining, optional ETA)
  • Friendly copy + links to their order/portal

🧠 Approach overview

  • Trigger: afterSubmit on Item Fulfillment (IF) → type create.
  • Logic: Load the Sales Order (SO) via createdfrom. For each SO line, compare quantity vs quantityfulfilled. If any line is partially fulfilled (fulfilled > 0 and < ordered) or any line remains 0 while others shipped → it’s a partial.
  • Action: Build a simple HTML email summarizing shipped vs. remaining lines. Include tracking pulled from the IF.
  • Opt-in: Only notify when SO checkbox custbody_notify_on_partial is true (so ops can control).

🔧 Prerequisites

  • SO body checkbox: custbody_notify_on_partial (Default = T if you want system-wide).
  • (Optional) SO line date field for ETA: custcol_expected_ship_date (Date).
  • (Optional) Email template file (HTML) in File Cabinet; or use inline HTML (below).
  • Deployment role permissions: Read/Send Email, Load SO/IF, View tracking fields.

✅ User Event (SuiteScript 2.1) — Item Fulfillment afterSubmit

Copy/paste, deploy on Item Fulfillment.

/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 * Title: UE | Email customer on partial fulfillment
 * Author: The NetSuite Pro
 */
define(['N/record','N/search','N/email','N/runtime','N/format','N/log','N/url'],
(record, search, email, runtime, format, log, url) => {

  const SO_NOTIFY_FIELD = 'custbody_notify_on_partial';
  const SO_LINE_ETA = 'custcol_expected_ship_date'; // optional

  const afterSubmit = (ctx) => {
    if (ctx.type !== ctx.UserEventType.CREATE) return;

    try {
      const ifRec = record.load({ type: record.Type.ITEM_FULFILLMENT, id: ctx.newRecord.id });
      const soId = ifRec.getValue('createdfrom');
      if (!soId) return;

      const so = record.load({ type: record.Type.SALES_ORDER, id: soId });
      const notify = !!so.getValue(SO_NOTIFY_FIELD);
      if (!notify) return;

      const customerId = so.getValue('entity');
      const customerEmail = so.getValue('email'); // fallback to entity email; could also search for primary contact

      // Build shipped lines (from the IF just created)
      const shipped = [];
      const ifLineCount = ifRec.getLineCount({ sublistId: 'item' });
      for (let i = 0; i < ifLineCount; i++) {
        const itemId = ifRec.getSublistValue({ sublistId:'item', fieldId:'item', line:i });
        const itemName = ifRec.getSublistText({ sublistId:'item', fieldId:'item', line:i });
        const qtyShip = Number(ifRec.getSublistValue({ sublistId:'item', fieldId:'quantity', line:i }) || 0);
        if (qtyShip > 0) {
          shipped.push({ itemId, itemName, qty: qtyShip });
        }
      }

      // Compute remaining lines from the SO snapshot
      const remaining = [];
      const soLineCount = so.getLineCount({ sublistId: 'item' });
      let anyShipped = shipped.length > 0;
      let anyRemaining = false;

      for (let i = 0; i < soLineCount; i++) {
        const itemName = so.getSublistText({ sublistId:'item', fieldId:'item', line:i });
        const qtyOrdered = Number(so.getSublistValue({ sublistId:'item', fieldId:'quantity', line:i }) || 0);
        const qtyFulfilled = Number(so.getSublistValue({ sublistId:'item', fieldId:'quantityfulfilled', line:i }) || 0);
        const eta = so.getSublistValue({ sublistId:'item', fieldId: SO_LINE_ETA, line:i });

        const qtyRemain = Math.max(qtyOrdered - qtyFulfilled, 0);
        if (qtyRemain > 0) {
          anyRemaining = true;
          remaining.push({
            itemName,
            qtyRemain,
            eta: eta ? tryFormatDate(eta) : ''
          });
        }
      }

      // If not partial (i.e., shipped and nothing remaining), do nothing
      const isPartial = anyShipped && anyRemaining;
      if (!isPartial) return;

      // Tracking (from IF packages) – optional
      const trackingList = collectTracking(ifRec);

      // Compose email HTML
      const soTranId = so.getValue('tranid');
      const subject = `Your order ${soTranId} has partially shipped`;
      const html = buildEmailHtml({
        soTranId,
        shipped,
        remaining,
        trackingList
      });

      // Recipient: customer email or fall back to primary contact
      if (!customerEmail) {
        log.audit('No customer email on SO; skipping send', { soId, customerId });
        return;
      }

      // Send
      const author = runtime.getCurrentUser().id;
      email.send({
        author,
        recipients: customerEmail,
        subject,
        body: html,
        relatedRecords: { transactionId: soId }
      });

      log.audit('Partial fulfillment email sent', { soId, customerEmail, shippedCount: shipped.length, remainingCount: remaining.length });

    } catch (e) {
      log.error('Partial fulfillment UE error', e);
    }
  };

  function collectTracking(ifRec) {
    const tracking = [];
    const pkgCount = ifRec.getLineCount({ sublistId: 'package' });
    for (let i = 0; i < pkgCount; i++) {
      const num = ifRec.getSublistValue({ sublistId:'package', fieldId:'packagetrackingnumber', line:i });
      const carrier = ifRec.getSublistText({ sublistId:'package', fieldId:'packagecarrier', line:i });
      if (num) tracking.push({ carrier, number: num });
    }
    return tracking;
  }

  function tryFormatDate(val) {
    try {
      return format.format({ value: val, type: format.Type.DATE });
    } catch (_e) {
      return String(val);
    }
  }

  function buildEmailHtml({ soTranId, shipped, remaining, trackingList }) {
    const css = `
      <style>
        .tbl{border-collapse:collapse;width:100%;}
        .tbl th,.tbl td{border:1px solid #e5e5e5;padding:6px 8px;font-size:13px;}
        .num{text-align:right;}
        .muted{color:#666;}
        h2{margin:0 0 8px 0;font-size:18px;}
        h3{margin:16px 0 6px 0;font-size:15px;}
      </style>`;
    const shippedRows = shipped.map(s => `<tr><td>${escapeHtml(s.itemName)}</td><td class="num">${s.qty}</td></tr>`).join('');
    const remainRows = remaining.map(r => `<tr><td>${escapeHtml(r.itemName)}</td><td class="num">${r.qtyRemain}</td><td>${r.eta || '<span class="muted">—</span>'}</td></tr>`).join('');
    const trackRows = trackingList.length
      ? trackingList.map(t => `<div>${escapeHtml(t.carrier || 'Carrier')}: <b>${escapeHtml(t.number)}</b></div>`).join('')
      : '<div class="muted">Tracking not available for all packages yet.</div>';

    return `
      ${css}
      <div>
        <h2>Good news — part of your order has shipped!</h2>
        <div>Order: <b>${escapeHtml(soTranId)}</b></div>

        <h3>Shipped Items</h3>
        <table class="tbl">
          <tr><th>Item</th><th class="num">Qty Shipped</th></tr>
          ${shippedRows || '<tr><td colspan="2" class="muted">No items detected.</td></tr>'}
        </table>

        <h3>Items Still on the Way</h3>
        <table class="tbl">
          <tr><th>Item</th><th class="num">Qty Remaining</th><th>ETA</th></tr>
          ${remainRows || '<tr><td colspan="3" class="muted">None.</td></tr>'}
        </table>

        <h3>Tracking</h3>
        ${trackRows}

        <p class="muted">You’ll receive another email once the remaining items ship. If you have questions, just reply to this message.</p>
      </div>
    `;
  }

  function escapeHtml(s) {
    return String(s || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
  }

  return { afterSubmit };
});

🧪 Testing checklist

  1. On a Sales Order, set Notify on Partial = T.
  2. Partially fulfill one or more lines (create IF) → email should send.
  3. Fulfill remaining lines → no email (full fulfillment).
  4. Add tracking on the IF → tracking shows in the email.
  5. Set ETAs on SO lines (optional) → appears in “Items Still on the Way”.

🌱 Optional: Daily “Backorder Digest” (Scheduled/MR)

Send one consolidated email per customer listing all open SO lines with qty remaining > 0 and optional ETA.

  • Input: Saved Search on Sales Order with status = Pending Fulfillment/Partially Fulfilled and formula quantity - quantityfulfilled > 0.
  • Group by Customer and email once per day at 08:00.

🧠 Tips & gotchas

  • Email recipient: If SO.email is blank, consider the primary contact email; or route to an internal team if missing.
  • Edge cases: Drop-ship lines, kit components, or serialized items—validate fields in your account.
  • Branding: Swap the inline HTML for a File Cabinet template if Marketing wants full control.
  • Rate limits: Keep the email simple; avoid heavy rendering if you process hundreds per hour.

📌 Summary

ObjectiveResult
Proactively inform customers on partialsAutomatic, clear emails with shipped vs. remaining
Reduce support ticketsFewer “where’s my order?” questions
Improve CXInclude tracking + optional ETAs
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