Source State record to sublist field - netsuite

I am trying to create a custom sublist with sublist field with source to be States record that is managed in Setup > Company > States/Provinces/Countries section.
Here is the example code that I am using and it doesn't work.
_sublist.addField({
id: 'custpage_license_state,
type: serverWidgetModule.FieldType.SELECT,
label: 'LICENSE STATE',
source: 'state' //not recognizing record id
});
I have tried using 'state', 'states', '-195', -195 (was able to locate that this is the internal id for states record in our instance "-195"), but nothing works.
Does anybody has an idea on how to make that work.
Thanks.

The State/Province record isn't exposed. You'll need to add the options to the field manually. You could either perform a search against the customer records which will only return states currently assigned;
/**
* Gets customers geographical states.
*
* #returns {Array} of state information.
*/
function getStates() {
var records = [];
var customerSearchObj = search.create({
type: "customer",
filters: [
["formulatext: {country}", "isnotempty", ""],
"AND",
["formulatext: {state}", "isnotempty", ""]
],
columns: [
search.createColumn({
name: "statedisplayname",
summary: "GROUP",
sort: search.Sort.ASC
}),
search.createColumn({ // abbreviation
name: "state",
summary: "GROUP"
})
]
});
customerSearchObj.run().each(function (result) {
var rec = {
state: result.getValue({name: 'state', summary: 'GROUP'}),
stateDisplay: result.getValue({name: 'statedisplayname', summary: 'GROUP'})
};
records.push(rec);
return true;
});
return records;
}
Or, create a customer in memory and then get the states; (Sorry, SS1 code, taken from SA 63293.)
function getAllStatesForCountry() {
var customer_record = nlapiCreateRecord('customer', {recordmode: 'dynamic'});
customer_record.selectLineItem('addressbook', 1);
var addrSubrecord = customer_record.createCurrentLineItemSubrecord('addressbook', 'addressbookaddress');
addrSubrecord.setFieldValue('country', 'GB');
var stateField = addrSubrecord.getField('dropdownstate');
return stateField.getSelectOptions();
}
And then loop through the result and add them to your field using mySelect.addSelectOption().

Related

SuiteScript 2.1 record updates too slow

I have afterSubmit function that I wrote that will iterate through all related transactions connected with the same CTG_ID value (which is a custom parent record) and the script will actually update just one field on all of these values.
My problem is because this is a really slow method, more transactions I have connected to the same parent more time the user needs to wait after clicking the "Save" button. Script execution time is terrible.
Is there any faster / better way to update a certain field on a group of records?
My function for updating those transactions:
function afterSubmit(context) {
const newRecord = context.newRecord;
const ctgId = newRecord.getValue({ fieldId: 'custbody_ctgid' });
const currentCustomerPo = newRecord.getValue({ fieldId: 'custbodyctg_customer_po'})
search.create({
type: 'transaction',
filters: [['custbody_ctgid', 'anyof', ctgId],
"AND",
['mainline','is','T']],
columns: ['custbodyctg_customer_po']
}).run().getRange({start: 0, end:100}).forEach((result,line) => {
const ctgPo = result.getValue('custbodyctg_customer_po') as string;
const recType = result.recordType;
const recId = result.id;
let rec = record.load({
type: recType,
id: recId,
isDynamic: true
})
rec.setValue({
fieldId: 'custbodyctg_customer_po',
value: currentCustomerPo,
ignoreFieldChange: true
})
rec.save();
})
}
Thanks to Brian Duffy's answer, this is working a lot better!
I modified the script so now I iterate through results with each instead of forEach function. I'm using record.submitFields function instead of record.load
function afterSubmit(context) {
const newRecord = context.newRecord;
const oldRecord = context.oldRecord;
const ctgId = newRecord.getValue({fieldId: 'custbody_ctgid'});
const currentCustomerPo = newRecord.getValue({fieldId: 'custbodyctg_customer_po'})
const oldCustomerPo = oldRecord.getValue({fieldId: 'custbodyctg_customer_po'})
if (oldCustomerPo !== currentCustomerPo) {
search.create({
type: 'transaction',
filters: [['custbody_ctgid', 'anyof', ctgId],
"AND",
['mainline', 'is', 'T']],
columns: ['custbodyctg_customer_po', 'type']
}).run().each((result) => {
if (result.recordType !== 'salesorder') {
const recType = result.recordType;
const recId = result.id;
record.submitFields({
type: recType,
id: recId,
values: {
custbodyctg_customer_po: currentCustomerPo
},
options: {
enableSourcing: false,
ignoreMandatoryFields: true
}
});
}
return true;
})
}
}
Still, after testing few transactions with an average of 5 linked transactions to them this is running like 5-7 seconds. Stil slow for me. If anyone has suggestions it would be AWESOME!
I would try using run.each instead of getRange for your search results and use record.sumbitFields instead of loading and saving the record.
If speed of saving the current record is key, consider making your logic asynchronous. The current record will save nearly as fast as normal, just taking the time to schedule a Map/Reduce or Scheduled script.
The user event script would just send as parameters the current record's Id, the value in CTG_ID and the value in custbodyctg_customer_po. The asynchronous script would search for all tranx with that same Id except for the one that triggered it (or skip any records with a matching value while looping thru results), then submit the custbodyctg_customer_po for each of those.
It's a heavier solution, so you must weigh priorities.
Alternatively, look into record.submitFields.promise(options). It seems a limitation is it cannot update Select fields (ie List/Record)

