Saturday 8 February 2020

How to create graph based custom report ?

Thanks to NetSuite - SuiteAnswers for great help, I referenced the code used in SuiteAnswers.

I have created a custom downloadable report/graph in pdf format generated from saved search results.

As per NS suite answer's suggestion, I have used BFO and SuiteLet in SuiteScript 2.0 version to design this. 

Design approach:

I have used "Search" module and "Render" modules in SuiteLet also BFO. 

Search - to create search and results.

Render - this module encapsulates functionality for printing, PDF creation, form creation from templates, and email creation from templates.

BFO can generate graphs like pie graphs, line graphs, bar graphs etc. For more info about BFO, one can download their guide from here: http://faceless.org/products/report/docs/userguide.pdf
BFO is also supported in SuiteScript and one can read about it on the help guide. The sample code below does the following:
  1. Search all cancelled Sales Orders and group them by location
  2. Generate an XML markup that conforms with BFO standards
  3. Transforms the XML markup to PDF using render module
  4. Prints the pie graph onto the screen
Sample Code:

/**
 * @NApiVersion 2.x
 * @NScriptType Suitelet
 * @NModuleScope SameAccount
 */
define(['N/search', 'N/render'],
    function(search, render) {
        function onRequest(context) {
            var salesorderSearchObj = search.create({
                type: "salesorder",
                filters: [
                    ["type", "anyof", "SalesOrd"],
                    "AND",
                    ["mainline", "is", "T"],
                    "AND",
                    ["datecreated","within","01/01/2019 12-00 am","02/03/2020 11-59 pm"]
                ],
                columns: [
                    search.createColumn({
                        name: "formulatext",
                        summary: "GROUP",
                        formula: "TO_CHAR({datecreated}, 'Month')",
                        label: "Formula (Text)"
                    }),
                    search.createColumn({
                        name: "formulanumeric",
                        summary: "GROUP",
                        formula: "TO_CHAR({datecreated}, 'mm')",
                        sort: search.Sort.ASC,
                        label: "Formula (Numeric)"
                    }),
                    search.createColumn({
                        name: "location",
                        summary: "GROUP",
                        label: "Location"
                    }),
                    search.createColumn({
                        name: "internalid",
                        summary: "COUNT",
                        label: "Internal ID"
                    })
                ]
            });
            var xml = '';
            xml += '<?xml version="1.0"?>\n<!DOCTYPE pdf PUBLIC "-//big.faceless.org//report" "report-1.1.dtd">\n';
            xml += '<pdf>\n';
            xml += '<head>\n' ;
            xml += '<meta name="title" value="Number of Sales Orders per Location"/>\n';
            xml += '</head>\n';
            xml += '<body>\n';
            xml += '<piegraph width="200" height="150" yrotation="30" display-key="flat-outer">\n';

            salesorderSearchObj.run().each(function(result) {
                var month = result.getValue({
                    name: "formulatext",
                    summary: "GROUP"
                });
                var location = result.getText({
                    name: "location",
                    summary: "GROUP"
                });
                var numOrders = result.getValue({
                    name: "internalid",
                    summary: "COUNT"
                });
                xml += '<gdata name="' + month + '" name2="' + location + '" value="' + numOrders + '"/>'; //getText is used for displaying the names of the locations.
                return true;
            });
            xml += '</piegraph>\n';
            xml += '</body>\n</pdf>';

            var renderer = render.create();
            renderer.templateContent = xml;
            var newfile = renderer.renderAsPdf();
            context.response.writeFile(newfile);
        }
        return {
            onRequest: onRequest
        };
    });

Output:


Tuesday 15 October 2019

How to upload CSV file to SFTP server and download any file to File Cabinet?

Hello everyone,

Below are the steps which i followed to make connection with SFTP server and download the file from SFTP server.

Considered 2 scripts here, 1. Map/Reduce, 2. SuiteLet

