Sample Code

Ad-hoc Invoice Override

Use Case

BillingPlatform provides a robust and broad set of options for high-volume, automated invoice generation based on recurring revenue but when it comes to ad-hoc invoice creation the User Interface may be less than elegant. To create an ad-hoc invoice using the standard UI you must first create the invoice, then add the line items one-by-one. 

In the following example, we will demonstrate how to use the toolkit to simplify manual invoice creation by combining several activities into a single UI and making that UI accessible via the Standard Application using a custom Button on the Account Node.

Components Demonstrated in this tutorial:

  • Page Widget
  • BP Toolkit Tag Library
    • BPUI.Page
    • BPUI.Divider
    • BPUI.Panel
    • BPUI.PanelRow
    • BPUI.PanelRowColumn
    • BPUI.EmbeddedList
    • BPUI.TableColumn
    • BPUI.InputField
    • BPUI.Button
    • BPUI.Message
  • BP JavSscript Tag Library
    • BPUI.ReferenceObject()
    • BPSystem
      • getSelectedEntityId
      • BPSystem.nodeName
      • nodeKey
      • rootNodeName
      • toBPCollection
      • cancel()
      • initialize()
    • BPObject
      • get()
      • set()
      • query
      • collection()
    • BPConnection
      • retrieveAsync()
      • retrieveFiltered()
      • retrieveFilteredAsync()
      • create()
      • delete()
    • BPActions
      • uploadFile()
      • handleErrors()
      • refreshState()
    • BillingProfile Object
    • Invoice Object
    • Activity Object
    • AccountProductQuote Object
  • Billing Platform Web Toolkit IDE
  • Custom Button
  • Custom Fields
  • Post Save Navigation 

The structure for Creating Page Widgets can be thought of as implementing the “Model, View, Controller” Design pattern wherein customizations to the Application Entities, Fields and Relationships represent the “Model”; the BP Tags and HTML represent the “View”; and the JavaScript, Toolkit Libraries represent the “Controller” .

 

Entity Setup (Model)

For this solution, we decided to add attachments to the ad-hoc invoice so that the use could add things like receipts, bill of materials, etc. in one spot while creating the ad-hoc invoice. To do this, navigate to System>Develop>Entities. Search for and select the INVOICE Entity. 

Navigate to the Entity Fields node and click “New”. Select the DOCUMENT field Type. Fill in the other information for the field according to the image below and save. Repeat this for Attachment2 and Attachment3 fields as well.

 

 

JavaScript (Controller)

To create a new Page Widget navigate to Setup>Develop>Page Widgets and Click “New”. Here you will find four tabs. JavaScript, CSS HTML and Preview. Here is where we enter the logic and processing for the widget – the “Controller” so to speak in a “Model View Controller” design pattern.

