Sunday, March 31, 2024

Sudo Make me a Sandwich: Adding Buttons for Privileged Actions


    Users have limited permissions, and for good reason. Sometimes we want to allow them to perform specific actions that are forbidden by their permissions though. Unfortunately, NetSuite doesn't have very granular controls for how users can interact with records.

    Let's say that a we have a "Trainer" role that can view, but not edit Employee records. Despite this, they need to be able to update Employee records to show that they trained a given employee. This involves setting a custom "Trained" checkbox, and a "Trained By" field that holds the ID of the trainer.
    The trainer would like to navigate to the Employee record, somehow set the fields, and move on. We can let them do this with a custom button.


Button, Button, Who's Got The Button


    Since our user doesn't have edit permissions on the record, this limits our methods of adding buttons. Unless we want to use a WorkFlow, a User Event is our only option. Here is the bare minimum to add our "Trained" button. We can deploy it for our Trainer role so that only they can see the button.

/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 */
define(['N/runtime'], (runtime) => {
    function beforeLoad(context) {
        context.form.addButton({ label: 'Trained', id: 'custpage_trained', functionName: 'setTrainedFields' });
    }

    return { beforeLoad };
});

    Now that we have the button, we need to make it do something. It's looking for a setTrainedFields() function that doesn't exist. We can add this function to a typical Client Script, but these only run on edit. One solution is to specify the CS from within the UE. This is done with the clientScriptModulePath property of the form object, and will make it load on view.

context.form.clientScriptModulePath = './setTrained_CS.js';



Building Your Clientele


    In our client script we need to somehow set two custom fields that we still don't have the required permissions to edit. Client scripts can't be set to run as a different role either. This means that we need to call yet another script, one that can have elevated permissions. The NetSuite help gives us a list of script types that can run as different roles here. Relevant options are: Suitelet, User Event, and Mass Update.    

    Scheduled scripts and Map/Reduces also run with elevated privileges, as they're hard coded to use the Administrator role. Unfortunately, these require the task module to call them, and it isn't available to client scripts.
    Mass Update scripts can't be called from other scripts at all, so this leaves Suitelets and User Events. We would typically use the redirect module to get to a Suitelet, but this isn't available to CSs either. We can manually redirect the browser though.
    A User Event would be convenient, since we're already using one to create the button. Normally we would use the beforeSubmit to change the field values, but no edit permission means beforeSubmit doesn't fire. We can continue to use the beforeLoad that we already created though.

    We can have our CS redirect to the same page we're on, but with the addition of a parameter that the UE can read. Here's an example.

/**
 * @NApiVersion 2.1
 * @NScriptType ClientScript
 */
define([], () => {
    function pageInit(context) { } //must have at least one entry point

    function setTrainedFields() { //reload record with param set to trigger UE
        window.location.replace(window.location + '&setTrained=T');
    }

    return { pageInit, setTrainedFields };
});



Friends Are Always Tellin’ Me You’re a User


    We just need to give our UE a role that can set the needed fields, and have it run when it sees the param from the CS. There is a wrinkle though; due to having to run in view mode beforeLoad, any values we change won't be shown in the UI. We have to redirect to the same record yet again in order to display the new values. Luckily, the redirect module is available now that we're in a UE.
    We can pass another param to indicate that the button action has completed.

let { form, newRecord, request } = context;

if (request.parameters['setTrained'] == 'T') { //set fields and reload
    const values = { custbody_trained: true, custbody_trainedby: runtime.getCurrentUser().id };
    record.submitFields({ type: 'employee', id: newRecord.id, values });
    redirect.toRecord({ type: 'employee', id: newRecord.id, parameters: { trainedSuccess: 'T' } });
}

    We use the second param to trigger a confirmation message to inform the user that the action was performed successfully.

if (request.parameters['trainedSuccess'] == 'T') //show green success message
    form.addPageInitMessage({ type: 0.0, message: 'Approval Successful', duration: 7000 });

    Here's one way to combine our UE logic:

