Monday, 26 May 2025

Custom List Entries Made Effortless – Just Script It with SuiteScript!

Business Scenario:

 In certain situations, business users may need the ability to manage a specific custom list field.


Example:

There is a custom list named "Color Names" with the internal ID "customlist_color". It currently contains the following values: Red, Green, Yellow, and Orange. The user would like to add a new color to this list.

Limitations:

 Custom lists in NetSuite do not support direct permission controls. Unlike standard or custom records, you cannot assign custom roles to modify customlist_* values.

Current Process:

 Currently, users must contact NetSuite admins to add new custom list values, which can cause delays due to approval steps and SOX compliance.

Alternative Solution:

We can implement this functionality through a SuiteLet script.

You can create a simple SuiteLet script that includes a single text field where users can enter a new value. Upon submitting the form, the script will automatically add the entered value to the specified custom list. 

Sample SuiteLet Code:


/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/ui/serverWidget','N/record','N/log','N/search'],
function(ui, record, log, search) {

// Replace with your custom list ID
const CUSTOM_LIST_ID = 'customlist_color_names';

function onRequest(context) {
if (context.request.method === 'GET') {
const form = homePage();
context.response.writePage(form);
} else {
const value = context.request.parameters.custpage_new_value_to_list;

try {
if (checkDuplicateValue(value)) {
buildErrorPage(context, value);
return;
}

const newId = addNewValueToCustomList(value);
buildSuccessPage(context, value, newId, context.request.url);

} catch (e) {
log.error('Error adding value to custom list', e);
context.response.write(`Error: ${e.message}`);
}
}
}

function homePage() {
const form = ui.createForm({ title:'Add New Value to Custom List'});
form.addField({
id: 'custpage_new_value_to_list',
type: ui.FieldType.TEXT,
label: 'Enter New Value for Custom List'
}).isMandatory = true;

form.addSubmitButton({ label: 'Add to Custom List' });
return form;
}

function checkDuplicateValue(value) {
const resultSet = search.create({
type: CUSTOM_LIST_ID,
filters: [['name', 'is', value]],
columns: ['internalid']
}).run().getRange({ start: 0, end: 1 });

return resultSet.length > 0;
}

function addNewValueToCustomList(value) {
const rec = record.create({ type: CUSTOM_LIST_ID });
rec.setValue({ fieldId: 'name', value: value });
return rec.save();
}

function buildErrorPage(context, value) {
const form = ui.createForm({title:'Duplicate Entry Detected'});
form.addField({
id: 'custpage_error',
type: ui.FieldType.INLINEHTML,
label: ' '
}).defaultValue = `
<div style="
padding: 10px 0;
color: #cc0000;
font-family: Arial, sans-serif;
font-size: 17px;
padding-left: 10px;
margin-top: 20px;">
<strong>Error:</strong>
The value "<strong>${value}</strong>"
already exists in the custom list.
<br><br>
<strong style="color:green">Resolution:</strong>
<span style="color:green">
Please enter a unique value.
</span>
</div>`;
context.response.writePage(form);
}

function buildSuccessPage(context, value, id, requestUrl){
const form = ui.createForm({ title: 'Success' });

form.addPageLink({
type: ui.FormPageLinkType.CROSSLINK,
title: 'Back to Form',
url: requestUrl
});

form.addField({
id: 'custpage_success',
type: ui.FieldType.INLINEHTML,
label: ' '
}).defaultValue = `
<div style="
color: green;
font-family: Arial, sans-serif;
font-size: 17px;
margin-top: 20px;">
<strong>Successfully</strong> added
"<strong style="color:#4d5f79">${value}
</strong>" to the custom list.
<br><br><strong>Internal ID:</strong>
<strong style="color:#4d5f79">${id}</strong>
</div>`;

context.response.writePage(form);
}

return {
onRequest
};
});


SuiteLet UI Sample Screen Shots:


1. Home Page:




2. Success Page:




3. Error Page


4. Ensure the new list value has been successfully added.



Other Possible Scenarios:

Similar to adding new values, existing entries in a custom list can also be deleted as needed.

Final Thoughts:

Granting unrestricted access to manage custom list values poses risks, such as accidental deletions or incorrect entries.

To mitigate this, access should be restricted to authorized users by validating roles or internal IDs (e.g., 'If user role is X' or 'If internal ID is Y'). This ensures data accuracy and system integrity.


Sample SuiteLet OutPut Video:





Monday, 17 February 2025

Various methods for automatically closing NetSuite PO items and expense lines

When should Purchase Order Expense or Item Lines be closed by business users?
A purchase order line that remains open in NetSuite indicates that a financial commitment exists. Businesses close PO records for the following reasons:

Closing PO Item Lines :