SuiteScript 2.0: How can I update the externalID through Script

I'm trying to create a script file on NetSuite using SuiteScript 2.0 to update the External Id of the classification records. For some reason, NetSuite updates all fields but externalId.
I want to create this script to update the externalIds because mostly of our CSV templates uses the externalIds to look for master data such as accounts and classes. The point is, NetSuite doesn't show the externalIds on the forms and all the records created via UI, has no information in this field. So the idea here is to schedule a script to fill this field automatically based on some other fields.
Here is my code
/**
* #NApiVersion 2.0
* #NScriptType ScheduledScript
*/
define(['N/runtime','N/log','N/record','N/search'], function(runtime,log,record,search) {
var qtyProjects=0;
function execute(context) {
try
{
log.debug('Script Started');
/********** Update Project (Classes) External ID ***********/
var classificationSearchObj = search.create({
type: "classification",
filters:
[
["externalid","is","#NONE#"]
],
columns:
[
search.createColumn({name: "name",label: "Name"}),
search.createColumn({name: "custrecord_proj_full_name",label: "FullName"}),
search.createColumn({name: "custrecord_proj_manager",label: "ProjectManager"}),
search.createColumn({name: "externalid",label: "Externalid"})
]
});
var prj_srch = classificationSearchObj.run().each(processProject);
log.debug('Quantity of projects: ',qtyProjects);
log.debug('Script Finished');
}
catch(e)
{
log.debug('Error',e.message);
}
}
function processProject(result) {
var number = result.getValue({name: "name"});
var fullName = result.getValue({name: "custrecord_proj_full_name"});
var externalid = result.getValue({name: "externalid"});
qtyProjects++;
log.debug('Update Number|Name|ExternalId: ',number + " | " + fullName + " | " + externalid);
record.submitFields({
"type":'classification',
"id": result.id,
"values": {
"externalId": externalid,
"custrecord_proj_full_name": "Test Ale 2",
}
});
/*
var project_id = project.save({
enableSourcing: true,
ignoreMandatoryFields: true
});
*/
//return true;
}
return {
execute: execute
};
});
You mispelled the id of ExtermalId, it is without Capital 'I' as externalid. This should work.
As a rule of thumb native field id's don't have capital letters in it
/**
* #NApiVersion 2.0
* #NScriptType ScheduledScript
*/
define(['N/runtime','N/log','N/record','N/search'], function(runtime,log,record,search) {
function execute(context) {
var so = record.load({
type: record.Type.SALES_ORDER,
id: 12345
});
so.setValue('external','6789');
so.save();
return true;
}
return {
execute: execute
}
});
Have you tried loadig the record, setting the external id then saving it?
Yes i tried loading the record and set external id , it worked for me . Please use record browser for internal id's of fields (externalid).
Your code is searching for classes with no externalid and is updating the class's externalid with the empty value found in the search. It appears you are missing a lookup to get what the new externalid should be.
I've found that SuiteScript 2 syntax does not work for updating externalId. For me, this simply doesn't do anything:
instance.setValue({ fieldId: 'externalid', value: 'FOO1' })
While using the older syntax actually will update externalId:
instance.setValue('externalid', 'FOO1')
You can run this in your browser's console while viewing a record to try it out:
require(['N/record'], function (record) {
const instance = record.load({ type: 'customerpayment', id: 5000 })
instance.setValue('externalid', 'PYMT1234')
instance.save()
})