Below is the source with Comments for Creating the JavaScript Controller for the Ad-Hoc Invoice Widget.       

	//
        //Using the Window Objects enables the ability to debug using the Javascript Console
        //
        window.billingProfile = new BPUI.ReferenceObject();
        window.invoice 	   = new BPUI.ReferenceObject();
        window.activities     = new BPUI.ReferenceObject();//ReferenceObject
	window.account        = new BPUI.ReferenceObject();		
        BPSystem.nodeKey      = BPSystem.getSelectedEntityId(BPSystem.nodeName)

        //
        //Initialize the BP connection and session for database access
        //
        BPSystem.initialize();

        //
        // Initialize the Form Objects
        //
        function init() {

            //instantiate an array of activity objects
            //initialize the activities collection with three blank rows.
            activities.set(BPSystem.toBPCollection([{},{},{}],new Activity()));
            
            //default the activity dates to today's date with formatting
            activities.get().forEach(function (element,index,allArray){
                element.ActivityDate = moment(new Date()).format('YYYY-MM-DD');
            });

            // If we are on the account tab then preload the billing profile and set it to the
            // selected account
            // usint the retrieveFiltered method
            if(BPSystem.rootNodeName=="ACCOUNT_ROOT" && BPSystem.nodeKey != null)
            {
                try{
                	billingProfile.set(BPConnection.BillingProfile.retrieveFiltered(
"AccountId = " + BPSystem.nodeKey ).single()); } catch(Err){ alert(Err.message); } } else { //Instantiate a new BillingProfile Object and fields billingProfile.set(new BillingProfile()); } //Initialize the Invoice Object and fields invoice.set(new Invoice()); //Set start date and end date for the invoice with formatting invoice.get().BillingCycleStartDate = moment(new Date()).format('YYYY-MM-DD'); invoice.get().BillingCycleEndDate = moment(new Date()).format('YYYY-MM-DD'); invoice.get().ManualCloseApprovedFlag = "0"; //If a billingProfile is set, set the Parent Id on the Invoice if(billingProfile.get().Id != null) invoice.get().BillingProfileId = billingProfile.get().Id; } BPUI.afterRender = function () { invoice.get().BillingProfileId = billingProfile.get().Id; } // //Simple Cancel function // function cancel() { BPSystem.cancel(); } // // Used to calculate the rate and amount given a selected Account, Product and Quantity. // This will fire from the onBlur event for each cell in the Activity Embedded List. // function calculateRate(row,column,event,scope) { var activityCollection = activities.get(); var rowElement = activityCollection.elements[row]; console.log("CALCULATE RATE ",column,row,event,scope); //javascript events for an embedded list are registered for each cell of the collection //we can reference each by column number and by array element name. if (column==1||column==3||column==4) { if (column == 1||column==3) { //the row is also passed in from the embedded list event listner if(rowElement.ProductId != null && rowElement.Quantity != null) { try { //leverage the AccountProductQuote view to retrieve rates for
//a given Account var whereClause = "account_id ="+BPSystem.nodeKey +" and product_id ="+ rowElement.ProductId +" and quantity ="+ rowElement.Quantity; //call the retrieveFiltered method using the supplied "Where" clause. BPConnection.AccountProductQuote.retrieveFilteredAsync(whereClause).single() .done(function (res){ rowElement.RateOverride = res.Rate.replace(/[^\d\.]*/g, ''); rowElement.TOTAL = res.GrandTotalAmount; }) .fail(function (fail){console.log(fail.message); alert(fail.message); }); } catch (e) { alert('ERROR: '+e); console.log(e); } } } //if a manual rate is supplied by the user, just do the simple math to calculate the
//associated amount if (rowElement.Quantity&&rowElement.RateOverride) { rowElement.TOTAL = (rowElement.RateOverride*rowElement.Quantity).toFixed(2); } } } // // Save invoice and invoice lines // function saveInvoices() { $('*').css('cursor', 'wait'); //clear any errors from previous processing BPActions.clearError(); // Create the Invoice record BPConnection.Invoice.create(invoice.get()).done(function (result){ //first attempt to upload the files to the new invoice BPActions.uploadFile("[data-name=Attachment1] [type=file]","INVOICE",
"Attachment1",result[0].Id).always(function (attachment1) { // Save second attachment BPActions.uploadFile("[data-name=Attachment2] [type=file]", "INVOICE",
"Attachment2", result[0].Id).always(function (attachment2) { // Save third attachment BPActions.uploadFile("[data-name=Attachment3] [type=file]", "INVOICE",
"Attachment3", result[0].Id).always(function (attachment3) { //uppon successful processing of the attachments, iterate through the
//Activity records //setting their Invoice and Account foreign keys activities.get().forEach( function (el,i,all) { el.InvoiceId = result[0].Id; el.AccountId = window.billingProfile.get().AccountId; }); activities.get().create(true).done(function (resultActivity) { //on successful creation of the invoice, attachments and Invoice
//Activity records, //navigate to the new Invoice record in "R" (record) mode. window.location =
"admin.jsp?name=BILLING_INVOICE&key="+result[0].Id+"&mode=R" }).fail(function (failError){ $('*').css('cursor', 'default'); //If the transaction fails, rollback by deleting
//the parent Invoice object and display the error BPConnection.Invoice.delete({"Id":result[0].Id}); //Display the Error on the UI BPActions.handleError(failError, activities); }); }) }); }); }).fail(function (res) { $('*').css('cursor', 'default'); BPActions.handleError(res); }); } // //addAll: Used to add a set of 3 activity line items // function addAll(value,field,entity) { var i = 0; while (i<3) { //add a new Activity Record to the current collection activities.get().addNew({}); i++; } //rfresh the UI to show the new Activity Records BPActions.refreshState("activities"); } // // clearAll: Used to clear the contents of the Activity Line Item Grid // function clearAll(value,field,entity) { //Iterate through each Activity Record setting its attributes to a blank value activities.get().forEach(function(el){ el['ProductId']=""; el['Description']=""; el['ActivityDate']=""; el['Quantity']=""; el['RateOverride']=""; el['TOTAL']=""; }) } // // accountIdUpdate: Used to reset the billingProfile data when the user changes the account
//from the lookup // function accountIdUpdate() { //Use the retrieveAsync method to load the Billing Profile in a seperate thread BPConnection.BillingProfile.retrieveAsync(invoice.get().BillingProfileId).done(function
(result) { window.billingProfile.set(result); }) } // // customRenderFunction: custom function called from the UI to set the date format when changed. // function customRenderFunction(val) { if (val) { return moment(val).format('MM/DD/YYYY'); } else { return val; } } //Call init() on page load. init();

 

HTML (View)

Here we are going to use the Toolkit’s built-in tags to create the user interface with minimal coding. Below snippet will go into the “HTML” tab on the development IDE on Setup>Develop>Page Widgets. The sample below will create an Invoice header section; An Invoice Line Items (Activities) section; Record controls for saving and canceling as well as input controls for attaching files. 

<BPUI.Page>

     {/*
     * CUSTOM AD-HOC INVOICE FORM
     * Notice the Curly braces wrapping the comment block here. This is part of the React JS framework.
     *
     */}
        {/*
         * Account and email Recipient data.
         * Loading the Account will retrieve the default email address from the associated Billing 
Profile */} <BPUI.Divider Name="Account Info" style ={{width: 1000 + "px"}}>
Account and Delivery Info</BPUI.Divider> <BPUI.Panel style ={{width: 900 + "px"}}> <BPUI.Message name="Quantity" variables={activities}/> <BPUI.Message name="ProductId" variables={activities}/> <BPUI.Message name="Rate" variables={activities}/> <BPUI.Message name="RateOverride" variables={activities}/> <BPUI.Message name="ActivityDate" variables={activities}/> <BPUI.PanelRow> <BPUI.InputField variable={invoice} field="BillingProfileId" label="BillingProfile"
onUpdate={accountIdUpdate}/> <BPUI.InputField name="BillingProfileId" variable={billingProfile} field="Email"/> </BPUI.PanelRow> </BPUI.Panel> {/* Spacer Row */} <BPUI.Panel style ={{width: 900 + "px"}}> <BPUI.PanelRow style ={{height: 30 + "px"}}> <BPUI.PanelRowColumn /> </BPUI.PanelRow> </BPUI.Panel> {/* * Invoice Line Items (AKA Activity) * Initialized on the javascript controller with 3 empty lines initially */} <BPUI.Divider Name="ActivityDiv">Invoice Line Items</BPUI.Divider> <BPUI.Panel style ={{width: 900 + "px"}}> <BPUI.PanelRow> <BPUI.PanelRowColumn colSpan="4"> {/*The EmbeddedList tag contains the attributes used to bind the data and
javascript events to the controller*/} <BPUI.EmbeddedList variable={activities} name="activities" width="100%"
onCellBlur={calculateRate}> <BPUI.TableColumn name="ActivityDate" type="DATE_SELECTOR"
displayTransform={customRenderFunction} index="1" label="Service Date" /> <BPUI.TableColumn name="ProductId" index="2" label="Product"/> <BPUI.TableColumn name="Description" index="3" label="Description"/> <BPUI.TableColumn name="Quantity" index="4" label="Quantity"/> <BPUI.TableColumn name="RateOverride" index="5" label="Rate"/> <BPUI.TableColumn name="TOTAL" index="6" label="Amount" editable="false"/> </BPUI.EmbeddedList> </BPUI.PanelRowColumn> </BPUI.PanelRow> </BPUI.Panel> {/* Spacer Row */} <BPUI.Panel> <BPUI.PanelRow style ={{height: 20 + "px"}}> <BPUI.PanelRowColumn /> </BPUI.PanelRow> </BPUI.Panel> {/* * Action Buttons * - Add Rows in bulk. Adds 5 new Invoice Line Item (Activity) rows * - Clear Rows: Clears the contents of all Invoice Line Items * - Save: Saves the invoice with approval pending. Redirects user to the new Invoice
in Record Mode. * - Save & Send: Saves the invoice with and Approves (sends if email is the delivery
option on the BillingProfile) * - Cancel: Returns to the original Node with the Key in context. */} <BPUI.Panel style ={{width: 900 + "px"}}> <BPUI.PanelRow> <BPUI.PanelRowColumn style ={{width: 50 + "px"}}/> <BPUI.PanelRowColumn style ={{width: 200 + "px"}}> <BPUI.Button name="addLines" title="addLines" onClick={addAll}/> <BPUI.Button name="clearLines" title="clearLines" onClick={clearAll}/> </BPUI.PanelRowColumn> <BPUI.PanelRowColumn style ={{width: 400 + "px"}}/> <BPUI.PanelRowColumn style ={{width: 300 + "px"}}> <BPUI.Button name="save" title="Save" onClick={saveInvoices}/> <BPUI.Button name="sendSave" title="Save & Send"/> <BPUI.Button name="cancel" title="Cancel" onClick={cancel}/> </BPUI.PanelRowColumn> </BPUI.PanelRow> </BPUI.Panel> {/* Spacer Row */} <BPUI.Panel> <BPUI.PanelRow style ={{height: 20 + "px"}}> <BPUI.PanelRowColumn /> </BPUI.PanelRow> </BPUI.Panel> {/* * Invoice Attachments. * Attachment Documents to associate with the Invoice */} <BPUI.Divider Name="Attachments" style ={{width: 1000 + "px"}}>Attachments</BPUI.Divider> <BPUI.Panel style ={{width: 900 + "px"}}> <BPUI.PanelRow> <BPUI.InputField variable={invoice} field="Attachment1" /> <BPUI.InputField variable={invoice} field="Attachment2"/> <BPUI.InputField variable={invoice} field="Attachment3"/> </BPUI.PanelRow> </BPUI.Panel> </BPUI.Page>

 

Previewing your project

The Built-in IDE comes with a “Preview” tab that allows you to check your progress and visual page manifestation as you develop. Clicking from tab to tab before saving will not loose your work as everything that you write for your widgets are executed in the browser. One thing to note is that is you intend to use variables to retrieve the data for your widget in context, remember to hard-code these keys for successful previewing. Here is an example of our widget in the Preview tab:

 

 

Adding the Widget to the Platform

There are three ways in which to add a Page Widget to the app so that users can access it:

  1. Add it to a custom button or action
  2. Override a standard “view” mode (i.e. New, Edit, Record or “Read”)
  3. Override the Entire Domain such that when the node is clicked in the menu, the widget takes over all view modes.

 

In our use case we are going to create a new button on the Account node for adding an Invoice “Ad-Hoc”. To add a button to the Account Entity go to Setup>Develop>Entities and select the “Account” entity. Select  “Buttons and Actions” Menu item and Click “New”.

 

Here you can create a button that will invoke our new AdHocInvoice Page widget. Give our new button a label, Name, and description. We will choose the “Button” Display type this time which will create a button with the supplied Label on the Account Node. Choose the “Page” Content Source (As opposed to Extension) and look up the Ad-Hoc Invoice Page Widget we just created. 

You can also supply filter criteria and associated Join logic for controlling conditional rendering of your button and also define which roles have access to this new component through your button. 

In this case, we want the button to display all the time to everyone. Add the Button to all Roles and click save. You will now see the button render on the Account Page and when clicked, the Ad-Hoc Invoice Page widget will render.

 

 

Use Case

On the Account screen we want to leverage the power of the Google Maps API to create a graphical representation of the Customers Address on the Account Page Just below the Address Details section on the Standard UI. We will use the Toolkit to create an interactive map widget that changes when the User updates any of the Address fields on the Account Page and Also displays the Account’s Current Address Map in View Mode.

Components Demonstrated in this tutorial:

  • Extension Widget
  • Account Page Layout Modification
  • BillingProfile Object
  • BPConnection
    • query()
  • BPSystem
    • nodeKey
  • BPUI
    • afterRender()
  • Google Map plug-in

 

Creating the Widget

For this project we will create an “Extension” widget. We use this type of widget when we want to plug it into the standard page or when we want to influence the standard Submit and Delete button behavior. To create a new Extension Widget go to Setup>Develop>Exension Widgets and Click “New”.

 

JavaScript

The Javascript Below demonstrates how to create a Google Map object and supply address incormation to it from the User Interface as well as the Database using the Toolkit.

    //
    // Initialize the Toolkit metadata, conection and session.
    //
    BPSystem.initialize();
    
    //
    // Global variables for referencing the map instance
    //
    var geocoder; 
    var map; 
    var marker;
    
    //
    // Common Function called to retrieve the Address Data in a format that Google will use to render 
//the map // function getAddress() { var country ; var state; var city; var zip; var address = ""; var street; //If in View or "Record" mode, Retrieve the address data from the record in context and
//construct the address string if(BPUI.viewMode == 'R') { var sql = "Select Address1, Address2, City, State, Zip, Country from BILLING_PROFIL
where AccountObj.Id = " +BPSystem.nodeKey; BPConnection.BillingProfile.query(sql).collection().forEach(function(el) { //alert(el.Address1); if(el.Address1) address += el.Address1; if(el.Address2) address += " " + el.Address2; if(el.city) address += ", " + el.City; if(el.state) address += ", " + el.State; if(el.Zip) address += " " + el.Zip; if(el.Country) address += " " + el.Country; }); } else{ //if in New or Edit mode, construct the address string from the form fields using J-Query. var addr1 = $('[name=ADDR1]').val(); var addr2 = $('[name=ADDR2]').val(); country = $('[name=COUNTRY]').val(); state = $('[name=STATE]').val(); city = $('[name=CITY]').val(); zip = $('[name=ZIP]').val(); if (addr1) address = addr1; if (addr2) address = address + " "+addr2; if (city) address = address + ", "+city; if (state) address = address + ", "+state; if (zip) address = address + " "+zip; if (country) address += " " + country; } return address; } // // Call this wherever needed to handle the map display // function codeAddress() { try { //Remove the current Map Marker marker.setMap(null); } catch (e) { //Log errors to the console console.log("Error remove marker",e); } //Load the map passing in the Address from the getAddress() function geocoder.geocode( { 'address': getAddress()}, function(results, status) { if (status == google.maps.GeocoderStatus.OK) { //Got a result, center the map and display map.setCenter(results[0].geometry.location); marker = new google.maps.Marker({ map: map, position: results[0].geometry.location }); } else { console.log("Error geo location",status); } }); } // // Method Called after the Page renders. This will be used to initialize and render the Google Map // BPUI.afterRender = function () { //Standard JQuery function for loading remote scripts $.getScript("https://maps.google.com/maps/api/js?
sensor=true&region=nz&async=2&callback=MapApiLoaded", function () { }); //function for creating the Map objects and Adding Listeners to the standard Address
//Fields in Edit and New mode window.MapApiLoaded = function () { geocoder = new google.maps.Geocoder(); //Default position var coord = {lat:44.5403,lng:-78.5463}; var mapCanvas = document.getElementById('map'); var mapOptions = { center: new google.maps.LatLng(coord.lat, coord.lng), zoom: 8, mapTypeId: google.maps.MapTypeId.ROADMAP }; map = new google.maps.Map(mapCanvas, mapOptions); //if we are not in "Record" or "Read-only" Mode, Add Listeners to each Address field to //call the codeAddtess() function on change if(BPUI.viewMode != 'R') { $('[name=ADDR1]').on('input', function() { codeAddress(); }); $('[name=ADDR2]').on('input', function() { codeAddress(); }); $('[name=ZIP]').on('input', function() { codeAddress(); }); $('[name=CITY]').on('input', function() { codeAddress(); }); $('[name=STATE]').on('input', function() { codeAddress(); }); } else //Call codeAddress to render the map based on a query codeAddress(); } };

The HTML for this project will consist of a simple div used as a target for Google to render the Map. In the HTML tab of the IDE add the following:

<BPUI.Extension>
  <div id="map" style={{"width": "500px",height: "500px"}}></div>
</BPUI.Extension>

 

Adding the Widget to the Account Page

Now that we have our widget developed, we need to add it to the page layout (especially since its 100% dependant on the Account’s Page context). To add the widget to the Account Standard Page go to Setup>Develop>Entities and select the Account Entity. Navigate to the Entity Fields Node and click “New”. Enter a Name and An Appropriate Label.



In this case I want to select the data type of “EXTENSION_WIDGET”. We can now look up the GoogleMap widget we just created. This Widget is Visible (others may not be that simply do some manipulation of data on the page). You have a choice to choose a one or two-column layout. Here we will choose a column span of 1 so that we can display the label and map together as we do with the other address fields. We will specify the height and in this case the “Scrollable” flag is not necessary as the Google map will conform to the space provided.

 

Save the Entity field and Navigate to the Account Node to view the results.

 

Put the account into “Edit” mode, adjust the address values and verify the changes in the map.

General Development Resources

ReactJS

https://facebook.github.io/react/docs/getting-started.html 

 

Debugging Toolkit Code

For debugging you can use your browsers standard development tools to view console output, javascript errors, etc. The screenshot below shows the built-in IDE with the Chrome Developer tools console open below.

 

Toolkit Variable Scope

Window and variable: Each toolkit should have its own execution scope. When using a window (or later referenced as Global Scope) your variables become accessible in any part of page and can be referenced by any widget - not only the current widget.

The main disadvantages of using Global Scope:

  1. Variable clash - For example, if user has 2 widgets (one Customer Address and one Payment Place) with variable addresses. If these widgets are made with a Global Scope variable, a change in the address for one widget will result in a change for the other widget. This can interfere with other javascript variables on the page which are generated by the system.
  2. Variables in Global Scope persist in memory until the user refreshes the page or closes the browser tab. Loading large amounts of data in global scope has the potential to slow the browser significantly or cause the browser to crash.

JavaScript Crash Course:

https://developer.mozilla.org/en-US/Learn/Getting_started_with_the_web/JavaScript_basics

 

jQuery Reference:

http://www.w3schools.com/jquery/default.asp

 

Have more questions? Submit a request

Comments

Powered by Zendesk