๐ผ Business problem
Many distribution or drop-ship companies handle hundreds of customer orders per day. Each time an SO is approved, the purchasing team must manually create matching POs for vendors.
This is repetitive, error-prone, and slows fulfillment.
Goal:
Automatically create one or more POs from each qualifying SO, grouped by vendor and subsidiary.
๐ง Approach overview
When to run:
- Nightly or hourly Map/Reduce script
- Optional User Event (beforeSubmit or afterSubmit) for real-time generation
How it works:
- Fetch all Sales Orders where:
- Status = Pending Fulfillment or Approved
- Custom checkbox โPO Createdโ = false
- For each order:
- Loop through item lines
- Group by vendor
- Build one PO per vendor
- Link PO to SO via
createdfrom
or a custom field - Mark โPO Createdโ = true to prevent duplicates
๐งพ Prerequisites
Component | Details |
---|---|
Custom Field | custbody_po_created (Checkbox, default = F) |
Saved Search | customsearch_so_pending_po Filters: Type = Sales Order Status = Pending Fulfillment custbody_po_created = F |
Permissions | Script Deployment role needs Create/Edit for Purchase Order and Sales Order |
๐งฉ Map/Reduce Script (SuiteScript 2.1)
/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
* Title: MR | Auto-Create POs from Sales Orders
* Author: The NetSuite Pro
*/
define(['N/search','N/record','N/log'], (search, record, log) => {
const SEARCH_ID = 'customsearch_so_pending_po';
const FIELD_PO_CREATED = 'custbody_po_created';
const getInputData = () => search.load({ id: SEARCH_ID });
const map = ctx => {
const soId = JSON.parse(ctx.value).id;
ctx.write({ key: soId, value: soId });
};
const reduce = ctx => {
const soId = ctx.key;
try {
const so = record.load({ type: record.Type.SALES_ORDER, id: soId });
const subsidiary = so.getValue('subsidiary');
const entity = so.getValue('entity');
// Collect lines by vendor
const vendorMap = {};
const lineCount = so.getLineCount('item');
for (let i = 0; i < lineCount; i++) {
const itemId = so.getSublistValue({ sublistId:'item', fieldId:'item', line:i });
const qty = so.getSublistValue({ sublistId:'item', fieldId:'quantity', line:i });
const rate = so.getSublistValue({ sublistId:'item', fieldId:'rate', line:i });
const vendor = so.getSublistValue({ sublistId:'item', fieldId:'povendor', line:i }); // built-in vendor mapping field
if (!vendor) continue;
if (!vendorMap[vendor]) vendorMap[vendor] = [];
vendorMap[vendor].push({ itemId, qty, rate });
}
Object.keys(vendorMap).forEach(vendorId => {
const po = record.create({
type: record.Type.PURCHASE_ORDER,
isDynamic: true
});
po.setValue('entity', vendorId);
po.setValue('subsidiary', subsidiary);
po.setValue('custbody_linked_salesorder', soId); // optional custom field
vendorMap[vendorId].forEach(line => {
po.selectNewLine({ sublistId: 'item' });
po.setCurrentSublistValue({ sublistId:'item', fieldId:'item', value:line.itemId });
po.setCurrentSublistValue({ sublistId:'item', fieldId:'quantity', value:line.qty });
po.setCurrentSublistValue({ sublistId:'item', fieldId:'rate', value:line.rate });
po.commitLine({ sublistId:'item' });
});
const poId = po.save({ enableSourcing:true, ignoreMandatoryFields:false });
log.audit('PO Created', { vendorId, poId, soId });
});
// Flag SO as processed
record.submitFields({
type: record.Type.SALES_ORDER,
id: soId,
values: { [FIELD_PO_CREATED]: true }
});
} catch (e) {
log.error(`SO ${soId} failed`, e);
}
};
const summarize = summary => {
log.audit('Auto-PO Summary', {
usage: summary.usage,
yields: summary.yields,
concurrency: summary.concurrency
});
};
return { getInputData, map, reduce, summarize };
});
๐งฎ Deployment Parameters
Parameter | Description | Example |
---|---|---|
Saved Search ID | Which SO search to use | customsearch_so_pending_po |
Subsidiary Filter | Optional if multi-sub account | 2 |
Log Only Mode | Dry run without creating PO | F |
โ๏ธ Scheduling
- Frequency: every hour or nightly at midnight
- Governance: ~15 usage per PO creation; safe for hundreds per run
- Best practice: run on off-peak hours to avoid contention with manual purchasing
โ Testing Checklist
- Create a Sales Order with items having vendor values.
- Run the script manually via Map/Reduce deployment โ Submit Script.
- Confirm POs appear under each vendor; check โCreated Fromโ link.
- Ensure
custbody_po_created
= T after run. - Try again โ script should skip the same SO (safety check).
๐งฉ Optional Enhancements
Enhancement | Description |
---|---|
Email Notification | Email buyer or vendor after PO creation. |
Drop-Ship Logic | Only generate PO if item is marked as Drop Ship. |
Vendor Approval | Auto-approve POs under $500 limit. |
Custom Field Mapping | Copy fields from SO to PO (e.g., project, terms, ship date). |
Combine Multiple SOs | Group items by vendor across different SOs if same subsidiary. |
๐ง Lessons Learned
- Always flag processed SOs to avoid duplicates.
- Vendor field (
povendor
) must exist on the item record. - For large volumes, consider batching by subsidiary or location.
- Add robust logging and notifications for easy monitoring.
๐ Summary
Goal | Outcome |
---|---|
Automate manual PO creation | โ๏ธ Reduced time per order by > 90% |
Eliminate errors & duplicates | โ๏ธ Safe with custom flag |
Improve vendor turnaround | โ๏ธ PO instantly generated on SO approval |
Leave a Reply