Cancel PO records that are not necessary: The business user can eliminate the commitment by closing the PO line if a vendor / supplier is unable to complete an order.

✅ Preventing month-end cleanup problems: Reconciliation issues during month-end or year-end financial close may arise from maintaining needless open PO lines.

✅ Enhance NetSuite system performance: Processing and reporting times may be slowed down by a large number of open POs with unused lines.


Closing PO Expense Lines :

Stopping Unauthorized or Duplicate Expenses: Closing an expense guarantees that it won't be accepted should it be entered improperly or lose validity, so preventing unauthorized or duplicate expenses.

✅ Prepaid Expense Reconciliation: Closing an expense line that has been pre-paid or refunded guarantees that no further action is taken on it.

✅ Maintaining Financial Discipline: Companies with stringent budgets eliminate unnecessary expenses.

Different ways of Automating PO lines Closing:

Based on the business need, we can close the entire PO item or expense lines or specific lines using NetSuite Saved Search or CSV File.





Closing all the PO Expense lines: Pseudo Code:
// Load the Purchase Order Record

let poRecObj = record.load({
type: record.Type.PURCHASE_ORDER,
id: poId,
isDynamic: true
});
// Closing Expense Line
let lineCount = poRecObj.getLineCount({ sublistId: 'expense' });
let closedLines = [];

// Iterate through PO Expense lines
for (let i = 0; i < lineCount; i++) {

let isPOLineClosed = poRecObj.getSublistValue({
sublistId: 'expense',
fieldId: 'isclosed',
line: i
})

// Check if the PO Line Number exists in the saved search results
if (!isPOLineClosed && lineNumbers.includes(actualLineNumber.toString())) {
log.
debug(`Closing PO Line`, `PO: ${poId}, Line: ${actualLineNumber}`);

//Select line and set it as closed
poRecObj.selectLine({ sublistId: 'expense', line: i });
poRecObj.setCurrentSublistValue({ sublistId: 'expense', fieldId: 'isclosed', value: true });
             poRecObj.commitLine({ sublistId: 'expense' });

closedLines.push(actualLineNumber);
}
}

//Save the updated PO Record
poRecObj.save();
log.debug(`PO ${poId} updated successfully`, `Closed Lines: ${closedLines}`);
Closing the specific Item lines:

Pseudo Code:
// Load the Purchase Order Record

let poRecObj = record.load({
type: record.Type.PURCHASE_ORDER,
id: poId,
isDynamic: true
});
// Closing Item Line
let lineCount = poRecObj.getLineCount({ sublistId: 'item' });
let closedLines = [];

// Iterate through PO lines
for (let i = 0; i < lineCount; i++) {
let actualLineNumber = poRecObj.getSublistValue({
sublistId: 'item',
fieldId: 'linesequencenumber',
line: i
});

let isPOLineClosed = poRecObj.getSublistValue({
sublistId: 'item',
fieldId: 'isclosed',
line: i
})

// Check if the PO Line Number exists in the saved search results
if (!isPOLineClosed && lineNumbers.includes(actualLineNumber.toString())) {
log.
debug(`Closing PO Line`, `PO: ${poId}, Line: ${actualLineNumber}`);

//Select line and set it as closed
poRecObj.selectLine({ sublistId: 'item', line: i });
poRecObj.setCurrentSublistValue({ sublistId: 'item', fieldId: 'isclosed', value: true
               });

poRecObj.commitLine({ sublistId: 'item' });

closedLines.push(actualLineNumber);
}
}

// Save the updated PO Record
poRecObj.save();
 log.debug(`PO ${poId} updated successfully`, `Closed Lines: ${closedLines}`); 

To close certain PO lines, we must use the internal standard field "Line Sequence Number" rather than the "Line Number".

The line number may not be the same as the PO sequence number.







Which type of Suite Script to choose?

  1.   For Bulk closing of PO's we can choose Scheduled Script / Map/Reduce Script.

    1. Schedule (or) Map/Reduce Script Logic:

      1. Get Input Stage : 

        1. Reads CSV File / Saved Search results and groups PO Internal Ids with their respective Line Numbers.

        2. Returns key-value pairs (PO Internal Id → Array of Line Numbers).

      2. Map Stage:

        1. Receives one PO Internal Id at a time.

        2. Emits all related Line Numbers for that PO.

      3. Reduce Stage:

        1. Loads the Purchase Order using record.load().

        2. Iterates through PO lines, checking if the PO Line Number exists in the CSV.

        3. Closes matching PO Lines.

        4. Saves the updated PO record.

      4. Summary Stage: 

        1. Logs errors and send PO closing completion status to the submitted user.


  2. For Better User Access: We can create the SuiteLet Script to provide the closing PO details.