1. Map/Reduce:

  • In getInputData() - created a saved search - Created the Customer payments search in my case.
  • processed the data from "map" function(key/value pair)
  • In Summerize section collected the data and pushed the results into csv file and created the file in a folder in file cabinet.
  • Established a connection with SFTP server (Received the Password GUID from the suitelet)
  • Uploaded the CSV file in SFTP server (unfortunately the considered test sftp server doesn't have the "write" permission hence i'm unable to upload, but this code works you can trust and use this, as it worked for one real time sftp server).
  • Downloaded a ".txt" file SFTP server in file cabinet. 
2. Suitelet:
  • Created 2 fields in form one for user name capturing and another credential field is password field (enter the sftp server user name and password  here)
  • the password field will be encrypted and send the value to Map/Reduce script parameter.
Above steps are my approach to connect to sftp server.

So, overall here are the steps to follow:

1. create a saved search.
2. Generate a CSV file using the saved search results.
3. Create a CSV file and upload in filecabinet.
4. Generate the host key from SFTP server (in my case i have used "https://www.sftp.net/public-online-sftp-servers" link and used "test.rebex.net" sftp server which has the "read-only" permission.
Got the host key from same website, you can use ssh -keyscan hostname or password.
5. use the host key in the script where you use sftp connection.
6. Create a Suitelet script and generate GUID using the sftp server password.
GUID stands for Global Unique Identifier. This is a unique identifier generated in Oracle NetSuite by a particular algorithm. You can use this algorithm to generate a GUID for a specific domain and script.
7. Create the connection and upload the CSV file into SFTP server or download the file from sftp to NS file Cabinet.

Code:
Map/Reduce Script:
/**
 * @NApiVersion 2.x
 * @NScriptType MapReduceScript
 * @NModuleScope SameAccount
 * 
 * This script push the saved search results into a Folder in FileCabinet
 * From File Cabinet, script uploads file from filecabinet to SFTP Server.
 * Also, this script downloads a file from SFTP server to File Cabinet.
 * 
 */
define(['N/file', 'N/sftp', 'N/search', 'N/url', 'N/runtime'],
/**
 * @param {file} file
 * @param {search} search
 */
function(file, sftp, search, url, runtime) {
   
    /**
     * Marks the beginning of the Map/Reduce process and generates input data.
     *
     * @typedef {Object} ObjectRef
     * @property {number} id - Internal ID of the record instance
     * @property {string} type - Record type id
     *
     * @return {Array|Object|Search|RecordRef} inputSummary
     * @since 2015.1
     */
    function getInputData() {
    // Create Customer Payments Saved Search
    return search.create({
       type: "customerpayment",
       filters:
       [
          ["type","anyof","CustPymt"], 
          "AND", 
          ["mainline","is","F"]
       ],
       columns:
       [
          "entity",
          "statusref",
          "amountpaid"
       ]
    });

    }

    /**
     * Executes when the map entry point is triggered and applies to each key/value pair.
     *
     * @param {MapSummary} context - Data collection containing the key/value pairs to process through the map stage
     * @since 2015.1
     */
    function map(context) {
   
    var searchResult = JSON.parse(context.value);
   
    var transId = searchResult.id;
    var entityId = searchResult.values.entity.text;
    var status = searchResult.values.statusref.text;
    var amount = searchResult.values.amountpaid;
    //key, value pairing
    context.write(searchResult.id,entityId+','+status+','+amount);
    }

    /**
     * Executes when the reduce entry point is triggered and applies to each group.
     *
     * @param {ReduceSummary} context - Data collection containing the groups to process through the reduce stage
     * @since 2015.1
     */
    function reduce(context) {
   

    }


    /**
     * Executes when the summarize entry point is triggered and applies to the result set.
     *
     * @param {Summary} summary - Holds statistics regarding the execution of a map/reduce script
     * @since 2015.1
     */
    function summarize(context) {
   
    var contents = 'ENTITY ID'+','+'STATUS'+','+'AMOUNT\n';
    var totalRows = 0;
   
   
    context.output.iterator().each(function(key, value) {
           contents += value+'\n';
             totalRows++;
             return true;
          });
    //set the date and time
    var date = new Date();
   
    var dd = date.getDate();
    var mm = date.getMonth()+1;
    var yy = date.getYear()+1900;
    var time = date.getHours() +":"+date.getMinutes()+":"+date.getSeconds();
    //append the date and time values to the filename
    var file_name = "Customer Payments"+"_"+dd+"/"+mm+"/"+yy+"_"+time
    //Create a CSV file and place the search results into it
          var fileObj = file.create({
             name: file_name,
             contents: contents,
             folder: 12641,
             fileType: 'CSV'
          });
          var FileObjNum = fileObj.save();
          // Get the password GUID from the script parameter (**The GUID is created in Suitelet script) 
          var pwdVal = runtime.getCurrentScript().getParameter({name:'custscript_custom_password'});
          log.debug("pasword Value",pwdVal);
          var json = JSON.parse(pwdVal);
          log.debug("json data",json);
          var myPwdGuid = json.password;
          
          // Get the host key from the SFT server
          var hostKey = "AAAAB3NzaC1yc2EAAAABJQAAAQEAkRM6RxDdi3uAGogR3nsQMpmt43X4WnwgMzs8"
           hostKey += "VkwUCqikewxqk4U7EyUSOUeT3CoUNOtywrkNbH83e6/yQgzc3M8i/eDzYtXaNGcK"
           hostKey += "yLfy3Ci6XOwiLLOx1z2AGvvTXln1RXtve+Tn1RTr1BhXVh2cUYbiuVtTWqbEgErT"
           hostKey += "20n4GWD4wv7FhkDbLXNi8DX07F9v7+jH67i0kyGm+E3rE+SaCMRo3zXE6VO+ijcm"
           hostKey += "9HdVxfltQwOYLfuPXM2t5aUSfa96KJcA0I4RCMzA/8Dl9hXGfbWdbD2hK1ZQ1pLv"
           hostKey += "vpNPPyKKjPZcMpOznprbg+jIlsZMWIHt7mq2OJXSdruhRrGzZw=="
           
            var sftUrl = "test.rebex.net";
          // Connect the SFTP with the details
            var connection = sftp.createConnection({
                   username : 'demo',
                   passwordGuid : myPwdGuid,
                   url : sftUrl,
                   port : 22,
                   directory : "/",
                   hostKey : hostKey
                    });
          
          log.debug("Connection established successfully")
          
          log.debug("File Objet",FileObjNum); 
          /**
           * Downloading a ".txt" file from external SFTP server to File Cabinet.
           */
          var downloadedFile = connection.download({
              directory : '/pub/example',
              filename : 'readme.txt'
          });
          downloadedFile.folder = 12641;
          downloadedFile.save();
          
          log.debug("Downloaded Successfully)")
          
          /**
           * Uploading the file to the external SFTP server.
           */
          var myFileToUpload = file.load({
              id : FileObjNum       
              });     
          
          connection.upload({
              directory : '/aspnet_client/system_web',
              filename : file_name,
              file : myFileToUpload,
              replaceExisting : true
          });
          
          log.debug("File Uploaded successfully");
          
    }
    return {
        getInputData: getInputData,
        map: map,
       // reduce: reduce,
        summarize: summarize
    };
    
});

Suitelet:

/**
 * @NApiVersion 2.x
 * @NScriptType Suitelet
 * @NModuleScope SameAccount
 */
define(['N/ui/serverWidget', 'N/task', 'N/file','N/runtime'],
/**
 * @param {serverWidget} serverWidget
 */
function(serverWidget, task, file, runtime) {
   
    /**
     * Definition of the Suitelet script trigger point.
     *
     * @param {Object} context
     * @param {ServerRequest} context.request - Encapsulation of the incoming request
     * @param {ServerResponse} context.response - Encapsulation of the Suitelet response
     * @Since 2015.2
     */
    function onRequest(context,FileObjNum) {
    if (context.request.method === 'GET') {
   
    // Create a GUID form
    var form = serverWidget.createForm({

    title: "SFTP - GUID Form",

    });
   
    // add the username field
    form.addField({

    id: 'username',

    type: serverWidget.FieldType.TEXT,

    label: 'Username'

    });
    // add password field
    var credField = form.addCredentialField({

    id: 'password',

    label: 'Password',
    //make sure you mention the script where SFTP connections takes place
    restrictToScriptIds: 'customscript_nf_mr_uploadcsv',

    restrictToDomains: "test.rebex.net"

    });
   
    credField.maxLength = 64;
    //Create a Submit button
    form.addSubmitButton({

    label: 'Submit Button'

    });
    context.response.writePage(form);

    return;
    }else {

    var requset = context.request;

    var myPwdGuid = requset.parameters.password;

    log.debug("myPwdGuid", myPwdGuid);

    context.response.write(myPwdGuid);

    }
   
    var objParam = JSON.stringify({"password": myPwdGuid});
    //Create task to call Map/Reduced
    var mrTask = task.create({
    taskType: task.TaskType.MAP_REDUCE,
    scriptId: "customscript_nf_mr_uploadcsv",
    deploymentId: "customdeploy1",
    params: {'custscript_custom_password': objParam} //use the MapReduce script parameter internalid here
    });
   
    var mrTaskId = mrTask.submit();
    var taskStatus = task.checkStatus(mrTaskId);
    log.debug("taskStatus", taskStatus);
           
    }

    return {
        onRequest: onRequest
    };
    
});

Output Screenshots:

1.GUID - Generation screen - from Suitelet
2. Create file in file cabinet and check the download file from sftp server.

3. Check the log whether connection established successfully or not and downloaded the file successfully or not


4.  Check the .txt file in sftp server which is downloaded successfully.

Unfortunately I don't have proper SFTP server where i cannot upload the file in file sftp server, but able to download the file into file cabinet. I'm sure this code will definitely work if we have proper sftp server with all permissions.

Note: If any issue like permission issue, like failed to upload then system will throw below error:
{"type":"error.SuiteScriptError","name":"UNEXPECTED_ERROR","message":null,"stack":["anonymous(N/file)","summarize(/SuiteScripts/Ram Suite Script files/_nf_uploadcsv_mr.js:160)"],"cause":{"type":"internal error","code":"UNEXPECTED_ERROR","details":null,"userEvent":null,"stackTrace":["anonymous(N/file)","summarize(/SuiteScripts/Ram Suite Script files/_nf_uploadcsv_mr.js:160)"],"notifyOff":false},"id":"c66e19b0-046f-4c01-887c-26e8a6e8724e-2d323031392e31302e3135","notifyOff":false,"userFacing":false}

But, please follow my suggestions, you will be able to upload the file in file cabinet.

Also, suggest me if you have any opensource sftp connection details including hostkey, i'll try to connect.

Thanks,
Ramu

Tuesday 17 September 2019

Understand how to validate the Address record using suite Script?

In NetSuite, Address is a subrecord which is placed in the address subtab.


Now, i wanted to make "Phone" is mandatory.

Here is the process how can we go-ahead:

1. Login to NetSuite, Go to Customization -> Forms -> Address Forms.
2. Create a custom address form.
3. Make sure create a script to validate the phone number field, (Use only suitescript 1.0)

Sample code is below: (Used Client Script)

function ValidateAddress(type){

var phNum = nlapiGetFieldValue('addrphone');
if ((phNum == "") || (phNum == null)) {
alert ("Please enter the Phone number");
}

    return true;
}

4. Now go back to the custom address form, go to Custom code subtab, "Script File" select the
script, which is created in above step. Attach the script here like :
   
        In the script file field -> click on the "Plus(+)" button, a window popups:


5. Select the script file and close the window.
6. under the "Validate Field Function" field, select the function from the suitescript function where we had the validation logic (ex: i have mentioned ValidateAddress).

7. Save the form and the customization is completed.

Now, go to address subtab in employee, click on the pencil icon, the address subrecord will be opened, now click on the "Ok" button. the alert populates:



Thanks,
Ramu






Thursday 22 August 2019

What is SuiteAnalytics Connector ? how can we establish a connection between Netsuite and MS PowerBI using the suite analytics connector?


Suppose if you want to connect NS with any 3rd paty Data analytics applications, we can do with “Suite Analytics connector”.

 Connect Service, lets you archive, analyze, and report on NetSuite data using a third-party tool or any custom-built application on any type of device using a Windows, Linux, or OS X operating system.
Once the Connect Service is enabled, then NetSuite offers ODBC, JDBC, and ADO.NET drivers that you can download, install, and use to connect to the Connect Service.

Note: The SuiteAnalytics Connect Service provides a read-only method for obtaining NetSuite data. You cannot use the Connect Service to update NetSuite data.

(There are many different ways that we can connect, but what I followed is using ODBC drivers, if you need more details I prefer you to go through Netsuite help docs)


To connect the Suite Analytics connector please follow below process:  (as administrator)
1. Login to NetSuite , Go to Setup -> Enable Features -> Analytics -> enable the “Suite Analytics Connector” checkbox and save it.
2. Go to Home page -> Settings portlet -> click on “Set Up SuiteAnalytics Connect”.

3.    Once clicked on Set Up Suite Analytics connect, then you will be navigated to “SuiteAnalytics Connect Driver Download “ page.

You can select the required bundle for the OS (windows/Linux).
In the example I have downloaded 64bit “ODBC INSTALLATION BUNDLE 64-BIT (7.20.55)” bundle.


4. Once the download is complete then you can see the "NetSuiteODBCDrivers_Windows64bit" zip file in the download folder.

5. Extract the "NetSuiteODBCDrivers_Windows64bit", and double click on the highlighted application.




6. the below window will be opened and click on the Next button.



7. Once Next button is clicked, then accept the license.


8. Once license is accepted, then you will be displayed with Datasource configuration details. Now, enter the configuration details like which are displayed in step 3.


9. Finish the installation process.

This way ensures that the ODBC setup is completed in the windows system.

Once installation completes we can see the connection details in Program Files.




Now, connecting with MS Power BI (I'm going through the steps after installing the MS Power BI)

1. Go to MS Power BI application, go to File -> Get Data -> Click on "More"

2. Go to Other and click on "ODBC" in the pop up window


3. Double click on the ODBC, and you will be navigated to "From ODBC" pop up window, select "NetSuite" in the drop down.


4. Once clicked on "OK" button, Navigator window will be open, select the data what ever you load in the Power BI.



5. Click on the Load button once you select what data you want to visualize.


This ensures the connection establishes between netsuite and power bi.

Like this we can connect any other analytics application.

Again, i'm repeating if you want more details about suite analytics connector please go though the suiteAnswers.

Thanks,
Ramu

Saturday 27 July 2019

How to access Sublist using Workflows ?

Accessing Sublist using workflows is pretty much possible using Suiteflow. 

As per the draft version from release notes of 2019.2 is :
 you can set the following workflow actions to execute on supported sublists:

■ Return User Error
■ Set Field Value
■ Set Field Display Type

This enhancement increases the functionality on sublists and allows for greater customization of workflows. For an overview of SuiteFlow actions, including action properties, triggers, and conditions, see the help topic Workflow Actions.
To set an action to execute on a sublist, in a workflow, select the state that you want to add the action to. Click the New Action icon in the context panel. In the New Action window, click an action. In the Triggering Client Fields section of the Workflow Action window, choose Sublist, and complete the parameters and conditions section of the action. When you are done entering the action details, click Save. For more information, see the help topic Creating an Action.

For more details, please download the release notes from SuiteAnswers.

Tuesday 23 July 2019

how to implement Token based authentication in NetSuite ?


Token Based Authentication in NetSuite:


NetSuite supports token-based authentication (TBA) a robust, industry standard-based mechanism that increases overall system security. 

This authentication mechanism enables client applications to use a token to access NetSuite through APIs, eliminating the need for RESTlets or web services integrations to store user credentials.

Tokens can be used instead of usernames and passwords.

Benefits of  using the TBA:

     -  Safer and Easier to manage.
2         -  Programmatically can create Tokens.
3         - Password rotation policies don’t apply to Tokens, like if any password that is expired then you  don’t               need to worry about it

.Below are the steps which needs to be followed to setup TBA in NetSuite:

1             
        -  Enable the Token Based Authentication feature in NetSuite.
2      -  Setup the TBA roles with permissions.
3      -  Assign the TBA Roles to the users.
4      -  Setup applications for Token based authentications.
5      -  Create User Tokens.

Here is how to implement in detail:

      -  Enable the Token Based Authentication feature in NetSuite.

N 1. Navigate to Enable Features page: 
s     Setup -> Company -> Enable Features:
       
      Under Suite cloud Tab -> Under Manage Authentication Section -> Select "Token Based Authentication.
     It will as you to take action on "Terms of Service" , select "I agree" button. Then click on save button.


2. Setup the TBA roles with permissions.

Once the service is enabled, we can assign below Token-based Authentication Permissions to any role: (Except Administrator)

Access Token Management
  • Users (Non Administrator role) with this role can create and revoke access tokens for user with TBA role enabled. 
  • A user who doesn't have "Administrator" role cannot create tokens for administrator.
  • Cannot create access for their own use.
  • Cannot use access tokens to login through RESTLets or WebServices.     


       User Access Tokens
  •  Users with this permission can manage their own tokens using the Manage Access Tokens link in the Settings portlet, and they can log in using a token.
  • Users can use access to login through RESTLets or WebServices.
Log in using Access Tokens
  • Users with only this permission can log in using a token, that is, they can to use tokens to call a RESTlet.
  • Cannot create their own access tokens through a link in the Settings portlet, or by calling the token endpoint.
Create a new Role and assign TBA permissions to it:

Go to Setup -> Users/ Roles - > Manage Roles -> New

Go to the role in create/Edit mode, Under Permissions Tab: Select the any of the TBA permissions as per the requirement and save the role.



Assign the TBA Roles to the users:

Now, after creating the role, then go to the employee record to whom you want assign, 
Lists -> Employees -> List -> Select the employee (Select New if you want to create):

In Access Subtab - > Roles -> Add the newly created role, and save the employee record.



Setup applications for Token based authentications.

             We can see all the Integration applications in the below navigation:
              Setup -> Integrations -> Manage Integrations -> List (we can see all the 3rd party integration applications list)

           For our test, we are creating new Integration, like below:

If the State is Enabled means:  if you want to permit connections from the external application represented by this integration record. 

If the State is Blocked means : if you want to prevent such connections.

Click on the Token-Based Authentication check box under "Authentication" subtab and save the integration record.



Once the Integration is saved then the "Application ID" will be generated automatically.

And under "Authentication" section, We can see "Consumer Key" and "Consumer Secret".

For Security reasons the Consumer key/Secret displays only one time. If suppose you forget the the key then you will need to reset them to obtain new values.

Create User Tokens:

We can assign user Tokens with below navigation:

               Setup -> Users/Roles -> Access Tokens -New
\
       Select the Application which needs to be integrated, then select the user (it will list the employee name who has the TBA roles), select the TBA role, Token Name field is populated with a concatenation of Application Name, User, and Role. Enter your own name for this token, if desired.

After saving this record, system will generate automatically a Token ID and Token Secret which will only displayed for one time.
            
       Creating own Tokens:
        
                 Go to Home -> Settings portlet -> Manage Token (here we can see list of the my access tokens ).