Source object access from ColumnSet

I try to split a sub-object in my recordset when importing data with initCB properties of a Column in ColumnSet.
But when I use two different init functions for two different destination names but one source I get same result.
const cs = new pgp.helpers.ColumnSet([
'id',
{ name: 'source_id', prop: 'source', init: function(obj) { return obj.value.id; } },
{ name: 'source_name', prop: 'source', init: function(obj) { return obj.value.name; } },
], { table: 'test_table' });
const data = [
{ id: 1, source: { id: 1, name: 'source1' } },
{ id: 2, source: { id: 1, name: 'source1' } },
{ id: 3, source: { id: 2, name: 'source2' } },
];
const insert = pgp.helpers.insert(data, cs);
The result is:
INSERT INTO "test_table"("id","source_id","source_name") VALUES
(1,'source1','source1'),
(2,'source1','source1'),
(3,'source2','source2')
instead of expected:
INSERT INTO "test_table"("id","source_id","source_name") VALUES
(1,1,'source1'),
(2,1,'source1'),
(3,2,'source2')
It seems like second invocation of callback function for THE SAME source field overriding result of previous invocation of ANOTHER callback function on THIS source field.
How I can avoid this?
Or there is another way of splitting a sub-object during import?
Option prop doesn't quite work that way. It is there to remap the value to a different property name, but it does not supply the direct object reference.
Instead, use property source of the column descriptor, to reference the source object. Ironically, you called the property in your data source as well, which means you will have to use source twice in your reference:
const cs = new pgp.helpers.ColumnSet([
'id',
{name: 'source_id', init: c => c.source.source.id},
{name: 'source_name', init: c => c.source.source.name}
], {table: 'test_table'});
The first source is what pg-promise API supports, while the second is your data column name :)
Also, as per documentation, the API sets source and this to the same, so if you prefer the ES5 function syntax (looks cleaner for your example), then you can do this instead:
const cs = new pgp.helpers.ColumnSet([
'id',
{ name: 'source_id', init: function() {return this.source.id;}},
{ name: 'source_name', init: function() {return this.source.name;}},
], { table: 'test_table' });
Above we have this point at the source data object.

Searching an array of items in NetSuite using SuiteScript 2

