From 73517939b48efe50da22bb002b107ae92a29ddf4 Mon Sep 17 00:00:00 2001
From: senator <senator@dcc99617-32d9-48b4-a31d-7c20da2025e4>
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
 		</links>
 	</class>
 
-	<class id="acqliad" controller="open-ils.cstore open-ils.reporter-store" oils_obj:fieldmapper="acq::lineitem_attr_definition" oils_persist:tablename="acq.lineitem_attr_definition" reporter:label="Line Item Attribute Definition">
+	<class id="acqliad" controller="open-ils.cstore open-ils.reporter-store open-ils.pcrud" oils_obj:fieldmapper="acq::lineitem_attr_definition" oils_persist:tablename="acq.lineitem_attr_definition" reporter:label="Line Item Attribute Definition">
 		<fields oils_persist:primary="id" oils_persist:sequence="acq.lineitem_attr_definition_id_seq">
 			<field reporter:label="Definition ID" name="id" reporter:datatype="id" />
 			<field reporter:label="Code" name="code" reporter:datatype="text" />
@@ -5101,6 +5101,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 			<field reporter:label="Is Identifier?" name="ident" reporter:datatype="bool"/>
 		</fields>
 		<links/>
+		<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+			<actions>
+				<retrieve />
+			</actions>
+		</permacrud>
 	</class>
 
 	<class id="acqlimad" controller="open-ils.cstore open-ils.reporter-store" oils_obj:fieldmapper="acq::lineitem_marc_attr_definition" oils_persist:tablename="acq.lineitem_marc_attr_definition" reporter:label="Line Item MARC Attribute Definition">
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 @@
 <!ENTITY staff.main.menu.acq.picklist.accesskey "L">
 <!ENTITY staff.main.menu.acq.bib_search.label "Title Search">
 <!ENTITY staff.main.menu.acq.bib_search.accesskey "T">
