An extension is created to search Sales orders based on PO# and quotes based on memo. The transaction column is enabled in SCA configuration. The extension extends list header js, quote list and purchase list. A template is also being overridden
define(
'SCG.SearchTransaction.ListHeaderCustom', [
'SC.Configuration', 'RecordViews.View', 'OrderHistory.List.Tracking.Number.View', 'Quote.List.View', 'OrderHistory.List.View', 'Overview.Home.View', 'ListHeader.View', 'Quote.Collection', 'OrderHistory.Collection', 'search_transaction_list_header.tpl', 'AjaxRequestsKiller', 'Handlebars', 'Backbone.CollectionView'
],
function(
Configuration, RecordViewsView, OrderHistoryListTrackingNumberView, QuoteListView, OrderHistoryListView, OverviewHomeView, ListHeaderView, QuoteCollection, OrderHistoryCollection, search_transaction_list_header_tpl, AjaxRequestsKiller, Handlebars, BackboneCollectionView
) {
'use strict';
return {
mountToApp: function mountToApp(container) {
// using the 'Layout' component we add a new child view inside the 'Header' existing view
// (there will be a DOM element with the HTML attribute data-view="Header.Logo")
// more documentation of the Extensibility API in
// https://system.netsuite.com/help/helpcenter/en_US/APIs/SuiteCommerce/Extensibility/Frontend/index.html
/** @type {LayoutComponent} */
var layout = container.getComponent('Layout');
// if(layout)
// {
// layout.addChildView('Header.Logo', function() {
// return new ListHeaderCustomView({ container: container });
// });
// }
_.extend(QuoteListView.prototype, {
setupListHeader: _.wrap(QuoteListView.setupListHeader, function(fn) { //EXTENDED TO SHOW SEARCH FIELD IN QUOTE LIST PAGE
// manges sorting and filtering of the collection
this.listHeader = new ListHeaderView({
view: this,
application: this.application,
collection: this.collection,
filters: this.filterOptions,
sorts: this.sortOptions,
allowEmptyBoundaries: true,
showSearch: true, //TO SHOW SEARCH FIELD
searchField: 'Memo'
});
})
});
_.extend(OrderHistoryListView.prototype, {
initialize: _.wrap(OrderHistoryListView.prototype.initialize, function wrappedInitialize(fn, options) {
fn.apply(this, _.toArray(arguments).slice(1));
// Manages sorting and filtering of the collection
this.listHeader = new ListHeaderView({
view: this,
application: this.application,
collection: this.collection,
sorts: this.sortOptions,
rangeFilter: 'date',
rangeFilterLabel: _('From').translate(),
hidePagination: true,
allowEmptyBoundaries: true,
showSearch: true, //TO SHOW SEARCH FIELD
searchField: 'PO#'
});
})
});
_.extend(ListHeaderView.prototype.events, {
'submit .list-header-search-form': 'searchHandler'
});
_.extend(ListHeaderView.prototype, {
template: search_transaction_list_header_tpl,
initialize: _.wrap(ListHeaderView.prototype.initialize, function wrappedInitialize(fn, options) { //GETS SEARCH TEXT
fn.apply(this, _.toArray(arguments).slice(1));
this.searchFilter = options.showSearch;
this.searchField = options.searchField; //PO or Memo
}),
getContext: _.wrap(ListHeaderView.prototype.getContext, function wrappedInitialize(fn, options) {
var res = fn.apply(this, _.toArray(arguments).slice(1));
//console.log("this", this);
_.extend(res, {
showHeader: res.showHeader || this.search !== '', //ADD SEARCH TEXT TO FIELD EVEN AFTER SUBMIT
searchFilter: this.searchFilter,
search: this.search,
searchField: this.searchField
});
return res;
}),
searchHandler: function(e) {
e.preventDefault();
var $form = jQuery(e.currentTarget);
// unselect all elements
this.unselectAll({
silent: true
});
// sets the selected filter
this.search = $form.find('.list-header-view-accordion-body-input-search').val();
// updates the url and the collection
this.updateUrl();
},
updateUrl: function() {
var url = Backbone.history.fragment;
this.searchParam = (this.search || '').replace(/ /g, '!').replace(/\t/g, ''); //REPLACE SPACE AND TABS SO THAT SEARCH WILL BE DONE IN ONE GO
url = !this.search ?
_.removeUrlParameter(url, 'search') :
_.setUrlParameter(url, 'search', this.searchParam); //UPDATE URL IF HAVE SEARCH TEXT
// if the selected filter is the default one
// remove the filter parameter
// else change it for the selected value
url = this.isDefaultFilter(this.selectedFilter) ?
_.removeUrlParameter(url, 'filter') :
_.setUrlParameter(url, 'filter', _.isFunction(this.selectedFilter.value) ? this.selectedFilter.value.apply(this.view) : this.selectedFilter.value);
// if the selected sort is the default one
// remove the sort parameter
// else change it for the selected value
url = this.isDefaultSort(this.selectedSort) ? _.removeUrlParameter(url, 'sort') : _.setUrlParameter(url, 'sort', this.selectedSort.value);
// if the selected order is the default one
// remove the order parameter
// else change it for the selected value
url = this.order === 1 ? _.removeUrlParameter(url, 'order') : _.setUrlParameter(url, 'order', 'inverse');
// if range from and range to are set up
// change them in the url
// else remove the parameter
if (this.selectedRange) {
url = this.selectedRange.from || this.selectedRange.to ? _.setUrlParameter(url, 'range', (this.selectedRange.from || '') + 'to' + (this.selectedRange.to || '')) : _.removeUrlParameter(url, 'range');
}
url = _.removeUrlParameter(url, 'page');
this.page = 1;
// just go there already, but warn no one
Backbone.history.navigate(url, { trigger: false });
return this.updateCollection();
},
updateCollection: function() {
var range = null;
var collection = this.collection;
if (this.selectedRange) {
//@class RangeFilter
//If there is no date selected i keep the range empty in order to get "transactions" dated in the future
range = {
//@property {String} from
from: this.selectedRange.from || (this.allowEmptyBoundaries ? '' : this.rangeFilterOptions.fromMin),
//@property {String} to
to: this.selectedRange.to || (this.allowEmptyBoundaries ? '' : this.rangeFilterOptions.toMax)
};
}
//@lass Collection.Filter
collection.update && collection.update({
searchText: this.searchParam
//@property {value:String} filter
,
filter: this.selectedFilter
//@property {RangeFilter} range
,
range: range
//@property {value:String} sort
,
sort: this.selectedSort
//@property {String} order
,
order: this.order,
page: this.page,
status: this.view.status
//@property {Number} killerId
,
killerId: AjaxRequestsKiller.getKillerId()
}, this);
//@class ListHeader.View
return this;
},
});
_.extend(QuoteCollection.prototype, {
update: function update(options) {
this.searchText = options.searchText;
var range = options.range || {},
from = range.from && _.stringToDate(range.from),
to = range.to && _.stringToDate(range.to);
this.fetch({
data: {
filter: options.filter.value,
sort: options.sort.value,
order: options.order,
from: from || null,
to: to || null,
page: options.page,
searchText: this.searchText
},
reset: true,
killerId: options.killerId
});
},
parse: function parse(response) {
this.totalRecordsFound = response.totalRecordsFound;
this.recordsPerPage = response.recordsPerPage;
this.isSearch = response.isSearch;
//console.log('search',this.isSearch);
return response.records;
}
});
_.extend(OrderHistoryCollection.prototype, {
update: function(options) {
var range = options.range || {};
this.searchText = options.searchText;
var data = {
filter: this.customFilters || options.filter && options.filter.value,
sort: options.sort.value,
order: options.order,
from: range.from,
to: range.to,
page: options.page,
searchText: options.searchText
};
this.fetch({
data: data,
reset: true,
killerId: options.killerId
});
//console.log('this',this);
},
parse: function(response) {
var self = this;
this.totalRecordsFound = response.totalRecordsFound;
this.recordsPerPage = response.recordsPerPage;
this.isSearch = response.isSearch;
var records = response.records;
// this.searchText = (this.searchText || '').trim();
// if (this.searchText != undefined && this.searchText != '' && this.searchText != null) {
// records = _.filter(records, function(eachRec) {
// if (eachRec.otherrefnum) {
// var otherrefnum = eachRec.otherrefnum.toLowerCase().replace(/ /g, '!');
// var key = self.searchText.toLowerCase();
// if (otherrefnum.indexOf(key) >= 0) {
// return eachRec;
// }
// }
// });
// }
return records;
}
});
_.extend(OverviewHomeView.prototype.childViews, { //TO ADD MEMO FIELD IN RECENT PURCHASE
'Order.History.Results': function() {
var self = this,
records_collection = new Backbone.Collection(this.collection.map(function(order) {
var dynamic_column;
if (self.isSCISIntegrationEnabled) {
dynamic_column = {
label: _('Origin:').translate(),
type: 'origin',
name: 'origin',
value: _.findWhere(Configuration.get('transactionRecordOriginMapping'), { origin: order.get('origin') }).name
};
} else {
dynamic_column = {
label: _('Status:').translate(),
type: 'status',
name: 'status',
value: order.get('status').name
};
}
var columns = [{
label: _('Date:').translate(),
type: 'date',
name: 'date',
value: order.get('trandate')
}, {
label: _('Amount:').translate(),
type: 'currency',
name: 'amount',
value: order.get('amount_formatted')
},
{
label: _('PO Number:').translate(),
type: 'po',
name: 'po',
value: order.get('otherrefnum')
},
{
type: 'tracking-number',
name: 'trackingNumber',
compositeKey: 'OrderHistoryListTrackingNumberView',
composite: new OrderHistoryListTrackingNumberView({
model: new Backbone.Model({
trackingNumbers: order.get('trackingnumbers')
}),
showContentOnEmpty: true,
contentClass: '',
collapseElements: true
})
}
];
columns.splice(2, 0, dynamic_column);
var model = new Backbone.Model({
title: new Handlebars.SafeString(_('<span class="tranid">$(0)</span>').translate(order.get('tranid'))),
touchpoint: 'customercenter',
detailsURL: '/purchases/view/' + order.get('recordtype') + '/' + order.get('internalid'),
recordType: order.get('recordtype'),
id: order.get('internalid'),
internalid: order.get('internalid'),
trackingNumbers: order.get('trackingnumbers'),
columns: columns
});
return model;
}));
return new BackboneCollectionView({
childView: RecordViewsView,
collection: records_collection,
viewsPerRow: 1
});
}
});
}
}
})
SS
define('SCG.Transaction.Search', [
'SC.Model', 'Application', 'Models.Init', 'Utils', 'ServiceController', 'OrderHistory.Model', 'Quote.Model'
], function(
SCModel, Application, ModelsInit, Utils, ServiceController, OrderHistoryModel, QuoteModel
) {
'use strict';
_.extend(OrderHistoryModel, {
setExtraListFilters: function() {
var searchText = this.data.searchText;
console.log('searchText',searchText)
if (searchText && searchText !== '') {
searchText=searchText.replace(/!/g, ' ')
console.log('searchText2',searchText)
this.filters.searchtext_operator = 'and';
this.filters.searchtext =['otherrefnum', 'equalto', searchText];
}
if (this.data.filter === 'status:open') // Status is open and this is only valid for Sales Orders.
{
this.filters.type_operator = 'and';
this.filters.type = ['type', 'anyof', ['SalesOrd']];
this.filters.status_operator = 'and';
this.filters.status = ['status', 'anyof', ['SalesOrd:A', 'SalesOrd:B', 'SalesOrd:D', 'SalesOrd:E', 'SalesOrd:F']];
} else if (this.isSCISIntegrationEnabled) {
if (this.data.filter === 'origin:instore') // SCIS Integration enabled, only In Store records (Including Sales Orders from SCIS)
{
this.filters.scisrecords_operator = 'and';
this.filters.scisrecords = [
['type', 'anyof', ['CashSale', 'CustInvc', 'SalesOrd']], 'and', ['createdfrom.type', 'noneof', ['SalesOrd']], 'and', ['location.locationtype', 'is', Configuration.get('locationTypeMapping.store.internalid')], 'and', ['source', 'is', '@NONE@']
];
} else // SCIS Integration enabled, all records
{
this.filters.scisrecords_operator = 'and';
this.filters.scisrecords = [
[
['type', 'anyof', ['CashSale', 'CustInvc']], 'and', ['createdfrom.type', 'noneof', ['SalesOrd']], 'and', ['location.locationtype', 'is', Configuration.get('locationTypeMapping.store.internalid')], 'and', ['source', 'is', '@NONE@']
], 'or', [
['type', 'anyof', ['SalesOrd']]
]
];
}
} else // SCIS Integration is disabled, show all the Sales Orders
{
this.filters.type_operator = 'and';
this.filters.type = ['type', 'anyof', ['SalesOrd']];
}
var is_contact = ModelsInit.session.getCustomer().getFieldValues().contactloginid !== '0';
if (!_.isEmpty(ModelsInit.session.getSiteSettings().cartsharingmode) && ModelsInit.session.getSiteSettings().cartsharingmode === 'PER_CONTACT' && is_contact) {
var contactId = parseInt(ModelsInit.session.getCustomer().getFieldValues().contactloginid),
email = nlapiLookupField('contact', contactId, 'email');
this.filters.email_opeartor = 'and';
this.filters.email = ['email', 'is', email];
}
}
});
_.extend(QuoteModel, {
setExtraListFilters: function() {
var searchText = this.data.searchText;
console.log('searchText',searchText)
if (searchText && searchText !== '') {
searchText=searchText.replace(/!/g, ' ')
console.log('searchText2',searchText)
this.filters.searchtext_operator = 'and';
this.filters.searchtext = ['memo', 'contains', searchText];
}
if (this.data.filter && this.data.filter !== 'ALL') {
this.filters.entitystatus_operator = 'and';
this.filters.entitystatus = ['entitystatus', 'is', this.data.filter];
}
}
});
});
Service Controller
define('SCG.Transaction.Search.Services', [
'SC.Model', 'Application', 'Models.Init', 'OrderHistory.ServiceController', 'Quote.ServiceController', 'Utils', 'ServiceController', 'OrderHistory.Model', 'Quote.Model', 'SCG.Transaction.Search'
], function(
SCModel, Application, ModelsInit, OrderHistoryServiceController, QuoteServiceController, Utils, ServiceController, OrderHistoryModel,QuoteModel, SCGTransactionSearch
) {
'use strict';
_.extend(OrderHistoryServiceController, {
get: function() {
var recordtype = this.request.getParameter('recordtype'),
id = this.request.getParameter('internalid');
console.log('Search',this.request.getParameter('searchText'))
//If the id exist, sends the response of Order.get(id), else sends the response of (Order.list(options) || [])
if (recordtype && id) {
return OrderHistoryModel.get(recordtype, id);
} else {
return OrderHistoryModel.list({
searchText: this.request.getParameter('searchText'),
filter: this.request.getParameter('filter'),
order: this.request.getParameter('order'),
sort: this.request.getParameter('sort'),
from: this.request.getParameter('from'),
to: this.request.getParameter('to'),
origin: this.request.getParameter('origin'),
page: this.request.getParameter('page') || 1,
results_per_page: this.request.getParameter('results_per_page')
});
}
}
});
_.extend(QuoteServiceController, {
get: function() {
var id = this.request.getParameter('internalid');
if (id) {
return QuoteModel.get('estimate', id);
} else {
console.log('Search',this.request.getParameter('searchText'))
return QuoteModel.list({
filter: this.request.getParameter('filter'),
order: this.request.getParameter('order'),
sort: this.request.getParameter('sort'),
from: this.request.getParameter('from'),
to: this.request.getParameter('to'),
page: this.request.getParameter('page') || 1,
types: this.request.getParameter('types'),
searchText: this.request.getParameter('searchText')
});
}
}
});
});
//Mainfest
{
"name": "SearchTransaction",
"fantasyName": "Search Transaction Using Memo",
"vendor": "SCG",
"version": "1.0.0",
"type": "extension",
"target": "SCA,SCS",
"description": "Search Quotes using Memo field",
"skins": [],
"assets": {
"img": {
"files": []
},
"fonts": {
"files": []
},
"services": {
"files": []
}
},
"templates": {
"application": {
"myaccount": {
"files": [
"Modules/ListHeaderCustom/Templates/search_transaction_list_header.tpl"
]
}
}
},
"javascript": {
"entry_points": {
"myaccount": "Modules/ListHeaderCustom/JavaScript/SCG.SearchTransaction.ListHeaderCustom.js"
},
"application": {
"myaccount": {
"files": [
"Modules/ListHeaderCustom/JavaScript/SCG.SearchTransaction.ListHeaderCustom.js",
"Modules/ListHeaderCustom/JavaScript/ListHeaderCustom.View.js"
]
}
}
},
"ssp-libraries": {
"entry_point": "Modules/ListHeaderCustom/SuiteScript/SCG.Transaction.Search.Services.js",
"files": [
"Modules/ListHeaderCustom/SuiteScript/SCG.Transaction.Search.js",
"Modules/ListHeaderCustom/SuiteScript/SCG.Transaction.Search.Services.js"
]
},
"local_folder": "Workspace\\SearchTransaction"
}
Template
{{#if showHeader}}
<div class="list-header-view" data-type="accordion">
<div class="list-header-view-accordion" data-action="accordion-header">
<div class="list-header-view-accordion-link">{{headerMarkup}}</div>
{{#if showHeaderExpandable}}
<div class="list-header-view-accordion-header">
<button class="list-header-view-filter-button" data-action="toggle-filters">
{{translate 'Filter'}} <i class="list-header-view-filter-button-icon" ></i>
</button>
</div>
<div class="list-header-view-accordion-body {{initiallyCollapsed}}" data-type="accordion-body" {{{accordionStyle}}}>
<div class="list-header-view-accordion-body-header {{classes}}">
{{#if rangeFilter}}
<div class="list-header-view-datepicker-from">
<label class="list-header-view-from" for="from">{{rangeFilterLabel}}</label>
<div class="list-header-view-datepicker-container-input">
<input class="list-header-view-accordion-body-input" id="from" name="from" type="date" autocomplete="off" data-format="yyyy-mm-dd" data-start-date="{{rangeFilterFromMin}}" data-end-date="{{rangeFilterFromMax}}" value="{{selectedRangeFrom}}" data-action="range-filter" data-todayhighlight="true"/>
<i class="list-header-view-accordion-body-calendar-icon"></i>
<a class="list-header-view-accordion-body-clear" data-action="clear-value">
<i class="list-header-view-accordion-body-clear-icon"></i>
</a>
</div>
</div>
<div class="list-header-view-datepicker-to">
<label class="list-header-view-to" for="to">{{translate 'to'}}</label>
<div class="list-header-view-datepicker-container-input">
<input class="list-header-view-accordion-body-input" id="to" name="to" type="date" autocomplete="off" data-format="yyyy-mm-dd" data-start-date="{{rangeFilterToMin}}" data-end-date="{{rangeFilterToMax}}" value="{{selectedRangeTo}}" data-action="range-filter" data-todayhighlight="true"/>
<i class="list-header-view-accordion-body-calendar-icon"></i>
<a class="list-header-view-accordion-body-clear" data-action="clear-value">
<i class="list-header-view-accordion-body-clear-icon"></i>
</a>
</div>
</div>
{{/if}}
{{#if searchFilter}}
<div class="list-header-view-test">
<form action="" class="list-header-search-form">
<input class="list-header-view-accordion-body-input-search" data-action="text-filter" name="text-filter" type="text" autocomplete="off" value="{{search}}" placeholder="Search by {{searchField}}" />
<button type="submit" style="color: white;">
<i class="site-search-button-icon"></i>
</button>
</form>
</div>
{{/if}}
{{#if sorts}}
<span class="list-header-view-sorts">
<label class="list-header-view-filters">
<select name="sort" class="list-header-view-accordion-body-select" data-action="sort">
{{#each sorts}}
<option value="{{value}}" data-permissions="{{permission}}" {{#if selected}} selected {{/if}}>{{name}}</option>
{{/each}}
</select>
</label>
<button class="list-header-view-accordion-body-button-sort" data-action="toggle-sort">
<i class="list-header-view-accordion-body-button-sort-up {{sortIconUp}}"></i>
<i class="list-header-view-accordion-body-button-sort-down {{sortIconDown}}"></i>
</button>
</span>
{{/if}}
{{#if filters}}
<label class="list-header-view-filters">
<select name="filter" class="list-header-view-accordion-body-select" data-action="filter">
{{#each filters}}
<option value="{{itemValue}}" class="{{cssClassName}}" data-permissions="{{permission}}" {{#if selected}} selected {{/if}}>{{name}}</option>
{{/each}}
</select>
</label>
{{/if}}
</div>
</div>
{{/if}}
</div>
</div>
{{/if}}
{{#if showSelectAll}}
<div class="list-header-view-select-all">
<label class="list-header-view-select-all-label" for="select-all">
{{#if unselectedLength}}
<input type="checkbox" name="select-all" id="select-all" data-action="select-all">{{translate 'Select All ($(0))' collectionLength}}
{{else}}
<input type="checkbox" name="select-all" id="select-all" data-action="unselect-all" checked>{{translate 'Unselect All ($(0))' collectionLength}}
{{/if}}
</label>
</div>
{{/if}}
{{#if showPagination}}
<div class="list-header-view-paginator">
<div data-view="GlobalViews.Pagination"></div>
{{#if showCurrentPage}}
<div data-view="GlobalViews.ShowCurrentPage"></div>
{{/if}}
</div>
{{/if}}
{{!----
Use the following context variables when customizing this template:
showHeader (Array)
classes (String)
showHeaderExpandable (Boolean)
rangeFilterLabel (String)
rangeFilterFromMin (String)
rangeFilterFromMax (String)
rangeFilterToMin (String)
rangeFilterToMax (String)
filters (Array)
sorts (Array)
sortIconUp (String)
sortIconDown (String)
showSelectAll (Boolean)
unselectedLength (Number)
collectionLength (Number)
showPagination (Boolean)
showCurrentPage (Boolean)
initiallyCollapsed (String)
headerMarkup (Object)
headerMarkup.string (String)
rangeFilter (String)
----}}