From 73517939b48efe50da22bb002b107ae92a29ddf4 Mon Sep 17 00:00:00 2001 From: senator Date: Fri, 12 Feb 2010 16:22:47 +0000 Subject: [PATCH] Acq: Added a working LI search interface. There's more to come on this, but for now you can search LIs by state, by related PO ordering agency, by any one attached attribute value, and even by providing a file of search terms to match some attributes (ISBN in particular). This doesn't have paging yet, and will also need tweaked to enable searching by more than one attribute at a time. git-svn-id: svn://svn.open-ils.org/ILS/trunk@15518 dcc99617-32d9-48b4-a31d-7c20da2025e4 --- Open-ILS/examples/fm_IDL.xml | 7 +- .../perlmods/OpenILS/Application/Acq/Lineitem.pm | 125 ++++++++++++++ Open-ILS/web/css/skin/default/acq.css | 12 ++ Open-ILS/web/js/dojo/openils/XUL.js | 55 +++++- Open-ILS/web/js/dojo/openils/acq/nls/acq.js | 7 +- Open-ILS/web/js/ui/default/acq/lineitem/search.js | 190 +++++++++++++++++++++ Open-ILS/web/js/ui/default/acq/po/li_search.js | 189 -------------------- Open-ILS/web/opac/locale/en-US/lang.dtd | 2 + .../web/templates/default/acq/lineitem/search.tt2 | 83 +++++++++ .../web/templates/default/acq/po/li_search.tt2 | 108 ------------ Open-ILS/web/templates/default/menu.tt2 | 2 +- .../xul/staff_client/chrome/content/main/menu.js | 4 + .../chrome/content/main/menu_frame_menus.xul | 2 + .../chrome/locale/en-US/offline.properties | 1 + 14 files changed, 486 insertions(+), 301 deletions(-) create mode 100644 Open-ILS/web/js/ui/default/acq/lineitem/search.js delete mode 100644 Open-ILS/web/js/ui/default/acq/po/li_search.js create mode 100644 Open-ILS/web/templates/default/acq/lineitem/search.tt2 delete mode 100644 Open-ILS/web/templates/default/acq/po/li_search.tt2 diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index b76e3b4db6..e718794b93 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -5093,7 +5093,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - + @@ -5101,6 +5101,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + + + + diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Acq/Lineitem.pm b/Open-ILS/src/perlmods/OpenILS/Application/Acq/Lineitem.pm index 611ec83489..4dc8cc70c6 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/Acq/Lineitem.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/Acq/Lineitem.pm @@ -257,6 +257,131 @@ sub lineitem_search { __PACKAGE__->register_method( + method => "lineitem_search_by_attributes", + api_name => "open-ils.acq.lineitem.search.by_attributes", + stream => 1, + signature => { + desc => "Performs a search against lineitem_attrs", + params => [ + {desc => "Authentication token", type => "string"}, + { desc => q/ +Search definition: + attr_value_pairs : list of pairs of (attr definition ID, attr value) where value can be scalar (fuzzy match) or array (exact match) + li_states : list of lineitem states + po_agencies : list of purchase order ordering agencies (org) ids + +At least one of these search terms is required. + /, + type => "object"}, + { desc => q/ +Options hash: + idlist : if set, only return lineitem IDs + clear_marc : if set, strip the MARC xml from the lineitem before delivery + flesh_attrs : flesh lineitem attributes; + /, + type => "object"} + ] + } +); + +__PACKAGE__->register_method( + method => "lineitem_search_by_attributes", + api_name => "open-ils.acq.lineitem.search.by_attributes.ident", + stream => 1, + signature => { + desc => "Performs a search against lineitem_attrs where ident is true.". + "See open-ils.acq.lineitem.search.by_attributes for params." + } +); + +sub lineitem_search_by_attributes { + my ($self, $conn, $auth, $search, $options) = @_; + + my $e = new_editor(authtoken => $auth, xact => 1); + return $e->die_event unless $e->checkauth; + # XXX needs permissions consideration + + return [] unless $search; + my $attr_value_pairs = $search->{attr_value_pairs}; + my $li_states = $search->{li_states}; + my $po_agencies = $search->{po_agencies}; # XXX if none, base it on perms + + my $query = { + "select" => {"acqlia" => ["lineitem"]}, + "from" => { + "acqlia" => { + "acqliad" => {"field" => "id", "fkey" => "definition"}, + "jub" => { + "field" => "id", + "fkey" => "lineitem", + "join" => { + "acqpo" => {"field" => "id", "fkey" => "purchase_order"} + } + } + } + } + }; + + my $where = {}; + $where->{"+acqliad"} = {"ident" => "t"} + if $self->api_name =~ /\.ident/; + + my $searched_for_something = 0; + + if (ref $attr_value_pairs eq "ARRAY") { + $where->{"-or"} = []; + foreach (@$attr_value_pairs) { + next if @$_ != 2; + my ($def, $value) = @$_; + push @{$where->{"-or"}}, { + "-and" => { + "attr_value" => (ref $value) ? + $value : {"ilike" => "%" . $value . "%"}, + "definition" => $def + } + }; + } + $searched_for_something = 1; + } + + if ($li_states and @$li_states) { + $where->{"+jub"} = {"state" => $li_states}; + $searched_for_something = 1; + } + + if ($po_agencies and @$po_agencies) { + $where->{"+acqpo"} = {"ordering_agency" => $po_agencies}; + $searched_for_something = 1; + } + + if (not $searched_for_something) { + $e->rollback; + return new OpenILS::Event( + "BAD_PARAMS", note => "You have provided no search terms." + ); + } + + $query->{"where"} = $where; + use Data::Dumper; + $Data::Dumper::Indent = 0; + $logger->info("XXX LFW " . Dumper($query)); + my $lis = $e->json_query($query); + + for my $li_id_obj (@$lis) { + my $li_id = $li_id_obj->{"lineitem"}; + if($options->{"idlist"}) { + $conn->respond($li_id); + } else { + $conn->respond( + retrieve_lineitem($self, $conn, $auth, $li_id, $options) + ); + } + } + undef; +} + + +__PACKAGE__->register_method( method => 'lineitem_search_ident', api_name => 'open-ils.acq.lineitem.search.ident', stream => 1, diff --git a/Open-ILS/web/css/skin/default/acq.css b/Open-ILS/web/css/skin/default/acq.css index 8926980055..9c1c250f74 100644 --- a/Open-ILS/web/css/skin/default/acq.css +++ b/Open-ILS/web/css/skin/default/acq.css @@ -107,6 +107,18 @@ } #oils-acq-metapo-summary td { text-align: right; } +/* li search page */ +h1.oils-acq-li-search { font-size: 150%;font-weight: bold;margin-bottom: 12px; } +h2.oils-acq-li-search { font-size: 138%;font-weight: bold;margin-bottom: 11px; } +h3.oils-acq-li-search { font-size: 125%;font-weight: bold;margin-bottom: 10px; } +h4.oils-acq-li-search { font-size: 112%;font-weight: bold;margin-bottom: 9px; } +#oils-acq-li-search-form-holder {border-bottom: 2px #666 inset; margin: 6px 0;} +.oils-acq-li-search-form-row { margin: 6px 0; } +input.oils-acq-li-search { margin: 0 12px; } +label.oils-acq-li-search { margin: 0 12px; } +span.oils-acq-li-search { margin: 0 12px; } +span#records-up { font-weight: bold; } + #acq-lit-table {width:100%} #acq-lit-table th {padding:5px; font-weight: bold; text-align:left;} #acq-lit-table td {padding:2px;} diff --git a/Open-ILS/web/js/dojo/openils/XUL.js b/Open-ILS/web/js/dojo/openils/XUL.js index c9cde521cb..6a39d1949e 100644 --- a/Open-ILS/web/js/dojo/openils/XUL.js +++ b/Open-ILS/web/js/dojo/openils/XUL.js @@ -49,6 +49,59 @@ if(!dojo._hasResource["openils.XUL"]) { } return true; } -} + /* This class cuts down on the obscenely long incantations needed to + * use XPCOM components. */ + openils.XUL.SimpleXPCOM = function() {}; + openils.XUL.SimpleXPCOM.prototype = { + "FP": { + "iface": Components.interfaces.nsIFilePicker, + "cls": "@mozilla.org/filepicker;1" + }, + "FIS": { + "iface": Components.interfaces.nsIFileInputStream, + "cls": "@mozilla.org/network/file-input-stream;1" + }, + "SIS": { + "iface": Components.interfaces.nsIScriptableInputStream, + "cls": "@mozilla.org/scriptableinputstream;1" + }, + "create": function(key) { + return Components.classes[this[key].cls]. + createInstance(this[key].iface); + }, + "getPrivilegeManager": function() { + return netscape.security.PrivilegeManager; + } + }; + + openils.XUL.contentFromFileOpenDialog = function(windowTitle) { + try { + var api = new openils.XUL.SimpleXPCOM(); + + /* The following enablePrivilege() call must happen at this exact + * level of scope -- not wrapped in another function -- otherwise + * it doesn't work. */ + api.getPrivilegeManager().enablePrivilege("UniversalXPConnect"); + var picker = api.create("FP"); + picker.init( + window, windowTitle || "Upload File", api.FP.iface.modeOpen + ); + if (picker.show() == api.FP.iface.returnOK && picker.file) { + var fis = api.create("FIS"); + var sis = api.create("SIS"); + + fis.init(picker.file, 1 /* MODE_RDONLY */, 0, 0); + sis.init(fis); + + return sis.read(-1); + } else { + return null; + } + } catch(E) { + alert(E); + return null; + } + }; +} diff --git a/Open-ILS/web/js/dojo/openils/acq/nls/acq.js b/Open-ILS/web/js/dojo/openils/acq/nls/acq.js index 86c93a1ad3..9077dfa5a2 100644 --- a/Open-ILS/web/js/dojo/openils/acq/nls/acq.js +++ b/Open-ILS/web/js/dojo/openils/acq/nls/acq.js @@ -9,5 +9,10 @@ 'DFA_NOT_ALL': "Could not record all of your applications of distribution forumulas.", 'APPLY': "Apply", 'RESET_FORMULAE': "Reset Formulas", - 'OUT_OF_COPIES': "You have applied distribution formulas to every copy." + 'OUT_OF_COPIES': "You have applied distribution formulas to every copy.", + 'ONE_LI_ATTR_SEARCH_AT_A_TIME': "You cannot both type in an attribute value search and search for an uploaded file of terms at the same time.", + 'LI_ATTR_SEARCH_CHOOSE_FILE': "Select file with search terms", + 'LI_ATTR_SEARCH_TOO_LARGE': "That file is too large for this operation.", + 'SELECT_AN_LI_ATTRIBUTE': "You must select an LI attribute.", + 'NO_RESULTS': "No results." } diff --git a/Open-ILS/web/js/ui/default/acq/lineitem/search.js b/Open-ILS/web/js/ui/default/acq/lineitem/search.js new file mode 100644 index 0000000000..00f4ba4abd --- /dev/null +++ b/Open-ILS/web/js/ui/default/acq/lineitem/search.js @@ -0,0 +1,190 @@ +dojo.require("dijit.form.Form"); +dojo.require("dijit.form.Button"); +dojo.require("dijit.form.RadioButton"); +dojo.require("dijit.form.TextBox"); +dojo.require("dijit.form.FilteringSelect"); +dojo.require("dojo.data.ItemFileReadStore"); +dojo.require("openils.User"); +dojo.require("openils.Util"); +dojo.require("openils.PermaCrud"); +dojo.require("openils.XUL"); +dojo.require("openils.widget.AutoFieldWidget"); + +var _searchable_by_array = ["issn", "isbn", "upc"]; +var combinedAttrValueArray = []; +var liTable; + +function prepareStateStore(pcrud) { + stateSelector.store = new dojo.data.ItemFileReadStore({ + "data": { + "label": "description", + "identifier": "code", + "items": [ + /* XXX i18n; Also, this list shouldn't be hardcoded here. */ + {"code": "new", "description": "New"}, + {"code": "on-order", "description": "On Order"}, + {"code": "pending-order", "description": "Pending Order"} + ] + } + }); +} + +function prepareScalarSearchStore(pcrud) { + attrScalarDefSelector.store = new dojo.data.ItemFileReadStore({ + "data": acqliad.toStoreData( + pcrud.search("acqliad", {"id": {"!=": null}}) + ) + }); +} + +function prepareArraySearchStore(pcrud) { + attrArrayDefSelector.store = new dojo.data.ItemFileReadStore({ + "data": acqliad.toStoreData( + pcrud.search("acqliad", {"code": _searchable_by_array}) + ) + }); +} + +function prepareAgencySelector() { + new openils.widget.AutoFieldWidget({ + "fmClass": "acqpo", + "fmField": "ordering_agency", + "parentNode": dojo.byId("agency_selector"), + "orgLimitPerms": ["VIEW_PURCHASE_ORDER"], + "dijitArgs": {"name": "agency", "required": false} + }).build(); +} + +function load() { + var pcrud = new openils.PermaCrud(); + + prepareStateStore(pcrud); + prepareScalarSearchStore(pcrud); + prepareArraySearchStore(pcrud); + + prepareAgencySelector(); + + liTable = new AcqLiTable(); + openils.Util.show("oils-acq-li-search-form-holder"); +} + +function toggleAttrSearchType(which, checked) { + /* This would be cooler with a slick dispatch table instead of branchy + * logic, but whatever... */ + if (checked) { + if (which == "scalar") { + openils.Util.show("oils-acq-li-search-attr-scalar", "inline"); + openils.Util.hide("oils-acq-li-search-attr-array"); + } else if (which == "array") { + openils.Util.hide("oils-acq-li-search-attr-scalar"); + openils.Util.show("oils-acq-li-search-attr-array", "inline"); + } else { + openils.Util.hide("oils-acq-li-search-attr-scalar"); + openils.Util.hide("oils-acq-li-search-attr-array"); + } + } +} + +var buildAttrSearchClause = { + "array": function(v) { + if (!v.array_def) { + throw new Error(localeStrings.SELECT_AN_LI_ATTRIBUTE); + } + return { + "attr_value_pairs": + [[Number(v.array_def), combinedAttrValueArray]] /* [[sic]] */ + }; + }, + "scalar": function(v) { + if (!v.scalar_def) { + throw new Error(localeStrings.SELECT_AN_LI_ATTRIBUTE); + } + return { + "attr_value_pairs": + [[Number(v.scalar_def), v.scalar_value]] /* [[sic]] */ + }; + }, + "none": function(v) { + //return {"attr_value_pairs": [[1, ""]]}; + return {}; + } +}; + +function naivelyParse(data) { + return data.split(/[\n, ]/).filter(function(o) {return o.length > 0; }); +} + +function clearTerms() { + combinedAttrValueArray = []; + dojo.byId("records-up").innerHTML = 0; +} + +function loadTermsFromFile() { + var rawdata = openils.XUL.contentFromFileOpenDialog( + localeStrings.LI_ATTR_SEARCH_CHOOSE_FILE + ); + if (!rawdata) { + return; + } else if (rawdata.length > 1024 * 128) { + /* FIXME 128k is completely arbitrary; needs researched for + * a sane limit and should also be made configurable. Further, if + * there's going to be a size limit, it'd be much better to apply + * it before reading in the file at all, not now. */ + alert(localeStrings.LI_ATTR_SEARCH_TOO_LARGE); + } else { + try { + combinedAttrValueArray = + combinedAttrValueArray.concat(naivelyParse(rawdata)); + dojo.byId("records-up").innerHTML = combinedAttrValueArray.length; + } catch (E) { + alert(E); + } + } +} + +function buildSearchClause(values) { + var o = {}; + if (values.state) o.li_states = [values.state]; + if (values.agency) o.po_agencies = [Number(values.agency)]; + return o; +} + +function doSearch(values) { + var results_this_time = 0; + liTable.reset(); + try { + fieldmapper.standardRequest( + ["open-ils.acq", "open-ils.acq.lineitem.search.by_attributes"], { + "params": [ + openils.User.authtoken, + dojo.mixin( + buildAttrSearchClause[values.attr_search_type](values), + buildSearchClause(values) + ), + { + "clear_marc": true, "flesh_attrs": true, + "flesh_notes": true + } + ], + "async": true, + "onresponse": function(r) { + var li = openils.Util.readResponse(r); + if (li) { + results_this_time++; + liTable.addLineitem(li); + liTable.show("list"); + } + }, + "oncomplete": function() { + if (results_this_time < 1) { + alert(localeStrings.NO_RESULTS); + } + } + } + ); + } catch (E) { + alert(E); // XXX + } +} + +openils.Util.addOnLoad(load); diff --git a/Open-ILS/web/js/ui/default/acq/po/li_search.js b/Open-ILS/web/js/ui/default/acq/po/li_search.js deleted file mode 100644 index d099ed0f2b..0000000000 --- a/Open-ILS/web/js/ui/default/acq/po/li_search.js +++ /dev/null @@ -1,189 +0,0 @@ -dojo.require('fieldmapper.Fieldmapper'); -dojo.require('dijit.ProgressBar'); -dojo.require('dijit.form.Form'); -dojo.require('dijit.form.TextBox'); -dojo.require('dijit.form.CheckBox'); -dojo.require('dijit.form.FilteringSelect'); -dojo.require('dijit.form.Button'); -dojo.require("dijit.Dialog"); -dojo.require('openils.Event'); -dojo.require('openils.Util'); -dojo.require('openils.acq.Lineitem'); -dojo.require('openils.acq.Provider'); -dojo.require('openils.acq.PO'); -dojo.require('openils.widget.OrgUnitFilteringSelect'); - -var recvCount = 0; -var createAssetsSelected = false; -var createDebitsSelected = false; - -var lineitems = []; - -function drawForm() { - buildProviderSelect(providerSelector); -} - -function buildProviderSelect(sel, oncomplete) { - openils.acq.Provider.createStore( - function(store) { - sel.store = new dojo.data.ItemFileReadStore({data:store}); - if(oncomplete) - oncomplete(); - }, - 'MANAGE_PROVIDER' - ); -} - -var liReceived; -function doSearch(values) { - var search = {}; - for(var v in values) { - var val = values[v]; - if(val != null && val != '') - search[v] = val; - } - - if(values.state == 'approved') - dojo.style('oils-acq-li-search-po-create', 'visibility', 'visible'); - else - dojo.style('oils-acq-li-search-po-create', 'visibility', 'hidden'); - - //search = [search, {limit:searchLimit, offset:searchOffset}]; - search = [search, {}]; - options = {clear_marc:1, flesh_attrs:1}; - - liReceived = 0; - lineitems = []; - dojo.style('searchProgress', 'visibility', 'visible'); - fieldmapper.standardRequest( - ['open-ils.acq', 'open-ils.acq.lineitem.search'], - { async: true, - params: [openils.User.authtoken, search, options], - onresponse: handleResult, - oncomplete: viewList - } - ); -} - -function handleResult(r) { - var result = r.recv().content(); - searchProgress.update({maximum: searchLimit, progress: ++liReceived}); - lineitems.push(result); -} - -function viewList() { - dojo.style('searchProgress', 'visibility', 'hidden'); - dojo.style('oils-acq-li-search-result-grid', 'visibility', 'visible'); - var store = new dojo.data.ItemFileWriteStore( - {data:jub.toStoreData(lineitems, null, - {virtualFields:['estimated_price', 'actual_price']})}); - var model = new dojox.grid.data.DojoData( - null, store, {rowsPerPage: 20, clientSort: true, query:{id:'*'}}); - JUBGrid.populate(liGrid, model, lineitems); -} - -function createPOFromLineitems(fields) { - var po = new acqpo(); - po.provider(newPOProviderSelector.getValue()); - createAssetsSelected = fields.create_assets; - createDebitsSelected = fields.create_debits; - - if(fields.which == 'selected') { - // find the selected lineitems - var selected = liGrid.selection.getSelected(); - var selList = []; - for(var idx = 0; idx < selected.length; idx++) { - var rowIdx = selected[idx]; - var id = liGrid.model.getRow(rowIdx).id; - for(var i = 0; i < lineitems.length; i++) { - var li = lineitems[i]; - if(li.id() == id && !li.purchase_order() && li.state() == 'approved') - selList.push(lineitems[i]); - } - } - } else { - selList = lineitems; - } - - if(selList.length == 0) return; - - openils.acq.PO.create(po, - function(poId) { - if(e = openils.Event.parse(poId)) - return alert(e); - updateLiList(poId, selList); - } - ); -} - -function updateLiList(poId, selList) { - _updateLiList(poId, selList, 0); -} - -function checkCreateDebits(poId) { - if(!createDebitsSelected) - return viewPO(poId); - fieldmapper.standardRequest( - ['open-ils.acq', 'open-ils.acq.purchase_order.debits.create'], - { async: true, - params: [openils.User.authtoken, poId, {encumbrance:1}], - oncomplete : function(r) { - var total = r.recv().content(); - if(e = openils.Event.parse(total)) - return alert(e); - viewPO(poId); - } - } - ); -} - -function viewPO(poId) { - location.href = 'view/' + poId; -} - -function _updateLiList(poId, selList, idx) { - if(idx >= selList.length) { - if(createAssetsSelected) - return createAssets(poId); - else - return checkCreateDebits(poId); - } - var li = selList[idx]; - li.purchase_order(poId); - li.state('in-process'); - new openils.acq.Lineitem({lineitem:li}).update( - function(stat) { - _updateLiList(poId, selList, ++idx); - } - ); -} - -function createAssets(poId) { - searchProgress.update({progress: 0}); - dojo.style('searchProgress', 'visibility', 'visible'); - - function onresponse(r) { - var stat = r.recv().content(); - if(e = openils.Event.parse(stat)) - return alert(e); - searchProgress.update({maximum: stat.total, progress: stat.progress}); - } - - function oncomplete(r) { - dojo.style('searchProgress', 'visibility', 'hidden'); - checkCreateDebits(poId); - } - - fieldmapper.standardRequest( - ['open-ils.acq','open-ils.acq.purchase_order.assets.create'], - { async: true, - params: [openils.User.authtoken, poId], - onresponse : onresponse, - oncomplete : oncomplete - } - ); -} - - -openils.Util.addOnLoad(drawForm); - diff --git a/Open-ILS/web/opac/locale/en-US/lang.dtd b/Open-ILS/web/opac/locale/en-US/lang.dtd index 64df98f60d..68c0c8b619 100644 --- a/Open-ILS/web/opac/locale/en-US/lang.dtd +++ b/Open-ILS/web/opac/locale/en-US/lang.dtd @@ -769,6 +769,8 @@ + + diff --git a/Open-ILS/web/templates/default/acq/lineitem/search.tt2 b/Open-ILS/web/templates/default/acq/lineitem/search.tt2 new file mode 100644 index 0000000000..1c2739b374 --- /dev/null +++ b/Open-ILS/web/templates/default/acq/lineitem/search.tt2 @@ -0,0 +1,83 @@ +[% WRAPPER 'default/base.tt2' %] +[% ctx.page_title = 'Lineitem Search' %] + + + +[% INCLUDE 'default/acq/common/li_table.tt2' %] +[% END %] diff --git a/Open-ILS/web/templates/default/acq/po/li_search.tt2 b/Open-ILS/web/templates/default/acq/po/li_search.tt2 deleted file mode 100644 index 58af3d3ca5..0000000000 --- a/Open-ILS/web/templates/default/acq/po/li_search.tt2 +++ /dev/null @@ -1,108 +0,0 @@ -[% WRAPPER default/base.tt2 %] - - - -
-
- - - - - - - - - - - - - -
- -
- -
Search
-
-
- -
-
-
- - -
- Create PO -
- - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- -
- -
- -
- -
- -
-
-
- -
- [% grid_jsid = 'liGrid'; domprefix = 'oils-acq-li-search' %] - [% INCLUDE 'default/acq/common/jubgrid.tt2' %] -
- -[% END %] - - diff --git a/Open-ILS/web/templates/default/menu.tt2 b/Open-ILS/web/templates/default/menu.tt2 index 66ca1f2de4..7f21b4c839 100644 --- a/Open-ILS/web/templates/default/menu.tt2 +++ b/Open-ILS/web/templates/default/menu.tt2 @@ -50,7 +50,7 @@ PO Search
+ onClick="location.href = '[% ctx.base_path %]/acq/lineitem/search';"> Lineitem Search