+<!ENTITY staff.main.menu.acq.li_search.label "Lineitem Search">
+<!ENTITY staff.main.menu.acq.li_search.accesskey "I">
 <!ENTITY staff.main.menu.acq.brief_record.label "New Brief Record">
 <!ENTITY staff.main.menu.acq.brief_record.accesskey "B">
 <!ENTITY staff.main.menu.acq.upload.label "Load Order Record">
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' %]
+<script src="[% ctx.media_prefix %]/js/ui/default/acq/lineitem/search.js"></script>
+<!-- later: "[% ctx.page_args.0 %]" -->
+<div id="oils-acq-li-search-form-holder" class="hidden">
+    <h1 class="oils-acq-li-search">Lineitem Search</h1>
+    <form dojoType="dijit.form.Form" action="" method=""
+        id="oils-acq-li-search-form" jsId="searchForm">
+        <script type="dojo/method" event="onSubmit">
+            doSearch(this.getValues());
+            return false; /* no redirect */
+        </script>
+
+        <div class="oils-acq-li-search-form-row">
+            <label class="oils-acq-li-search" for="state_selector">
+                Lineitem state
+            </label>
+            <input class="oils-acq-li-search" name="state"
+                dojoType="dijit.form.FilteringSelect" required="false"
+                id="state_selector" jsId="stateSelector"
+                labelAttr="description" searchAttr="description"
+                />
+            <label class="oils-acq-li-search" for="agency_selector">
+                PO ordering agency
+            </label>
+            <input class="oils-acq-li-search" id="agency_selector" />
+        </div>
+        <div class="oils-acq-li-search-form-row">
+            <input class="oils-acq-li-search" dojoType="dijit.form.RadioButton"
+                name="attr_search_type" jsId="attrSearchTypeNone"
+                id="attr_search_type_none" value="none" checked="checked"
+                onChange="toggleAttrSearchType(this.value, this.checked);" />
+            <label for="attr_search_type_none" class="oils-acq-li-search">
+                No further attributes to search by
+            </label>
+        </div>
+        <div class="oils-acq-li-search-form-row">
+            <input class="oils-acq-li-search" dojoType="dijit.form.RadioButton"
+                name="attr_search_type" jsId="attrSearchTypeScalar"
+                id="attr_search_type_scalar" value="scalar"
+                onChange="toggleAttrSearchType(this.value, this.checked);" />
+            <label for="attr_search_type_scalar" class="oils-acq-li-search">
+                Search by one attribute value
+            </label>
+            <div id="oils-acq-li-search-attr-scalar" class="hidden">
+                <input class="oils-acq-li-search"
+                    name="scalar_def" dojoType="dijit.form.FilteringSelect"
+                    jsId="attrScalarDefSelector"
+                    labelAttr="description" searchAttr="description" />
+                <input class="oils-acq-li-search" name="scalar_value"
+                    dojoType="dijit.form.TextBox"/>
+            </div>
+        </div>
+        <div class="oils-acq-li-search-form-row">
+            <input class="oils-acq-li-search" dojoType="dijit.form.RadioButton"
+                name="attr_search_type" jsId="attrSearchTypeArray"
+                id="attr_search_type_array" value="array"
+                onChange="toggleAttrSearchType(this.value, this.checked);" />
+            <label for="attr_search_type_array" class="oils-acq-li-search">
+                Provide a file of search terms
+            </label>
+            <div id="oils-acq-li-search-attr-array" class="hidden">
+                <input class="oils-acq-li-search"
+                    name="array_def" dojoType="dijit.form.FilteringSelect"
+                    jsId="attrArrayDefSelector"
+                    labelAttr="description" searchAttr="description" />
+                <span class="oils-acq-li-search">
+                    <span id="records-up">0</span> term(s) prepared for search
+                </span>
+                <span class="oils-acq-li-search" dojoType="dijit.form.Button"
+                    onClick="loadTermsFromFile();">Add file</span>
+                <span class="oils-acq-li-search" dojoType="dijit.form.Button"
+                    onClick="clearTerms();">Clear loaded search terms</span>
+            </div>
+        </div>
+        <div class="oils-acq-li-search-form-row"
+            id="oils-acq-li-search-attr-submit">
+            <span dojoType="dijit.form.Button" type="submit">Search</span>
+        </div>
+    </form>
+</div>
+[% 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 %]
-    <script src='[% ctx.media_prefix %]/js/ui/default/acq/po/li_search.js'> </script>
-    <script>
-        var searchLimit = 20;
-        var searchOffset = 0;
-    </script>
-
-    <div id='oils-acq-li-search-block' class='container'>
-        <form dojoType='dijit.form.Form' action='' method=''>
-            <script type="dojo/method" event="onSubmit">
-                doSearch(this.getValues());
-                return false; /* don't redirect */
-            </script>
-            <table class='oils-acq-basic-form-table'>
-                <tr>
-                    <td><label for='state'>State</label></td>
-                    <td>
-                        <select dojoType='dijit.form.FilteringSelect' name='state'>
-                            <option value='new'>New</option>
-                            <option value='approved'>Approved</option>
-                            <option value='in-process'>In Process</option>
-                            <option value='received'>Received</option>
-                        </select>
-                    </td>
-                </tr>
-                <tr>
-                    <td><label for='provider'>Provider</label></td>
-                    <td>
-                        <select dojoType='dijit.form.FilteringSelect' name='provider' 
-                            labalAttr='code' searchAttr='code' jsId='providerSelector'>
-                        </select>
-                    </td>
-                </tr>
-                <tr>
-                    <td colspan='2'><div dojoType='dijit.form.Button' type='submit'>Search</div></td>
-                </tr>
-            </table>
-        </form>
-    </div>
-
-    <div id='oils-acq-li-search-progress'>
-        <div dojoType="dijit.ProgressBar" style="width:300px" jsId="searchProgress" id="searchProgress"></div>
-    </div>
-    <script>dojo.style('searchProgress', 'visibility', 'hidden');</script>
-
-    <div dojoType="dijit.form.DropDownButton" id='oils-acq-li-search-po-create'>
-        <span>Create PO</span>
-        <div dojoType="dijit.TooltipDialog" execute="createPOFromLineitems(arguments[0]);">
-            <script type='dojo/connect' event='onOpen'>
-                buildProviderSelect(newPOProviderSelector, 
-                    function() {
-                        newPOProviderSelector.setValue(providerSelector.getValue());
-                    }
-                );
-                new openils.User().buildPermOrgSelector('CREATE_PURCHASE_ORDER', orderingAgencySelect);
-            </script>
-            <table class='dijitTooltipTable'>
-                <tr>
-                    <td colspan='2'>
-                        <input dojoType="dijit.form.RadioButton" name="which" type='radio' checked='checked' value='selected'/>
-                        <label for="name">For selected items</label>
-                        <input dojoType="dijit.form.RadioButton" name="which" type='radio' value='all'/>
-                        <label for="name">For all items</label>
-                    </td>
-                </tr>
-                <tr>
-                    <td><label for="name">Provider: </label></td>
-                    <td>
-                        <input jsId='newPOProviderSelector' name="provider" 
-                            dojoType="dijit.form.FilteringSelect" searchAttr='code' labelAttr='code'/>
-                    </td>
-                </tr>
-                <tr>
-                    <td><label for="name">Ordering Agency:</label></td>
-                    <td>
-                        <input dojoType="openils.widget.OrgUnitFilteringSelect" jsId='orderingAgencySelect'
-                            searchAttr="shortname" name="ordering_agency" autocomplete="true" labelAttr='shortname'> </input>
-                    </td>
-                </tr>
-                <tr>
-                    <td><label for="create_assets">Generate Bib/Copy Data</label></td>
-                    <td>
-                        <input name='create_assets' dojoType='dijit.form.CheckBox' checked='checked'> </input>
-                    </td>
-                </tr>
-                <tr>
-                    <td><label for="create_debits">Encumber funds</label></td>
-                    <td>
-                        <input name='create_debits' dojoType='dijit.form.CheckBox' checked='checked'> </input>
-                    </td>
-                </tr>
-                <tr>
-                    <td colspan='2' align='center'>
-                        <button dojoType='dijit.form.Button' type="submit">Create</button>
-                    </td>
-                </tr>
-            </table>
-        </div>
-    </div> 
-    <script>dojo.style('oils-acq-li-search-po-create', 'visibility', 'hidden');</script>
-    <div id='oils-acq-li-search-result-grid' style='height:100%'>
-        [% grid_jsid = 'liGrid'; domprefix = 'oils-acq-li-search' %]
-        [% INCLUDE 'default/acq/common/jubgrid.tt2' %]
-    </div>
-    <script>dojo.style('oils-acq-li-search-result-grid', 'visibility', 'hidden');</script>
-[% 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
                         </div>
                         <div dojoType="dijit.MenuItem" iconClass="dijitEditorIcon dijitEditorIconCopy"
-                            onClick="location.href = '[% ctx.base_path %]/acq/po/li_search';">
+                            onClick="location.href = '[% ctx.base_path %]/acq/lineitem/search';">
                             Lineitem Search
                         </div>
                         <!-- XXX 
diff --git a/Open-ILS/xul/staff_client/chrome/content/main/menu.js b/Open-ILS/xul/staff_client/chrome/content/main/menu.js
index 9085913fca..4ca01dda2d 100644
--- a/Open-ILS/xul/staff_client/chrome/content/main/menu.js
+++ b/Open-ILS/xul/staff_client/chrome/content/main/menu.js
@@ -671,6 +671,10 @@ main.menu.prototype = {
                 ['oncommand'],
                 function() { open_eg_web_page('acq/picklist/bib_search', 'menu.cmd_acq_bib_search.tab'); }
             ],
+            'cmd_acq_li_search' : [
+                ['oncommand'],
+                function() { open_eg_web_page('acq/lineitem/search', 'menu.cmd_acq_li_search.tab'); }
+            ],
             'cmd_acq_new_brief_record' : [
                 ['oncommand'],
                 function() { open_eg_web_page('acq/picklist/brief_record', 'menu.cmd_acq_new_brief_record.tab'); }
diff --git a/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul b/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
index 49ad231d81..54bdf79962 100644
--- a/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
+++ b/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
@@ -80,6 +80,7 @@
     <command id="cmd_acq_upload" />
     <command id="cmd_acq_view_po" />
     <command id="cmd_acq_bib_search" />
+    <command id="cmd_acq_li_search" />
     <command id="cmd_acq_new_brief_record" />
     <command id="cmd_acq_view_fund" />
     <command id="cmd_acq_view_funding_source" />
@@ -249,6 +250,7 @@
     <menupopup id="main.menu.acq.popup">
         <menuitem label="&staff.main.menu.acq.picklist.label;" accesskey="&staff.main.menu.acq.picklist.accesskey;" command="cmd_acq_view_picklist"/>
         <menuitem label="&staff.main.menu.acq.bib_search.label;" accesskey="&staff.main.menu.acq.bib_search.accesskey;" command="cmd_acq_bib_search"/>
+        <menuitem label="&staff.main.menu.acq.li_search.label;" accesskey="&staff.main.menu.acq.li_search.accesskey;" command="cmd_acq_li_search"/>
         <menuitem label="&staff.main.menu.acq.upload.label;" accesskey="&staff.main.menu.acq.upload.accesskey;" command="cmd_acq_upload"/>
         <menuitem label="&staff.main.menu.acq.brief_record.label;" accesskey="&staff.main.menu.acq.brief_record.accesskey;" command="cmd_acq_new_brief_record"/>
         <menuseparator />
diff --git a/Open-ILS/xul/staff_client/chrome/locale/en-US/offline.properties b/Open-ILS/xul/staff_client/chrome/locale/en-US/offline.properties
index 42df113f93..4a573e1a15 100644
--- a/Open-ILS/xul/staff_client/chrome/locale/en-US/offline.properties
+++ b/Open-ILS/xul/staff_client/chrome/locale/en-US/offline.properties
@@ -222,6 +222,7 @@ menu.cmd_local_admin_cash_reports.tab=Cash Reports
 menu.cmd_local_admin_transit_list.tab=Transits
 menu.cmd_acq_view_picklist.tab=Selection Lists
 menu.cmd_acq_bib_search.tab=Title Search
+menu.cmd_acq_li_search.tab=Lineitem Search
 menu.cmd_acq_upload.tab=Load Order Record
 menu.cmd_acq_new_brief_record.tab=New Brief Record
 menu.cmd_acq_view_po.tab=Purchase Orders
-- 
2.11.0