Query attachment from a Message record - netsuite

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");

Related

how to get the 'Credit Applied' sublist data in Vendor Payment Record from script? (NetSuite)

i need to reflect the credit applied sublist data in my printout suitelet
but when i get the line count for credit line( var itemCount_credit = billPayRec.getLineCount({ 'sublistId': 'credit' }); ), system logs line count as -1.
Is there any way to fetch this data and if its possible from search than any suggestion what filters / columns woul i require to include?
Unless you are able to intercept the request, no. They keep this information obfuscated and by the time it has reached the sublist, it's just cached data. You could get some of the field data with inspect element but likely only what you can see.
Since you are using a Suitelet you can extract the credits applied as follows. This was run in the console but the core of it would work in a SS2.1 script. Convert ()=>{ functions to function(){ for SS2.0
require(['N/search'], search=>{
const appys = [];
search.create({
type:'vendorbill',
filters: [
[ 'internalid', 'is', 'your vendor bill internalid']
],
columns:[
'tranid',
'applyinglinkamount',
'applyinglinktype',
'applyingtransaction'
]
}).run().each(ref=>{
if(ref.getValue({name:'applyingtransaction'})) appys.push(ref.getValue({name:'applyingtransaction'}));
const c = ref.columns;
c.forEach(c=>{
const v = ref.getValue(c);
if(!v) return;
console.log(c.name, v);
});
console.log('');
return true;
});
search.create({
type:'transaction', // or just vendorcredit for you original question
filters:[
['internalid', 'anyof', appys], 'AND',
['mainline', 'is', 'T']
],
columns:['tranid'] // plus any columns of interest.
}).run().each(ref=>{
// if filtering type:'transaction' you'll get a mix of vendorcredit, vendorpayment, journalentry
console.log(ref.recordType, ref.getValue({name:'tranid'}));
return true;
});
});

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)

knex js query many to many

i'm having trouble with node & knex.js
I'm trying to build a mini blog, with posts & adding functionality to add multiple tags to post
I have a POST model with following properties:
id SERIAL PRIMARY KEY NOT NULL,
name TEXT,
Second I have Tags model that is used for storing tags:
id SERIAL PRIMARY KEY NOT NULL,
name TEXT
And I have many to many table: Post Tags that references post & tags:
id SERIAL PRIMARY KEY NOT NULL,
post_id INTEGER NOT NULL REFERENCES posts ON DELETE CASCADE,
tag_id INTEGER NOT NULL REFERENCES tags ON DELETE CASCADE
I have managed to insert tags, and create post with tags,
But when I want to fetch Post data with Tags attached to that post I'm having a trouble
Here is a problem:
const data = await knex.select('posts.name as postName', 'tags.name as tagName'
.from('posts')
.leftJoin('post_tags', 'posts.id', 'post_tags.post_id')
.leftJoin('tags', 'tags.id', 'post_tags.tag_id')
.where('posts.id', id)
Following query returns this result:
[
{
postName: 'Post 1',
tagName: 'Youtube',
},
{
postName: 'Post 1',
tagName: 'Funny',
}
]
But I want the result to be formated & returned like this:
{
postName: 'Post 1',
tagName: ['Youtube', 'Funny'],
}
Is that even possible with query or do I have to manually format data ?
One way of doing this is to use some kind of aggregate function. If you're using PostgreSQL:
const data = await knex.select('posts.name as postName', knex.raw('ARRAY_AGG (tags.name) tags'))
.from('posts')
.innerJoin('post_tags', 'posts.id', 'post_tags.post_id')
.innerJoin('tags', 'tags.id', 'post_tags.tag_id')
.where('posts.id', id)
.groupBy("postName")
.orderBy("postName")
.first();
->
{ postName: 'post1', tags: [ 'tag1', 'tag2', 'tag3' ] }
For MySQL:
const data = await knex.select('posts.name as postName', knex.raw('GROUP_CONCAT (tags.name) as tags'))
.from('posts')
.innerJoin('post_tags', 'posts.id', 'post_tags.post_id')
.innerJoin('tags', 'tags.id', 'post_tags.tag_id')
.where('posts.id', id)
.groupBy("postName")
.orderBy("postName")
.first()
.then(res => Object.assign(res, { tags: res.tags.split(',')}))
There are no arrays in MySQL, and GROUP_CONCAT will just concat all tags into a string, so we need to split them manually.
->
RowDataPacket { postName: 'post1', tags: [ 'tag1', 'tag2', 'tag3' ] }
The result is correct as that is how SQL works - it returns rows of data. SQL has no concept of returning anything other than a table (think CSV data or Excel spreadsheet).
There are some interesting things you can do with SQL that can convert the tags to strings that you concatenate together but that is not really what you want. Either way you will need to add a post-processing step.
With your current query you can simply do something like this:
function formatter (result) {
let set = {};
result.forEach(row => {
if (set[row.postName] === undefined) {
set[row.postName] = row;
set[row.postName].tagName = [set[row.postName].tagName];
}
else {
set[row.postName].tagName.push(row.tagName);
}
});
return Object.values(set);
}
// ...
query.then(formatter);
This shouldn't be slow as you're only looping through the results once.

Source State record to sublist field

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().

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 }) ]
})

Resources