I am attempting to write a suitescript search that allows me to pass an array of identifiers to a particular field in Netsuite and return the results. I have tried 'ANYOF', 'ALLOF' and "WITHIN' but I keep getting errors
Here is my code so far:
if(params.type=='sku'){
var filter_name = 'itemid';
}else{
var filter_name = 'upccode';
}
var filters = [
search.createFilter({
name: filter_name,
operator: search.Operator.ANYOF,
values: ['HERHR5201','HERHR5202','HERHR5203']
}),
];
var s = search.create({
'type': record.Type.INVENTORY_ITEM,
'filters':filters,
}).run();
s = s.getRange(0,100);
return JSON.stringify(s);
Does anyone know the right sequence to create a multiple search of itemid's? Also, for a bonus, is there a way to have the resultset return the columns I need verses just the ideas? Do I need to createColumn?
You cannot use ANYOF, ALLOF, etc. when filtering a search on a text field. You'll need to create a filter expression with ORs to search on multiple values.
I would do this:
if(params.type=='sku'){
var filter_name = 'itemid';
}else{
var filter_name = 'upccode';
}
var filters = [
[filter_name, 'is', 'HERHR5201'], 'OR',
[filter_name, 'is', 'HERHR5202'], 'OR',
[filter_name, 'is', 'HERHR5203']
];
var s = search.create({
'type': record.Type.INVENTORY_ITEM,
'filters':filters
}).run();
As far as returning specific columns from your search, you'll need to use search.createColumn(), as you point out. So it'd be something like:
//Previous code...
var s = search.create({
'type': record.Type.INVENTORY_ITEM,
'filters':filters,
'columns': [search.createColumn({name: 'internalid'}),
search.createColumn({name: 'upccode'}),
search.createColumn({name: 'itemid'})
/*Other columns as needed*/]
}).run();
The provided answer is correct, however based on your example code provided I am assuming that the search needs to be created somewhat dynamically. Meaning the 'array of identifiers' you mention will not always be the same, nor will they always be the same length. In order to create a search that is completely dynamic based on incoming 'array of identifiers' you would need to get pretty creative. In the below solution I am assuming the function parameter 'params' is an object with a 'type' property, and an arrIn (array of strings) property. The search below uses the formula function 'DECODE', a description of which can be found here.
function execute(params) {
var filter_name;
var itemSearchObj;
var stringArr = '';
var arrIn = params.arrIn;
var i;
var count;
// create search filter type
filter_name = params.type === 'sku' ? 'itemid' : 'upccode';
// create stringArr using incoming arrIn
for (i = 0; arrIn && arrIn.length > i; i += 1) {
stringArr += i > 0 ? ", '" + arrIn[i] + "', 'true'" : "'" + arrIn[i] + "', 'true'";
}
if (arrIn.length > 0) {
itemSearchObj = nsSearch.create({
type: 'item',
filters: [
["formulatext: DECODE({" + filter_name + "}," + stringArr + ")", "is", 'true']
],
columns: [
'itemid', // dont need to get fancy here, just put the internal id of whichever fields you want in the columns
'description'
]
});
count = itemSearchObj.runPaged().count;
itemSearchObj.run().each(function (result) {
// Do things for each result
});
}
}
I found this quite a bit of a curveball as I was overthinking it. The search requires an array:
results = search.create({
type: search.Type.CUSTOMER,
filters: [],
columns: [ search.createColumn({ name: "internalid", sort: search.Sort.ASC }) ]
})
This filter array consists of a search term array and logical operators (AND, OR).
['fieldid', 'is', yourValue], // search term array
'AND' // logical operator
So you can create your own filter array, by pushing in the search terms and logical operators as required.
For example if you want to return the internal id for customer emails in an array:
let email = ['abc#test.com', 'def#test.com'];
let myFilter = createFilter(email);
function createFilter(email){
let filter = [];
email.forEach((result, index) => {
if(index > 0)
filter.push('OR'); // Logical Operator
filter.push(['email', 'is', result]);
});
return filter;
}
Variable myFilter will now equal:
[ ['email', 'is', 'abc#test.com'], 'OR', ['email', 'is', 'def#test.com'] ]
And can be directly referenced as the filter in the search.
let results = search.create({
type: search.Type.CUSTOMER,
filters: myFilter,
columns: [ search.createColumn({ name: "internalid", sort: search.Sort.ASC }) ]
})

Query attachment from a Message record