/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 */
define(['N/record', 'N/runtime', 'N/redirect'], (record, runtime, redirect) => {
    function beforeLoad(context) {
        let { form, newRecord, request } = context;

        if (request.parameters['setTrained'] == 'T') { //set fields and reload
            const values = { custbody_trained: true, custbody_trainedby: runtime.getCurrentUser().id };
            record.submitFields({ type: 'employee', id: newRecord.id, values });
            redirect.toRecord({ type: 'employee', id: newRecord.id, parameters: { trainedSuccess: 'T' } });
        } else if (request.parameters['trainedSuccess'] == 'T') { //show green success message
            form.addPageInitMessage({ type: 0.0, message: 'Approval Successful', duration: 7000 });
        } else { //display button
            form.addButton({ label: 'Trained', id: 'custpage_trained', functionName: 'approveVendBill' });
            form.clientScriptModulePath = './setTrained_CS.js';
        }
    }

    return { beforeLoad };
});


The Time Has Come to… Push the Button


    Upon pushing the button, the page should reload with the 'setTrained' param. The UE will detect it, update the values, and reload again with the 'trainedSuccess' param. In this way, a user can edit a record without having edit permissions.

Friday, March 29, 2024

Starting With a Bang: Handling Item Group Explosions


    Item Groups are a perpetual source of pain when customizing transaction sublists. They break the conventions of how items are added to sublists by using a poorly documented process called "exploding".

This rhythmatic explosion is what your frame of mind has chosen - Nas
 

A Series of Unfortunate Events


    NetSuite will trigger several events as an Item Group is added and exploded. The most useful one is sublistChanged().
    It's important to know that Item Groups can have their start/end lines disabled. This means you won't see those extra lines on your transaction, and the explosion process will be slightly different.
    

First...

    sublistChanged fires as the IG item is committed. At this point, the sourcing is done, and you can read the itemtype sublist field to identify it as a 'Group' type item. You can't assume that every commit of an IG is the start of an explosion though! It could be an edit. We can read a couple of seemingly undocumented sublist fields to help tell them apart: groupsetup and includegroupwrapper.
    groupsetup tells us if the IG is already setup (exploded). 'T' if exploded, 'F' if not. It's only populated on the start line of an IG though. As mentioned earlier, this line can be disabled, meaning it might not be present. We can read the next field to know what to expect.
    includegroupwrapper tells us if the IG has start/end lines. 'T' if the lines are enabled, 'F' if not.

And maybe...

    sublistChanged will fire again to remove the IG item if includegroupwrapper is 'F'. This happens prior to the members being added, and will temporarily yield an empty sublist if no other items have been added.

Then...

    sublistChanged fires one last time once the members have been added. The line index will be set to the last line of the sublist. Confusingly, the values on the current line will match the line after the end of the item group. You can actually duplicate this line just by committing the current line. This seems to be an oversight, and is only noticeable when inserting an IG. When adding an IG to the end of a sublist, the line after the IG and the last line of the sublist will be the same blank line.

Finally...

    postSourcing fires on various tax-related fields, and signifies the end of the explosion process.


Getting to the Bottom of It


    Another difficulty is determining the last line of an IG. As previously mentioned, the explosion leaves us on the last line of the sublist, and not the last line of the IG.
    We can get the first line # of the IG during the first sublistChanged event. If we know the IG has an end line, we can search for it beneath the start line. This means testing each line for an itemtype of 'EndGroup'. If we encounter the start of another IG, or the end of the sublist before we find the end, then something has gone wrong.
    We need a different approach for IGs that don't have start/end lines enabled though. We can store the sublist's length on the first sublistChanged, and subtract it from the length on the second sublistChanged. This will give us the length of the IG. Adding this to the first line # gives us the last line #.


Putting it Together


    1. We detect the start of an explosion by waiting for a sublistChange that meets the following criteria:
  • context.sublistId is 'item'
  • context.operation is 'commit'
  • the current sublist value for itemtype is 'Group'
  • groupsetup is not 'T' or includegroupwrapper is not 'T'
Now that we've found the first line, we can store its number along with the current length of the sublist.
    
    2. We wait for the next sublistChange that's a commit. It should be for the last line of the sublist.
We determine the last line of the IG by either searching for the End Of Group item, or subtracting the original line count from the current line count and adding the start line #.

    3. We wait for postSourcing to fire for 'nexus' or other tax fields. Now we know the Item Group has exploded, where it is, and that it's safe to manipulate.

Don't you never, ever, pull my lever because I explode and my nine is easy to load - LL Cool J

 

Return to Sender: POST a Suitelet without redirecting

     We all know the pain of submitting a form in NetSuite, only to have it error and lose the input. This happens because NetSuite forces a...