SuiteScript: Convert Purchase Requisition to PO
If you’ve ever ventured into the trenches of NetSuite implementations, you know that trying to shepherd a transaction from its genesis to its commitment is where most custom scripts meet their inevita…
Ethan James MarshalSenior SuiteScript Architect & Lead NetSuite Engineer
If you’ve ever ventured into the trenches of NetSuite implementations, you know that trying to shepherd a transaction from its genesis to its commitment is where most custom scripts meet their inevitable collapse. The movement between a Purchase Requisition (PR) and a Purchase Order (PO) is not a simple field transition. They are fundamentally different business entities with distinct lifecycles, approval gates, and financial commitments.
The PR is the internal planning phase, a vetted need to procure goods or services. The PO, conversely, is the external, binding legal document that commits specific funds to a verified vendor upon issuance. Trying to mash these two concepts together without understanding the divide is a recipe for brittle, race-condition prone code.
This article cuts through the noise. We are addressing how to build a stable, production-grade conversion pipeline using SuiteScript 2.1. We aren't patching holes; we are architecting a migration from an internal idea into a financially binding commitment, diagnosing and mitigating the exact runtime errors that sink premature deployments.
The Conceptual Chasm: Blueprint vs. Construction Site
Before touching a single line of code, we must nail down why this conversion is difficult.
A PR lifecycle manages the internal buying motion:
- Need Defined: The request is entered into the system.
- Budget/Inventory Check: It validates against organizational constraints.
- Internal Sign-off: Management approves the need to proceed.
A PO lifecycle is transactional and external:
- Vendor Lock: It targets a specific, paid vendor account.
- Binding Commitment: The moment it is sent or officially posted, the organization has a financial obligation.
If your script assumes they are identical cousins, if you try to simply "upgrade" the PR record into a PO, you are fundamentally misunderstanding NetSuite’s data model. The transition requires the script to execute a rigorous mental (and technical) jump: taking the PR’s approved status and using it as a validated data blueprint to construct an entirely new, unrelated PO record.
Ethan's Warning: Don't try to morph the beast. The PR is the data source; the PO must be instantiated as a new entity.
Diagnosing Conversion Failures: The Cannot read property 'type' of undefined Error
When developers hit that dreaded runtime error, "Cannot read property 'type' of undefined," during a purported record.transform call between these two types, the error message is almost meaningless noise. It only tells you that your script tried to access a property on an object variable that was never properly initialized, it was undefined.
When this manifests in a PR-to-PO conversion, the failure point is almost always found during the most delicate phase: array iteration and object assembly. The change function probably ran, but one of the expected objects, likely an array holding line items or custom metadata specific to that record, was accessed before it was populated.
Why Defensive Coding Is Non-Negotiable
Many amateur attempts attempt to simply pipe the PR data into the PO schema. This is dangerous and brittle code that will bite you during upgrades.
The proper transactional flow demands a multi-stage process, not a single drag-and-drop action:
- PR Status Check: The source PR must be in a definitive, final
APPROVEDstatus. If it’s still pending internal review when your script runs, you are only compounding the approval latency and causing headaches down the road. - PO Instantiation: You are not modifying the PR; you are creating a brand-new PO record object. This separation is important for maintaining auditing trails and lifecycle history.
- Data Transposition: You map the item codes, quantities, and rates from the PR's transactional line items into the PO's corresponding line item schema.
- Lifecycle Flagging: You set all the flags that indicate a financial commitment (like vendor visibility, tax codes, etc.), successfully completing the transition from "request" to "contract."
The key takeaway for those hitting undefined is simple: If you are trying to read a property on a nested array object, you must first ensure that the parent collection exists and is non-empty.
The Blueprint: A validated Conversion Pattern for Production
To successfully ship this conversion feature, you must move beyond the idea of "updating" and adopt the mindset of a migration specialist.
1. Trigger and Read Phase (The Data Acquisition)
The process must be initiated by a definitive action on the PR, a dedicated button click, perhaps tied to the approval gateway. The script runs as a read-only extractor initially.
/**
* @NApiVersion 2.1
* @NextAction Before Record Submit // Running the conversion function
*/
// Step 1: Retrieve the source PR object. This must be stable.
const prRecord = org.getCurrentRecord();
// Step 2: Defensive check on the critical data collection.
// This prevents your script from crashing if a user forgot to add items.
if (prRecord.getCurrentSublistValues('items', 'sorter_field').length === 0) {
throw new Error("cannot convert: Source PR has no line items to transition.");
}
// Now, we have a known good data source. We can proceed to the next phase.
2. PO Object Assembly (The Write Phase)
A fresh PO object is instantiated. You are now building the destination record.
// Step 3: Instantiate the new PO object in memory.
const poObject = record.create({
type: 'purchaseorder',
isDynamic: false // Match how you want to submit it
});
// Step 4: Map the header details (Vendor, PO Date, etc.). These are simple key-value pairs.
poObject.setValue({ field: 'vendor', value: prRecord.getValue('vendor') });
poObject.setValue({ field: 'tran_date', value: prRecord.getValue('ship_date') });
// Step 5: Line Item Translation - The critical mapping.
const prLineItems = []; // Assume we retrieved the line items into this array during the Read Phase
prLineItems.forEach(function(lineItemData) {
// We map the data from the PR's internal structure into the PO's expected schema.
const poLine = {};
poLine.item_id = lineItemData.itemId;
poLine.quantity = lineItemData.quantity;
poLine.rate = lineItemData.unit_price; // Crucial: use the final price, not just unit cost.
// Use NetSuite's API methods for transactional integrity:
poObject.selectNewLine({ module: 'item' });
poObject.setCurrentSublistValue({ field: 'item', values: poLine.item_id });
poObject.setCurrentSublistValue({ field: 'quantity', values: poLine.quantity });
poObject.setCurrentSublistValue({ field: 'rate', values: poLine.rate });
poObject.commitLine({ module: 'item' });
});
// This is the clean, copy-pasteable solution for transactional mapping.
3. Final Submission (The Commit)
Only once the PO object has successfully received all its line items and mapped header data is it ready for final validation and submission. This is where the record.transform function, if used, acts as a final business logic gate before you hit the commit button.
// Step 6: Final validation and save/submit.
poObject.compress(); // Perform final data integrity checks based on the PO template.
poObject.save({ });
// Success! The financial commitment has been shipped.
To move past scripts that crash during initial testing, adopt the mindset of a system architect. The PR-to-PO transition is a financial transaction migration across two NetSuite environments. It requires:
- Clean Separation: Treat the PR as a validated input source, not a mutable object.
- Defensive Coding: Never trust an initialized variable or collection boundary. Check size before iteration to prevent
undefinederrors. - Canonical Integrity: Use NetSuite’s
record.Typeenums and transactional methods (selectNewLine,commitLine) for maximum stability.
Apply these patterns and you replace guessing with a validated codebase that survives mandatory upgrades.