Through the UI, I have created several Message records attached to a Support Ticket record, two of which have file attachments. I have been able to retrieve the ticket, and its related messages in Suitescript - which are correctly reporting hasAttachment as 'T' - but I cannot seem to access the attachments themselves. The documentation states that the attachments are a sublist called 'mediaitem' (or 'mediaitemlist', depending on where you look), but none of the sublist APIs have any success on those names.
var record = nlapiLoadRecord('message', 1092823, {recordmode: 'dynamic'});
var itemCount = record.getLineItemCount('mediaitem');
// returns -1
The documentation and other online info is pretty sparse, so any help would be greatly appreciated.
Yeah, indeed there is a poor documentation. And mediaitem sublist did not help me either to give any meaningful result.
However, there is an alternate solution to it.
Create a saved search from UI on message record type.
Make sure you add a search column Attachments : Internal ID (i.e.
using attachment fields...)
Once, this is done, run your search in suitescript as
var res = nlapiSearchRecord('message', 'YOUR_UI_SEARCH_ID', ARRAY_OF_ADDITIONAL_FITLTERS);
res[i].getValue('internalid', 'attachments')
This is how you can do it in Suitescript 2.0. First search the message ids then search for the attachments related to those message ids. You can create the searches on the fly so no need for saved searches.
You can pass an array of internal ids of cases or messages if you want to save governance points depending on your scenario.
Note: The following code samples assumes that you loaded the search module as SEARCHMODULE.
Step 1 - This is how to get the message ids with attachments from a support case record (just change the type to support ticket):
function getMessageIdsFromCase(supportCaseId){
var supportcaseSearchObj = SEARCHMODULE.create({
type: "supportcase", //Change if you need to
filters: [
["internalid","anyof",supportCaseId],
"AND",
["messages.hasattachment","is","T"]
],
columns: [
SEARCHMODULE.createColumn({
name: "internalid",
join: "messages"
})
]
});
var resultsSet = supportcaseSearchObj.run();
var results = resultsSet.getRange(0, 999);
var messages = [];
for (var i in results) {
var result = results[i];
var message = result.getValue(result.columns[0]);
messages.push(message);
}
return messages;
}
Then you just call the function like this:
getMessageIdsFromCase(caseInternalId); //Returns an array of message ids
Step 2 - Then you search the attachments using the message internal id with this function:
function getAttachmentIdsFromMessage(messageInternalId){
var messageSearchObj = SEARCHMODULE.create({
type: "message",
filters: [
["internalid","anyof",messageInternalId]
],
columns: [
SEARCHMODULE.createColumn({
name: "internalid",
join: "attachments"
})
]
});
var resultsSet = messageSearchObj.run();
var results = resultsSet.getRange(0, 999);
var attachments = [];
for (var i in results) {
var result = results[i];
var attachment = result.getValue(result.columns[0]);
attachments.push(attachment);
}
return attachments;
}
Then you just call the function like this:
getAttachmentIdsFromMessage(messageInternalId); //Returns an array of attachment ids
UPDATE:
heard from NS after submitting a case. Appears this is not supported yet:
Hi Shane,
I hope you are doing well today.
Upon checking, the ability to access files attached to records are not yet supported in SuiteScript. You can check out SuiteScript Records Browser at SuiteAnswers ID 10511 for the full list of all available records in SuiteScripts and each's accessible sublist. Let me know if you have further questions.
Caleb Francisco | Customer Support
NetSuite: Where Business is Going
Using search.createColumn with joins is the key it looks like. I ended up using quick code below this to get any $files (html) attached to $transaction (returnauthorization), which were in my case, supposed to be mediaitems on a return auth that I couldn't get via the record module in ss2.0
var getHtmlFilesOnReturnAuth = function (return_auth_id, file_type) {
var filters = [
search.createFilter({name: "internalid", operator: "is", values: [return_auth_id]}),
search.createFilter({name: "filetype", join: "file", operator: "is", values: [file_type]}),
];
var images = [];
search.create({
type: "returnauthorization",
filters: filters,
columns: [
search.createColumn({name: "internalid", summary: "group"}),
search.createColumn({name: "internalid", join: "file", summary: "group"}),
search.createColumn({name: "filetype", join: "file", summary: "group"}),
search.createColumn({name: "name", join: "file", summary: "group"}),
]
}).run().each(function (result) {
if (result) {
images.push({
id: result.getValue({name: "internalid", join: "file", summary: "group"}),
file_name: result.getValue({name: "name", join: "file", summary: "group"}),
file_type: result.getValue({name: "filetype", join: "file", summary: "group"}),
});
}
return true;
});
return images;
};
var images = getHtmlFilesOnReturnAuth("2134404", "HTMLDOC");

Resources