From e9a9875da27e85e8437c082b6e40ad39ff9a1ba6 Mon Sep 17 00:00:00 2001
From: Jason Etheridge <jason@EquinoxInitiative.org>
Date: Mon, 12 Mar 2018 18:02:47 -0400
Subject: [PATCH] lp1774277 Improvements to Patron Acquisition Request

Squashed and rebased against master, this is an Angular reimplementation of the
Patron Acquisition Request user interface with some improvements.  It still
reaches into the Dojo-based Acquisition interfaces.

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>

toward acq requests

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>

4-status-not-updating-to-recieved-unless-all-items-in-order-are-recieved

Change to acq patron request status logic, which now looks like this:

If a cancel_reason is set on the patron request, then status = "Canceled"

If there is an associated hold request that has fulfillment_time set,
then status = 'Fulfilled"

If there is an associated lineitem has a state of "received", then status =
"Received"

If there is an associated purchase order with a state of "on-order" and an
associated hold request, then status = "Ordered, Hold Placed"

If there is an associated purchase order with a state of "on-order" but no
associated hold request (created through the automated process), then status =
"Ordered, Hold Not Placed"

If there is an associated lineitem (selection list), then status = "Pending"

If there is no associated lineitem, then status = "New"

Any other condition, which should be impossible (I should never say that), will
give a status of "Error"

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>

6-upc-not-on-patron-request-form

Adds a UPC column to the patron acq request table

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>

2-hold-request-fields-that-make-use-of-user-preferences

For new requests (or edited requests when a user barcode is scanned), the user's
preferences (if any) for hold notifications and pickup library will be used to
set various fields in the request dialog.

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>

5-pick-up-library-not-defaulting-to-patrons-home-library

when creating new requests, given a user, default to the user's pickup library
preference setting, or absent a preference, default to their home library.

Absent a user, default to the pickup library selector value from the request
list, if it's of an org type that can have volumes.  Otherwise, default to the
workstation library.  Technically, the without-a-user behavior is going to be
mooted whenever a user is chosen.

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>

5-pick-up-library-not-defaulting-to-patrons-home-library

Fix defaulting to patron home library in absense of user setting when creating
acq patron request from user context

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>

misc fixes

to the IDL and for the email_notify checkbox.

some refactoring to avoid using foreign fields in the request object

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>

7-retrieve-patron-fails-to-load-patron-record

give the user_request.view permission some parity with VIEW_USER

And some defensive programming if trying to create a request in
the user already known context without adequate permission

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>

handle undefined values for email/hold checkboxes

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>

remove acq.holds.allow_holds_from_purchase_request

This was added a long time ago but never actually used by the code.

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>

match pcrud perm for aur with aurs

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>

live_t/ test

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
Signed-off-by: Bill Erickson <berickxx@gmail.com>
---
 Open-ILS/examples/fm_IDL.xml                       | 103 +++-
 .../perlmods/lib/OpenILS/Application/Acq/Order.pm  | 136 ++++-
 Open-ILS/src/perlmods/live_t/22-acq-requests.t     | 340 ++++++++++++
 Open-ILS/src/sql/Pg/200.schema.acq.sql             |  23 +-
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |  29 +-
 .../XXXX.data.schema.acq.patron_requests.sql       |  88 ++++
 .../src/templates/staff/acq/requests/index.tt2     |  26 +
 .../src/templates/staff/acq/requests/t_cancel.tt2  |  31 ++
 .../src/templates/staff/acq/requests/t_clear.tt2   |  25 +
 .../src/templates/staff/acq/requests/t_edit.tt2    | 240 +++++++++
 .../src/templates/staff/acq/requests/t_list.tt2    |  82 +++
 .../templates/staff/acq/requests/t_set_no_hold.tt2 |  25 +
 .../staff/acq/requests/t_set_yes_hold.tt2          |  25 +
 Open-ILS/src/templates/staff/circ/patron/index.tt2 |   5 +
 Open-ILS/src/templates/staff/navbar.tt2            |   2 +-
 Open-ILS/web/js/ui/default/acq/common/li_table.js  |  12 +-
 .../web/js/ui/default/acq/picklist/brief_record.js |   6 +-
 .../web/js/ui/default/staff/acq/requests/list.js   | 239 +++++++++
 .../js/ui/default/staff/acq/services/requests.js   | 582 +++++++++++++++++++++
 19 files changed, 1993 insertions(+), 26 deletions(-)
 create mode 100644 Open-ILS/src/perlmods/live_t/22-acq-requests.t
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX.data.schema.acq.patron_requests.sql
 create mode 100644 Open-ILS/src/templates/staff/acq/requests/index.tt2
 create mode 100644 Open-ILS/src/templates/staff/acq/requests/t_cancel.tt2
 create mode 100644 Open-ILS/src/templates/staff/acq/requests/t_clear.tt2
 create mode 100644 Open-ILS/src/templates/staff/acq/requests/t_edit.tt2
 create mode 100644 Open-ILS/src/templates/staff/acq/requests/t_list.tt2
 create mode 100644 Open-ILS/src/templates/staff/acq/requests/t_set_no_hold.tt2
 create mode 100644 Open-ILS/src/templates/staff/acq/requests/t_set_yes_hold.tt2
 create mode 100644 Open-ILS/web/js/ui/default/staff/acq/requests/list.js
 create mode 100644 Open-ILS/web/js/ui/default/staff/acq/services/requests.js

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 300c910f51..4a10387015 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -2332,7 +2332,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 			<link field="usr" reltype="has_a" key="id" map="" class="au"/>
 		</links>
 	</class>
-	<class id="aus" controller="open-ils.cstore" oils_obj:fieldmapper="actor::user_setting" oils_persist:tablename="actor.usr_setting" reporter:label="User Setting">
+	<class id="aus" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::user_setting" oils_persist:tablename="actor.usr_setting" reporter:label="User Setting">
 		<fields oils_persist:primary="id" oils_persist:sequence="actor.usr_setting_id_seq">
 			<field reporter:label="Setting ID" name="id" reporter:datatype="id" />
 			<field reporter:label="Name" name="name" reporter:datatype="link"/>
@@ -2343,6 +2343,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 			<link field="name" reltype="has_a" key="name" map="" class="cust"/>
 			<link field="usr" reltype="has_a" key="id" map="" class="au"/>
 		</links>
+		<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+			<actions>
+				<retrieve permission="VIEW_USER">
+					<context link="usr" field="home_ou" />
+				</retrieve>
+			</actions>
+		</permacrud>
 	</class>
 	<class id="mafe" controller="open-ils.cstore" oils_obj:fieldmapper="metabib::author_field_entry" oils_persist:tablename="metabib.author_field_entry" reporter:label="Author Field Entry">
 		<fields oils_persist:primary="id" oils_persist:sequence="metabib.author_field_entry_id_seq">
@@ -3691,7 +3698,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 		</links>
 		<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
 			<actions>
-				<retrieve permission="VIEW_USER" context_field="home_ou" />
+				<retrieve permission="VIEW_USER user_request.view" context_field="home_ou" />
 			</actions>
 		</permacrud>
 	</class>
@@ -6279,6 +6286,7 @@ SELECT  usr,
 			<field reporter:label="Notes" name="notes" reporter:datatype="link" oils_persist:virtual="true"/>
 			<field reporter:label="Current Shelf Lib" name="current_shelf_lib" reporter:datatype="org_unit"/>
 			<field reporter:label="Behind Desk" name="behind_desk" reporter:datatype="bool"/>
+			<field reporter:label="Acquisition Request" name="acq_request" reporter:datatype="link" />
 		</fields>
 		<links>
 			<link field="fulfillment_lib" reltype="has_a" key="id" map="" class="aou"/>
@@ -6297,6 +6305,7 @@ SELECT  usr,
 			<link field="notes" reltype="has_many" key="hold" map="" class="ahrn"/>
 			<link field="current_shelf_lib" reltype="has_a" key="id" map="" class="aou"/>
 			<link field="sms_carrier" reltype="has_a" key="id" map="" class="csc"/>
+			<link field="acq_request" reltype="has_a" key="id" map="" class="aur"/>
 		</links>
 		<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
 			<actions>
@@ -6426,6 +6435,7 @@ SELECT  usr,
 			<field reporter:label="Shelf Expire Time" name="shelf_expire_time" reporter:datatype="timestamp"/>
 			<field reporter:label="Notes" name="notes" reporter:datatype="link" oils_persist:virtual="true"/>
 			<field reporter:label="Current Shelf Lib" name="current_shelf_lib" reporter:datatype="org_unit"/>
+			<field reporter:label="Acquisition Request" name="acq_request" reporter:datatype="link" />
 			<field reporter:label="Copy Location Sort Order" name="copy_location_order_position" reporter:datatype="int" />
 			<field reporter:label="User First Given Name" name="usr_first_given_name" reporter:datatype="text" />
 			<field reporter:label="User Second Given Name" name="usr_second_given_name" reporter:datatype="text" />
@@ -6459,6 +6469,7 @@ SELECT  usr,
 			<link field="notes" reltype="has_many" key="hold" map="" class="ahrn"/>
 			<link field="current_shelf_lib" reltype="has_a" key="id" map="" class="aou"/>
 			<link field="sms_carrier" reltype="has_a" key="id" map="" class="csc"/>
+			<link field="acq_request" reltype="has_a" key="id" map="" class="aur"/>
 		</links>
 		<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
 			<actions>
@@ -6510,6 +6521,7 @@ SELECT  usr,
 			<field reporter:label="Notes" name="notes" reporter:datatype="link" oils_persist:virtual="true"/>
 			<field reporter:label="Current Shelf Lib" name="current_shelf_lib" reporter:datatype="org_unit"/>
 			<field reporter:label="Behind Desk" name="behind_desk" reporter:datatype="bool"/>
+			<field reporter:label="Acquisition Request" name="acq_request" reporter:datatype="link" />
 		</fields>
 		<links>
 			<link field="fulfillment_lib" reltype="has_a" key="id" map="" class="aou"/>
@@ -6527,6 +6539,7 @@ SELECT  usr,
 			<link field="cancel_cause" reltype="might_have" key="id" map="" class="ahrcc"/>
 			<link field="notes" reltype="has_many" key="hold" map="" class="ahrn"/>
 			<link field="current_shelf_lib" reltype="has_a" key="id" map="" class="aou"/>
+			<link field="acq_request" reltype="has_a" key="id" map="" class="aur"/>
 		</links>
 	</class>
 
@@ -6850,7 +6863,7 @@ SELECT  usr,
 		</links>
 		<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
 			<actions>
-				<retrieve permission="VIEW_USER">
+				<retrieve permission="VIEW_USER user_request.view">
 					<context link="usr" field="home_ou" />
 				</retrieve>
 			</actions>
@@ -8378,6 +8391,7 @@ SELECT  usr,
 			<field reporter:label="Need Before Date/Time" name="need_before" reporter:datatype="timestamp" />
 			<field reporter:label="Max Acceptable Fee" name="max_fee" reporter:datatype="text" />
 			<field reporter:label="ISxN" name="isxn" reporter:datatype="text" />
+			<field reporter:label="UPC" name="upc" reporter:datatype="text" />
 			<field reporter:label="Title" name="title" reporter:datatype="text" />
 			<field reporter:label="Volume" name="volume" reporter:datatype="text" />
 			<field reporter:label="Author" name="author" reporter:datatype="text" />
@@ -8389,6 +8403,7 @@ SELECT  usr,
 			<field reporter:label="Mentioned In" name="mentioned" reporter:datatype="text" />
 			<field reporter:label="Other Info" name="other_info" reporter:datatype="text" />
 			<field reporter:label="Cancel Reason" name="cancel_reason" reporter:datatype="link" />
+			<field reporter:label="Cancel Date/Time" name="cancel_time" reporter:datatype="timestamp" />
 		</fields>
 		<links>
 			<link field="usr" reltype="has_a" key="id" map="" class="au"/>
@@ -8416,6 +8431,88 @@ SELECT  usr,
         </permacrud>
 	</class>
 
+	<class id="aurs" controller="open-ils.cstore open-ils.reporter-store open-ils.pcrud" oils_obj:fieldmapper="acq::user_request_status" reporter:label="User Purchase Request with Status" oils_persist="readonly">
+        <oils_persist:source_definition><![CDATA[
+            SELECT r.*, CASE
+                        WHEN r.cancel_reason IS NOT NULL THEN 7 -- Canceled
+                        WHEN h.fulfillment_time IS NOT NULL THEN 6 -- Fulfilled
+                        WHEN l.state = 'received' THEN 5 -- Received
+                        WHEN p.state = 'on-order' AND h.id IS NOT NULL THEN 4 -- Ordered, Hold Placed
+                        WHEN p.state = 'on-order' AND h.id IS NULL THEN 3 -- Ordered, Hold Not Placed
+                        WHEN l.id IS NOT NULL THEN 2 -- Pending
+                        WHEN l.id IS NULL THEN 1 -- New
+                        ELSE 0 -- Error
+                    END AS request_status
+                    ,u.home_ou
+            FROM      acq.user_request r
+            JOIN actor.usr u ON (r.usr = u.id)
+            LEFT JOIN acq.lineitem l ON (r.lineitem = l.id)
+            LEFT JOIN acq.purchase_order p ON (l.purchase_order = p.id)
+            LEFT JOIN action.hold_request h ON (h.acq_request = r.id)
+        ]]></oils_persist:source_definition>
+		<fields oils_persist:primary="id">
+			<field reporter:label="ID" name="id" reporter:datatype="id" reporter:selector='label'/>
+			<field reporter:label="User" name="usr" reporter:datatype="link" />
+			<field reporter:label="Request Type" name="request_type" oils_obj:required="true" reporter:datatype="link" />
+			<field reporter:label="Place Hold" name="hold" reporter:datatype="bool" />
+			<field reporter:label="Pickup Library" name="pickup_lib" reporter:datatype="link" />
+			<field reporter:label="Holdable Formats" name="holdable_formats" reporter:datatype="text" />
+			<field reporter:label="Phone Notify" name="phone_notify" reporter:datatype="text" />
+			<field reporter:label="Email Notify" name="email_notify" reporter:datatype="bool" />
+			<field reporter:label="PO Line Item" name="lineitem" reporter:datatype="link" />
+			<field reporter:label="Bib Record" name="eg_bib" reporter:datatype="link" />
+			<field reporter:label="Request Date/Time" name="request_date" reporter:datatype="timestamp" />
+			<field reporter:label="Need Before Date/Time" name="need_before" reporter:datatype="timestamp" />
+			<field reporter:label="Max Acceptable Fee" name="max_fee" reporter:datatype="text" />
+			<field reporter:label="ISxN" name="isxn" reporter:datatype="text" />
+			<field reporter:label="UPC" name="upc" reporter:datatype="text" />
+			<field reporter:label="Title" name="title" reporter:datatype="text" />
+			<field reporter:label="Volume" name="volume" reporter:datatype="text" />
+			<field reporter:label="Author" name="author" reporter:datatype="text" />
+			<field reporter:label="Article Title" name="article_title" reporter:datatype="text" />
+			<field reporter:label="Article Pages" name="article_pages" reporter:datatype="text" />
+			<field reporter:label="Publisher" name="publisher" reporter:datatype="text" />
+			<field reporter:label="Publication Location" name="location" reporter:datatype="text" />
+			<field reporter:label="Publication Date" name="pubdate" reporter:datatype="text" />
+			<field reporter:label="Mentioned In" name="mentioned" reporter:datatype="text" />
+			<field reporter:label="Other Info" name="other_info" reporter:datatype="text" />
+			<field reporter:label="Cancel Reason" name="cancel_reason" reporter:datatype="link" />
+			<field reporter:label="Cancel Date/Time" name="cancel_time" reporter:datatype="timestamp" />
+			<field reporter:label="Request Status" name="request_status" reporter:datatype="link" />
+			<field reporter:label="Home Library" name="home_ou" reporter:datatype="link"/>
+		</fields>
+		<links>
+			<link field="usr" reltype="has_a" key="id" map="" class="au"/>
+			<link field="pickup_lib" reltype="has_a" key="id" map="" class="aou"/>
+			<link field="lineitem" reltype="has_a" key="id" map="" class="jub"/>
+			<link field="eg_bib" reltype="has_a" key="id" map="" class="bre"/>
+			<link field="request_type" reltype="has_a" key="id" map="" class="aurt"/>
+			<link field="cancel_reason" reltype="has_a" key="id" map="" class="acqcr"/>
+			<link field="request_status" reltype="has_a" key="id" map="" class="aurst"/>
+			<link field="home_ou" reltype="has_a" key="id" map="" class="aou"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <retrieve permission="user_request.view">
+                    <context link="usr" field="home_ou"/>
+				</retrieve>
+            </actions>
+        </permacrud>
+	</class>
+
+	<class id="aurst" controller="open-ils.cstore open-ils.reporter-store open-ils.pcrud" oils_obj:fieldmapper="acq::user_request_status_type" oils_persist:tablename="acq.user_request_status_type" reporter:label="Acquisition Patron Request Status Type">
+		<fields oils_persist:primary="id">
+			<field reporter:label="Status ID" name="id" reporter:datatype="id" reporter:selector='label'/>
+			<field reporter:label="Status" name="label" reporter:datatype="text" oils_persist:i18n="true" />
+		</fields>
+		<links/>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <retrieve/>
+            </actions>
+        </permacrud>
+	</class>
+
 	<class id="acqct" controller="open-ils.cstore open-ils.reporter-store open-ils.pcrud" oils_obj:fieldmapper="acq::currency_type" oils_persist:tablename="acq.currency_type" reporter:label="Currency Type">
 		<fields oils_persist:primary="code">
 			<field reporter:label="Currency Code" name="code" reporter:datatype="text" reporter:selector='label'/>
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Order.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Order.pm
index 42fbebb918..feacb2f1e1 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Order.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Order.pm
@@ -265,6 +265,17 @@ sub promote_lineitem_holds {
 
         next unless ($U->is_true( $request->hold ));
 
+        my $existing_hold = $mgr->editor->search_action_hold_request(
+            {acq_request => $request->id})->[0];
+        if ($existing_hold) {
+            $logger->warn("Existing hold found where acq_request = $request->id");
+            next;
+        }
+        if (! $li->eg_bib_id) {
+            $logger->error("Hold creation attempt for aur $request->id where li.eg_bib_id is null");
+            next;
+        }
+
         my $hold = Fieldmapper::action::hold_request->new;
         $hold->usr( $request->usr );
         $hold->requestor( $request->usr );
@@ -275,6 +286,7 @@ sub promote_lineitem_holds {
         $hold->phone_notify( $request->phone_notify );
         $hold->email_notify( $request->email_notify );
         $hold->expire_time( $request->need_before );
+        $hold->acq_request( $request->id );
 
         if ($request->holdable_formats) {
             my $mrm = $mgr->editor->search_metabib_metarecord_source_map( { source => $li->eg_bib_id } )->[0];
@@ -3605,6 +3617,21 @@ __PACKAGE__->register_method (
         }
     }
 );
+__PACKAGE__->register_method (
+    method    => 'update_user_request',
+    api_name  => 'open-ils.acq.user_request.set_yes_hold.batch',
+    stream    => 1,
+    signature => {
+        desc   => 'Set hold to true for a user request or set of requests',
+        params => [
+            { desc => 'Authentication token',              type => 'string' },
+            { desc => 'ID or array of IDs for the user requests to modify'  }
+        ],
+        return => {
+            desc => 'progress object, event on error',
+        }
+    }
+);
 
 sub update_user_request {
     my($self, $conn, $auth, $aur_ids, $cancel_reason) = @_;
@@ -3637,7 +3664,14 @@ sub update_user_request {
 
         if($self->api_name =~ /set_no_hold/) {
             if ($U->is_true($aur_obj->hold)) { 
-                $aur_obj->hold(0); 
+                $aur_obj->hold(0); # FIXME - this is not really removing holds per the description
+                $e->update_acq_user_request($aur_obj) or return $e->die_event;
+            }
+        }
+
+        if($self->api_name =~ /set_yes_hold/) {
+            if (!$U->is_true($aur_obj->hold)) {
+                $aur_obj->hold(1);
                 $e->update_acq_user_request($aur_obj) or return $e->die_event;
             }
         }
@@ -3645,6 +3679,7 @@ sub update_user_request {
         if($self->api_name =~ /cancel/) {
             if ( $cancel_reason ) {
                 $aur_obj->cancel_reason( $cancel_reason );
+                $aur_obj->cancel_time( 'now' );
                 $e->update_acq_user_request($aur_obj) or return $e->die_event;
                 create_user_request_events( $e, [ $aur_obj ], 'aur.rejected' );
             } else {
@@ -3660,6 +3695,105 @@ sub update_user_request {
 }
 
 __PACKAGE__->register_method (
+    method    => 'clear_completed_user_requests',
+    api_name  => 'open-ils.acq.clear_completed_user_requests',
+    stream    => 1,
+    signature => {
+        desc  => q/
+                Auto-cancel the specified user requests if they are complete.
+                Completed is defined as having either a Request Status of Fulfilled
+                (which happens when the request is not Canceled and has an associated
+                hold request that has a fulfillment time), or having a Request Status
+                of Received (which happens when the request status is not Canceled or
+                Fulfilled and has an associated Purchase Order with a State of
+                Received) and a Place Hold value of False.
+        /,
+        params => [
+            { desc => 'Authentication token',              type => 'string' },
+            { desc => 'ID for home library of user requests to auto-cancel.'  }
+        ],
+        return => {
+            desc => 'progress object, event on error',
+        }
+    }
+);
+
+sub clear_completed_user_requests {
+    my($self, $conn, $auth, $potential_aur_ids) = @_;
+    my $e = new_editor(xact => 1, authtoken => $auth);
+    return $e->die_event unless $e->checkauth;
+    my $rid = $e->requestor->id;
+
+    my $potential_requests = $e->search_acq_user_request_status({
+             id => $potential_aur_ids
+            ,'-or' => [
+              { request_status => 6 }, # Fulfilled
+              { '-and' => [ { request_status => 5 }, { hold => 'f' } ] }  # Received
+            ]
+        }
+    );
+    my $aur_ids = [];
+
+    my %perm_test = (); my %perm_test2 = ();
+    for my $request (@$potential_requests) {
+        if ($rid != $request->usr()) {
+            if (!defined $perm_test{ $request->home_ou() }) {
+                $perm_test{ $request->home_ou() } =
+                    $e->allowed( ['user_request.view'], $request->home_ou() );
+            }
+            if (!defined $perm_test2{ $request->home_ou() }) {
+                $perm_test2{ $request->home_ou() } =
+                    $e->allowed( ['CLEAR_PURCHASE_REQUEST'], $request->home_ou() );
+            }
+            if (!$perm_test{ $request->home_ou() }) {
+                next; # failed test
+            }
+            if (!$perm_test2{ $request->home_ou() }) {
+                next; # failed test
+            }
+        }
+        push @$aur_ids, $request->id();
+    }
+
+    my $x = 1;
+    my %perm_test3 = ();
+    for my $id (@$aur_ids) {
+
+        my $aur_obj = $e->retrieve_acq_user_request([
+            $id,
+            {   flesh => 1,
+                flesh_fields => { "aur" => ['lineitem', 'usr'] }
+            }
+        ]) or return $e->die_event;
+
+        my $context_org = $aur_obj->usr()->home_ou();
+        $aur_obj->usr( $aur_obj->usr()->id() );
+
+        if ($rid != $aur_obj->usr) {
+            if (!defined $perm_test3{ $context_org }) {
+                $perm_test3{ $context_org } = $e->allowed( ['user_request.update'], $context_org );
+            }
+            if (!$perm_test3{ $context_org }) {
+                next; # failed test
+            }
+        }
+
+        $aur_obj->cancel_reason( 1015 ); # Canceled: Fulfilled
+        $aur_obj->cancel_time( 'now' );
+        $e->update_acq_user_request($aur_obj) or return $e->die_event;
+        create_user_request_events( $e, [ $aur_obj ], 'aur.rejected' );
+        # FIXME - hrmm, since this is a special type of "cancelation", should we not fire these
+        # events or should we put the burden on A/T to filter things based on cancel_reason if
+        # desired?  I don't think anyone is actually using A/T for these in practice
+
+        $conn->respond({maximum => scalar(@$aur_ids), progress => $x++});
+    }
+
+    $e->commit;
+    return {complete => 1};
+}
+
+__PACKAGE__->register_method (
     method    => 'new_user_request',
     api_name  => 'open-ils.acq.user_request.create',
     signature => {
diff --git a/Open-ILS/src/perlmods/live_t/22-acq-requests.t b/Open-ILS/src/perlmods/live_t/22-acq-requests.t
new file mode 100644
index 0000000000..2fc3d89e9f
--- /dev/null
+++ b/Open-ILS/src/perlmods/live_t/22-acq-requests.t
@@ -0,0 +1,340 @@
+#!perl
+use strict; use warnings;
+use Test::More tests => 26;
+use OpenILS::Utils::TestUtils;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Application::Acq::Order;
+
+diag("Tests ACQ purchase requests");
+
+my $script = OpenILS::Utils::TestUtils->new();
+$script->bootstrap;
+
+$script->authenticate({
+    username => 'admin',
+    password => 'demo123',
+    type => 'staff'
+});
+
+my $ses = $script->session('open-ils.storage');
+my $req = $ses->request('open-ils.storage.direct.actor.user.retrieve', 87);
+if (my $resp = $req->recv) {
+    if (my $user = $resp->content) {
+# -----------------------------------------------------------------------------
+# 1. We'll use Smith, Sarah (with usrname 99999303411 and home lib SL1)
+# -----------------------------------------------------------------------------
+        is(
+            $user->usrname,
+            '99999303411',
+            'User with id = 87 is 99999303411'
+        );
+    }
+}
+
+# -----------------------------------------------------------------------------
+# 2. Check for auth
+# -----------------------------------------------------------------------------
+ok($script->authtoken, 'Have an authtoken');
+
+$req = $script->session('open-ils.pcrud')->request(
+    'open-ils.pcrud.retrieve.acqcr',
+    $script->authtoken, 1015);
+if (my $resp = $req->recv) {
+    if (my $new_cr = $resp->content) {
+# -----------------------------------------------------------------------------
+# 3. Check for Canceled: Fulfilled
+# -----------------------------------------------------------------------------
+        is($new_cr->label,'Canceled: Fulfilled','New cancel reason for fulfilled requests');
+    }
+}
+
+$req = $script->session('open-ils.pcrud')->request(
+    'open-ils.pcrud.retrieve.aurt',
+    $script->authtoken, 1);
+if (my $resp = $req->recv) {
+    if (my $aurt = $resp->content) {
+# -----------------------------------------------------------------------------
+# 4. Check for user request type Books
+# -----------------------------------------------------------------------------
+        is($aurt->label,'Books','Found user request type Books');
+    }
+}
+
+my $aur;
+my $aur_hash = {};
+$aur_hash->{'request_type'} = 1; # Books
+$aur_hash->{'usr'} = 87;         # Smith
+$aur_hash->{'pickup_lib'} = 8;   # SL1
+$aur_hash->{'email_notify'} = 'f';
+$aur_hash->{'hold'} = 'f';
+$aur_hash->{'title'} = 'test';
+
+$req = $script->session('open-ils.acq')->request(
+    'open-ils.acq.user_request.create',
+    $script->authtoken, $aur_hash);
+if (my $resp = $req->recv) {
+    if ($aur = $resp->content) {
+# -----------------------------------------------------------------------------
+# 5. Check for created user request
+# -----------------------------------------------------------------------------
+        is(ref $aur, 'Fieldmapper::acq::user_request', 'User request created');
+        diag('User Request ID = ' . $aur->id);
+    }
+}
+
+$req = $script->session('open-ils.pcrud')->request(
+    'open-ils.pcrud.retrieve.aurs',
+    $script->authtoken, $aur->id);
+if (my $resp = $req->recv) {
+    if (my $aurs = $resp->content) {
+# -----------------------------------------------------------------------------
+# 6,7,8. Check for status-enhanced user request
+# -----------------------------------------------------------------------------
+        is($aurs->id,$aur->id,'Found status-enhanced user request');
+        is($aurs->request_status,1,'Request Status = New');
+        is($aurs->home_ou,8,'Home Lib = SL1');
+    }
+}
+
+# open-ils.acq.picklist.create
+# {"__c":"acqpl","__p":[null,1,"4","test",null,null,null,null,1,1]}
+# {"__c":"acqpl","__p":[1,1,4,"test","2018-07-31T16:33:39-0400","now",null,null,1,1]}
+
+my $picklist_id;
+my $picklist = Fieldmapper::acq::picklist->new;
+$picklist->isnew(1);
+$picklist->owner(1);            # admin
+$picklist->creator(1);          # admin
+$picklist->editor(1);           # admin
+$picklist->org_unit(8);         # SL1
+$picklist->name( $script->authtoken ); # $picklist->name('22-acq-requests.t');
+$picklist->create_time('now');
+$picklist->edit_time('now');
+
+$req = $script->session('open-ils.acq')->request(
+    'open-ils.acq.picklist.create',
+    $script->authtoken, $picklist);
+if (my $resp = $req->recv) {
+    if ($picklist_id = $resp->content) {
+# -----------------------------------------------------------------------------
+# 9. Check for created picklist
+# -----------------------------------------------------------------------------
+        ok($picklist_id > 0,'Created picklist aka selection list');
+        diag('Picklist ID = ' . $picklist_id);
+    }
+}
+
+my $jub_id;
+my $jub = Fieldmapper::acq::lineitem->new;
+$jub->selector(1);          # admin
+$jub->picklist($picklist_id);
+$jub->create_time('now');
+$jub->edit_time('now');
+$jub->marc('<record xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.loc.gov/MARC21/slim" xmlns:marc="http://www.loc.gov/MARC21/slim" xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/ standards/marcxml/schema/MARC21slim.xsd"><leader>00000nam a22000007a 4500</leader><marc:datafield tag="245" ind1=" " ind2=" "><marc:subfield code="a">test  </marc:subfield></marc:datafield></record>');
+$jub->state('new');
+$jub->creator(1);           # admin
+$jub->editor(1);            # admin
+$jub->estimated_unit_price(1.00);
+$jub->isnew(1);
+
+$req = $script->session('open-ils.acq')->request(
+    'open-ils.acq.lineitem.create',
+    $script->authtoken, $jub);
+if (my $resp = $req->recv) {
+    if ($jub_id = $resp->content) {
+# -----------------------------------------------------------------------------
+# 10. Check for created lineitem
+# -----------------------------------------------------------------------------
+        ok($jub_id > 0,'Created lineitem');
+        diag('Lineitem ID = ' . $jub_id);
+    }
+}
+
+$req = $script->session('open-ils.pcrud')->request(
+    'open-ils.pcrud.retrieve.aur',
+    $script->authtoken, $aur->id);
+if (my $resp = $req->recv) {
+    if ($aur = $resp->content) {
+# -----------------------------------------------------------------------------
+# 11. Retrieve bare user request
+# -----------------------------------------------------------------------------
+        is(ref $aur,'Fieldmapper::acq::user_request','Retrieved bare user request');
+    }
+}
+
+$aur->ischanged(1);
+$aur->lineitem($jub_id);
+
+diag('Updating aur->lineitem');
+my $pcrud_ses = $script->session('open-ils.pcrud');
+$pcrud_ses->connect();
+my $xact = $pcrud_ses->request(
+    'open-ils.pcrud.transaction.begin',
+    $script->authtoken
+)->gather(1);
+my $aur_id = $pcrud_ses->request(
+    'open-ils.pcrud.update.aur',
+    $script->authtoken,
+    $aur
+)->gather(1);
+# -----------------------------------------------------------------------------
+# 12. Updated user request with lineitem
+# -----------------------------------------------------------------------------
+is($aur_id,$aur->id,'Updated user request with lineitem');
+
+$pcrud_ses->request(
+    'open-ils.pcrud.transaction.commit',
+    $script->authtoken
+)->gather(1);
+$pcrud_ses->disconnect();
+undef($pcrud_ses);
+
+$req = $script->session('open-ils.acq')->request(
+    'open-ils.acq.lineitem.batch_update',
+    $script->authtoken, { 'lineitems' => [$jub_id] }, {
+        "item_count" => 1, "location" => 118, "owning_lib" => 4, "fund" => 1});
+if (my $resp = $req->recv) {
+    if (my $return = $resp->content) {
+# -----------------------------------------------------------------------------
+# 13. Check adding of copy to line
+# -----------------------------------------------------------------------------
+        is($return,$jub_id,'Added copy to lineitem');
+    }
+}
+
+$req = $script->session('open-ils.pcrud')->request(
+    'open-ils.pcrud.retrieve.aurs',
+    $script->authtoken, $aur->id);
+if (my $resp = $req->recv) {
+    if (my $aurs = $resp->content) {
+# -----------------------------------------------------------------------------
+# 14,15,16. Check user request status and lineitem
+# -----------------------------------------------------------------------------
+        is($aurs->id,$aur->id,'Re-retrieved status-enhanced user request');
+        is($aurs->request_status,2,'Request Status = Pending');
+        is($aurs->lineitem,$jub_id,'Lineitem matches');
+    }
+}
+
+my $purchase_order_id;
+my $purchase_order = Fieldmapper::acq::purchase_order->new;
+$purchase_order->owner(1);                   # admin
+$purchase_order->create_time('now');
+$purchase_order->edit_time('now');
+$purchase_order->provider(2);                # BRODART
+$purchase_order->state('pending');
+$purchase_order->ordering_agency(4);         # BR1
+$purchase_order->creator(1);                 # admin
+$purchase_order->editor(1);                  # admin
+$purchase_order->name( $script->authtoken ); # $purchase_order->name('22-acq-requests.t');
+$purchase_order->isnew(1);
+
+$req = $script->session('open-ils.acq')->request(
+    'open-ils.acq.purchase_order.create',
+    $script->authtoken, $purchase_order, { 'lineitems' => [$jub_id] });
+if (my $resp = $req->recv) {
+    if (my $return = $resp->content) {
+#FIXME: open-ils.acq.purchase_order.create docs needs to be updated with correct return value 
+#FIXME: open-ils.acq.purchase_order.create docs needs to be updated for lineitem_ids argument
+# -----------------------------------------------------------------------------
+# 17. Check for created purchase_order
+# -----------------------------------------------------------------------------
+        $purchase_order_id = $$return{'purchase_order'}->id;
+        ok($purchase_order_id > 0,'Created purchase_order');
+        diag('Purchase Order ID = ' . $purchase_order_id);
+    }
+}
+
+$req = $script->session('open-ils.pcrud')->request(
+    'open-ils.pcrud.retrieve.aurs',
+    $script->authtoken, $aur->id);
+if (my $resp = $req->recv) {
+    if (my $aurs = $resp->content) {
+# -----------------------------------------------------------------------------
+# 18, 19. Check user request status is still Pending
+# -----------------------------------------------------------------------------
+        is($aurs->id,$aur->id,'Re-retrieved status-enhanced user request');
+        is($aurs->request_status,2,'Request Status = Pending');
+    }
+}
+
+
+# open-ils.acq.purchase_order.assets.create
+my $vlArgs = {
+    'vandelay' => {
+        'auto_overlay_1match' => 0,
+        'match_quality_ratio' => '0.0',
+        'queue_name' => $script->authtoken, #'queue_name' => '22-acq-requests.t',
+        'import_no_match' => 'on',
+        'bib_source' => '',
+        'fall_through_merge_profile' => '',
+        'merge_profile' => '',
+        'auto_overlay_best_match' => 0,
+        'strip_field_groups' => [],
+        'auto_overlay_exact' => 0,
+        'existing_queue' => '',
+        'match_set' => ''
+    }
+};
+$req = $script->session('open-ils.acq')->request(
+    'open-ils.acq.purchase_order.assets.create',
+    $script->authtoken, $purchase_order_id, $vlArgs);
+if (my $resp = $req->recv) {
+    if (my $return = $resp->content) {
+# -----------------------------------------------------------------------------
+# 20. Check for created assets
+# -----------------------------------------------------------------------------
+        is($return->{'complete'},1,'Assets created');
+    }
+}
+$req = $script->session('open-ils.acq')->request(
+    'open-ils.acq.purchase_order.activate',
+    $script->authtoken, $purchase_order_id, {
+        'no_assets' => 0, 'zero_copy_activate' => 0});
+if (my $resp = $req->recv) {
+    if (my $return = $resp->content) {
+# -----------------------------------------------------------------------------
+# 21. Check for activated purchase order
+# -----------------------------------------------------------------------------
+        is($return,1,'Purchase order activated');
+    }
+}
+
+$req = $script->session('open-ils.pcrud')->request(
+    'open-ils.pcrud.retrieve.aurs',
+    $script->authtoken, $aur->id);
+if (my $resp = $req->recv) {
+    if (my $aurs = $resp->content) {
+# -----------------------------------------------------------------------------
+# 22, 23. Check user request status Ordered, No Hold Placed
+# -----------------------------------------------------------------------------
+        is($aurs->id,$aur->id,'Re-retrieved status-enhanced user request');
+        is($aurs->request_status,3,'Request Status = Ordered, Hold Not Placed');
+    }
+}
+
+$req = $script->session('open-ils.acq')->request(
+    'open-ils.acq.user_request.cancel.batch.atomic',
+    $script->authtoken, [ $aur_id ], 1015); # Canceled: Fulfilled
+if (my $resp = $req->recv) {
+    if (my $return = $resp->content) {
+# -----------------------------------------------------------------------------
+# 24. Check for activated purchase order
+# -----------------------------------------------------------------------------
+        is($return->[1]->{'complete'},1,'User request canceled with Canceled: Fulfilled');
+    }
+}
+
+$req = $script->session('open-ils.pcrud')->request(
+    'open-ils.pcrud.retrieve.aurs',
+    $script->authtoken, $aur->id);
+if (my $resp = $req->recv) {
+    if (my $aurs = $resp->content) {
+# -----------------------------------------------------------------------------
+# 25, 26. Check user request status Ordered, No Hold Placed
+# -----------------------------------------------------------------------------
+        is($aurs->id,$aur->id,'Re-retrieved status-enhanced user request');
+        is($aurs->request_status,7,'Request Status = Canceled');
+    }
+}
+
diff --git a/Open-ILS/src/sql/Pg/200.schema.acq.sql b/Open-ILS/src/sql/Pg/200.schema.acq.sql
index c820e74409..68efce9bcc 100644
--- a/Open-ILS/src/sql/Pg/200.schema.acq.sql
+++ b/Open-ILS/src/sql/Pg/200.schema.acq.sql
@@ -942,6 +942,24 @@ CREATE TABLE acq.user_request_type (
     label   TEXT    NOT NULL UNIQUE -- i18n-ize
 );
 
+CREATE TABLE acq.user_request_status_type (
+     id  SERIAL  PRIMARY KEY
+    ,label TEXT
+);
+
+INSERT INTO acq.user_request_status_type (id,label) VALUES
+     (0,oils_i18n_gettext(0,'Error','aurst','label'))
+    ,(1,oils_i18n_gettext(1,'New','aurst','label'))
+    ,(2,oils_i18n_gettext(2,'Pending','aurst','label'))
+    ,(3,oils_i18n_gettext(3,'Ordered, Hold Not Placed','aurst','label'))
+    ,(4,oils_i18n_gettext(4,'Ordered, Hold Placed','aurst','label'))
+    ,(5,oils_i18n_gettext(5,'Received','aurst','label'))
+    ,(6,oils_i18n_gettext(6,'Fulfilled','aurst','label'))
+    ,(7,oils_i18n_gettext(7,'Canceled','aurst','label'))
+;
+
+SELECT SETVAL('acq.user_request_status_type_id_seq'::TEXT, 100);
+
 CREATE TABLE acq.user_request (
     id                  SERIAL  PRIMARY KEY,
     usr                 INT     NOT NULL REFERENCES actor.usr (id), -- requesting user
@@ -959,6 +977,7 @@ CREATE TABLE acq.user_request (
   
     request_type        INT     NOT NULL REFERENCES acq.user_request_type (id),
     isxn                TEXT,
+    upc                 TEXT,
     title               TEXT,
     volume              TEXT,
     author              TEXT,
@@ -970,9 +989,11 @@ CREATE TABLE acq.user_request (
     mentioned           TEXT,
     other_info          TEXT,
 	cancel_reason       INT    REFERENCES acq.cancel_reason( id )
-	                           DEFERRABLE INITIALLY DEFERRED
+	                           DEFERRABLE INITIALLY DEFERRED,
+    cancel_time         TIMESTAMPTZ
 );
 
+ALTER TABLE action.hold_request ADD COLUMN acq_request INT REFERENCES acq.user_request (id);
 
 -- Functions
 
diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index d9e75eb684..e0e02c66c9 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -1913,7 +1913,9 @@ INSERT INTO permission.perm_list ( id, code, description ) VALUES
  (608, 'APPLY_WORKSTATION_SETTING',
    oils_i18n_gettext(608, 'APPLY_WORKSTATION_SETTING', 'ppl', 'description')),
  ( 609, 'MANAGE_CUSTOM_PERM_GRP_TREE', oils_i18n_gettext( 609,
-    'Allows a user to manage custom permission group lists.', 'ppl', 'description' ))
+    'Allows a user to manage custom permission group lists.', 'ppl', 'description' )),
+ ( 610, 'CLEAR_PURCHASE_REQUEST', oils_i18n_gettext(610,
+    'Clear Completed User Purchase Requests', 'ppl', 'description'))
 ;
 
 
@@ -2591,6 +2593,7 @@ INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
 		aout.name = 'Consortium' AND
 		perm.code IN (
 			'ALLOW_ALT_TCN',
+			'CLEAR_PURCHASE_REQUEST',
 			'CREATE_BIB_IMPORT_QUEUE',
 			'CREATE_IMPORT_ITEM',
 			'CREATE_INVOICE',
@@ -2964,12 +2967,13 @@ INSERT INTO config.settings_group (name, label) VALUES
 
 
 INSERT INTO acq.user_request_type (id,label) VALUES (1, oils_i18n_gettext('1', 'Books', 'aurt', 'label'));
-INSERT INTO acq.user_request_type (id,label) VALUES (2, oils_i18n_gettext('2', 'Journal/Magazine & Newspaper Articles', 'aurt', 'label'));
+INSERT INTO acq.user_request_type (id,label) VALUES (2, oils_i18n_gettext('2', 'Articles', 'aurt', 'label'));
 INSERT INTO acq.user_request_type (id,label) VALUES (3, oils_i18n_gettext('3', 'Audiobooks', 'aurt', 'label'));
 INSERT INTO acq.user_request_type (id,label) VALUES (4, oils_i18n_gettext('4', 'Music', 'aurt', 'label'));
 INSERT INTO acq.user_request_type (id,label) VALUES (5, oils_i18n_gettext('5', 'DVDs', 'aurt', 'label'));
+INSERT INTO acq.user_request_type (id,label) VALUES (6, oils_i18n_gettext('6', 'Other', 'aurt', 'label'));
 
-SELECT SETVAL('acq.user_request_type_id_seq'::TEXT, 6);
+SELECT SETVAL('acq.user_request_type_id_seq'::TEXT, 7);
 
 
 -- org_unit setting types
@@ -3028,15 +3032,6 @@ INSERT into config.org_unit_setting_type
         'coust', 'description'),
     'integer', null)
 
-,( 'acq.holds.allow_holds_from_purchase_request', 'acq',
-    oils_i18n_gettext('acq.holds.allow_holds_from_purchase_request',
-        'Allows patrons to create automatic holds from purchase requests.',
-        'coust', 'label'),
-    oils_i18n_gettext('acq.holds.allow_holds_from_purchase_request',
-        'Allows patrons to create automatic holds from purchase requests.',
-        'coust', 'description'),
-    'bool', null)
-
 ,( 'acq.tmp_barcode_prefix', 'acq',
     oils_i18n_gettext('acq.tmp_barcode_prefix',
         'Temporary barcode prefix',
@@ -3484,19 +3479,19 @@ INSERT into config.org_unit_setting_type
 
 ,( 'circ.holds.canceled.display_age', 'holds',
     oils_i18n_gettext('circ.holds.canceled.display_age',
-        'Canceled holds display age',
+        'Canceled holds/requests display age',
         'coust', 'label'),
     oils_i18n_gettext('circ.holds.canceled.display_age',
-        'Show all canceled holds that were canceled within this amount of time',
+        'Show all canceled entries in patron holds and patron acquisition requests interfaces that were canceled within this amount of time',
         'coust', 'description'),
     'interval', null)
 
 ,( 'circ.holds.canceled.display_count', 'holds',
     oils_i18n_gettext('circ.holds.canceled.display_count',
-        'Canceled holds display count',
+        'Canceled holds/requests display count',
         'coust', 'label'),
     oils_i18n_gettext('circ.holds.canceled.display_count',
-        'How many canceled holds to show in patron holds interfaces',
+        'How many canceled entries to show in patron holds and patron acquisition requests interfaces',
         'coust', 'description'),
     'integer', null)
 
@@ -11959,6 +11954,8 @@ INSERT INTO acq.cancel_reason (keep_debits, id, org_unit, label, description) VA
 	oils_i18n_gettext(1007, 'This line item is not accepted by the seller.', 'acqcr', 'description')),
 ('f',( 10+1000), 1, oils_i18n_gettext(1010, 'Canceled: Not Found', 'acqcr', 'label'),
        oils_i18n_gettext(1010, 'This line item is not found in the referenced message.', 'acqcr', 'description')),
+('f',( 15+1000), 1, oils_i18n_gettext(1015, 'Canceled: Fulfilled', 'acqcr', 'label'),
+       oils_i18n_gettext(1015, 'This acquisition request has been fulfilled.', 'acqcr', 'description')),
 ('t',( 24+1000), 1, oils_i18n_gettext(1024, 'Delayed: Accepted with amendment', 'acqcr', 'label'),
        oils_i18n_gettext(1024, 'Accepted with changes which require no confirmation.', 'acqcr', 'description'));
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.schema.acq.patron_requests.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.schema.acq.patron_requests.sql
new file mode 100644
index 0000000000..5035008af7
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.schema.acq.patron_requests.sql
@@ -0,0 +1,88 @@
+BEGIN;
+
+--SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+ALTER TABLE acq.user_request ADD COLUMN cancel_time TIMESTAMPTZ;
+ALTER TABLE acq.user_request ADD COLUMN upc TEXT;
+ALTER TABLE action.hold_request ADD COLUMN acq_request INT REFERENCES acq.user_request (id);
+
+UPDATE
+    config.org_unit_setting_type
+SET
+    label = oils_i18n_gettext(
+        'circ.holds.canceled.display_age',
+        'Canceled holds/requests display age',
+        'coust', 'label'),
+    description = oils_i18n_gettext(
+        'circ.holds.canceled.display_age',
+        'Show all canceled entries in patron holds and patron acquisition requests interfaces that were canceled within this amount of time',
+        'coust', 'description')
+WHERE
+    name = 'circ.holds.canceled.display_age'
+;
+
+UPDATE
+    config.org_unit_setting_type
+SET
+    label = oils_i18n_gettext(
+        'circ.holds.canceled.display_count',
+        'Canceled holds/requests display count',
+        'coust', 'label'),
+    description = oils_i18n_gettext(
+        'circ.holds.canceled.display_count',
+        'How many canceled entries to show in patron holds and patron acquisition requests interfaces',
+        'coust', 'description')
+WHERE
+    name = 'circ.holds.canceled.display_count'
+;
+
+INSERT INTO acq.cancel_reason (org_unit, keep_debits, id, label, description)
+    VALUES (
+        1, 'f', 1015,
+        oils_i18n_gettext(1015, 'Canceled: Fulfilled', 'acqcr', 'label'),
+        oils_i18n_gettext(1015, 'This acquisition request has been fulfilled.', 'acqcr', 'description')
+    )
+;
+
+UPDATE
+    acq.user_request_type
+SET
+    label = oils_i18n_gettext('2', 'Articles', 'aurt', 'label')
+WHERE
+    id = 2
+;
+
+INSERT INTO acq.user_request_type (id,label)
+    SELECT 6, oils_i18n_gettext('6', 'Other', 'aurt', 'label');
+
+SELECT SETVAL('acq.user_request_type_id_seq'::TEXT, (SELECT MAX(id)+1 FROM acq.user_request_type));
+
+INSERT INTO permission.perm_list ( id, code, description ) VALUES
+ ( 610, 'CLEAR_PURCHASE_REQUEST', oils_i18n_gettext(610,
+    'Clear Completed User Purchase Requests', 'ppl', 'description'))
+;
+
+CREATE TABLE acq.user_request_status_type (
+     id  SERIAL  PRIMARY KEY
+    ,label TEXT
+);
+
+INSERT INTO acq.user_request_status_type (id,label) VALUES
+     (0,oils_i18n_gettext(0,'Error','aurst','label'))
+    ,(1,oils_i18n_gettext(1,'New','aurst','label'))
+    ,(2,oils_i18n_gettext(2,'Pending','aurst','label'))
+    ,(3,oils_i18n_gettext(3,'Ordered, Hold Not Placed','aurst','label'))
+    ,(4,oils_i18n_gettext(4,'Ordered, Hold Placed','aurst','label'))
+    ,(5,oils_i18n_gettext(5,'Received','aurst','label'))
+    ,(6,oils_i18n_gettext(6,'Fulfilled','aurst','label'))
+    ,(7,oils_i18n_gettext(7,'Canceled','aurst','label'))
+;
+
+SELECT SETVAL('acq.user_request_status_type_id_seq'::TEXT, 100);
+
+-- not used
+DELETE FROM actor.org_unit_setting WHERE name = 'acq.holds.allow_holds_from_purchase_request';
+DELETE FROM config.org_unit_setting_type_log WHERE field_name = 'acq.holds.allow_holds_from_purchase_request';
+DELETE FROM config.org_unit_setting_type WHERE name = 'acq.holds.allow_holds_from_purchase_request';
+
+COMMIT;
diff --git a/Open-ILS/src/templates/staff/acq/requests/index.tt2 b/Open-ILS/src/templates/staff/acq/requests/index.tt2
new file mode 100644
index 0000000000..c6ae3d392d
--- /dev/null
+++ b/Open-ILS/src/templates/staff/acq/requests/index.tt2
@@ -0,0 +1,26 @@
+[%
+  WRAPPER "staff/base.tt2";
+  ctx.page_title = l("Acquisition Patron Requests");
+  ctx.page_app = "egAcqRequestsApp";
+%]
+
+[% BLOCK APP_JS %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/user.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/acq/services/requests.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/acq/requests/list.js"></script>
+<script>
+angular.module('egCoreMod').run(['egStrings', function(s) {
+    s.CREATE_USER_REQUEST_SUCCESS = "[% l('Created Acquisition Patron Request') %]";
+    s.CREATE_USER_REQUEST_FAIL = "[% l('Failed to Create Acquisition Patron Request') %]";
+    s.EDIT_USER_REQUEST_SUCCESS = "[% l('Edited Acquisition Patron Request') %]";
+    s.EDIT_USER_REQUEST_FAIL = "[% l('Failed to Edit Acquisition Patron Request') %]";
+}]);
+</script>
+<link rel="stylesheet" href="[% ctx.base_path %]/staff/css/circ.css" />
+[% END %]
+
+<div ng-view></div>
+
+[% END %]
diff --git a/Open-ILS/src/templates/staff/acq/requests/t_cancel.tt2 b/Open-ILS/src/templates/staff/acq/requests/t_cancel.tt2
new file mode 100644
index 0000000000..ba1db9d281
--- /dev/null
+++ b/Open-ILS/src/templates/staff/acq/requests/t_cancel.tt2
@@ -0,0 +1,31 @@
+[% ctx.page_title = l("Cancel Selected Patron Requests"); %]
+<!-- use <form> so we get submit-on-enter for free -->
+<form class="form-validated" novalidate name="form" ng-submit="ok(cancel_reason)">
+    <div> <!-- modal-content -->
+        <div class="modal-header">
+            <button type="button" class="close" ng-click="cancel()"
+                aria-hidden="true">&times;</button>
+            <h4 class="modal-title">
+                [% l('Cancel Selected Patron Requests') %]</h4>
+        </div>
+        <div class="modal-body">
+            <div class="form-group">
+                <label for="ids">[% l('Request IDs') %]</label>
+                <input type="text" class="form-control"
+                    id="ids" ng-model="ids" ng-disabled="true"/>
+            </div>
+            <div class="form-group">
+                <label for="cancel-reason-selector">[% l('Cancel Reason') %]</label>
+                <select id="cancel-reason-selector" class="form-control" required
+                    ng-model="cancel_reason"
+                    ng-options="rt.label() for rt in cancel_reasons"/>
+            </div>
+        </div>
+        <div class="modal-footer">
+            <input type="submit" ng-disabled="form.$invalid"
+                class="btn btn-primary" value="[% l('Cancel Requests') %]"/>
+            <button class="btn btn-warning"
+                ng-click="cancel()">[% l('Abort Cancellation') %]</button>
+        </div>
+    </div> <!-- modal-content -->
+</form>
diff --git a/Open-ILS/src/templates/staff/acq/requests/t_clear.tt2 b/Open-ILS/src/templates/staff/acq/requests/t_clear.tt2
new file mode 100644
index 0000000000..b15206aa04
--- /dev/null
+++ b/Open-ILS/src/templates/staff/acq/requests/t_clear.tt2
@@ -0,0 +1,25 @@
+[% ctx.page_title = l("Clear Completed Patron Requests"); %]
+<!-- use <form> so we get submit-on-enter for free -->
+<form class="form-validated" novalidate name="form" ng-submit="ok(true)">
+    <div> <!-- modal-content -->
+        <div class="modal-header">
+            <button type="button" class="close" ng-click="cancel()"
+                aria-hidden="true">&times;</button>
+            <h4 class="modal-title">
+                [% l('Clear Completed Patron Requests') %]</h4>
+        </div>
+        <div class="modal-body">
+            <div class="form-group">
+                <label for="ids">[% l('Request IDs') %]</label>
+                <input type="text" class="form-control"
+                    id="ids" ng-model="ids" ng-disabled="true"/>
+            </div>
+        </div>
+        <div class="modal-footer">
+            <input type="submit" ng-disabled="form.$invalid"
+                class="btn btn-primary" value="[% l('Clear Requests') %]"/>
+            <button class="btn btn-warning"
+                ng-click="cancel()">[% l('Abort Clear Requests') %]</button>
+        </div>
+    </div> <!-- modal-content -->
+</form>
diff --git a/Open-ILS/src/templates/staff/acq/requests/t_edit.tt2 b/Open-ILS/src/templates/staff/acq/requests/t_edit.tt2
new file mode 100644
index 0000000000..a62a2281fc
--- /dev/null
+++ b/Open-ILS/src/templates/staff/acq/requests/t_edit.tt2
@@ -0,0 +1,240 @@
+[% ctx.page_title = l("Create/Edit/View patron Request"); %]
+<!-- use <form> so we get submit-on-enter for free -->
+<form class="form-validated" novalidate name="form"
+      ng-submit="ok(request,extra)">
+    <div> <!-- modal-content -->
+        <div class="modal-header">
+            <button type="button" class="close" ng-click="cancel()"
+                aria-hidden="true">&times;</button>
+            <h4 ng-if="mode=='create'" class="modal-title">
+                [% l('Create Patron Request') %]</h4>
+            <h4 ng-if="mode=='edit'" class="modal-title">
+                [% l('Edit Patron Request') %]</h4>
+            <h4 ng-if="mode=='view'" class="modal-title">
+                [% l('View Patron Request') %]</h4>
+        </div>
+        <div class="modal-header">
+            <div class="row">
+                <div class="form-group col-sm-6">
+                    <label for="edit-request-usr">
+                        [% l('User Barcode') %]</label>
+                    <input type="text" ng-model="extra.barcode" id="barcode"
+                        class="form-control" focus-me="focusMe"
+                        ng-model-options="{ debounce: 1000 }"
+                        ng-disabled="mode=='view'"
+                        placeholder="[% l('Barcode...') %]"/>
+                    <span ng-show="extra.barcode && request.usr">
+                        [% l('[_1], [_2] [_3] : [_4]',
+                          '{{extra.user_obj.family_name}}'
+                          '{{extra.user_obj.first_given_name}}'
+                          '{{extra.user_obj.second_given_name}}'
+                          '{{extra.user_obj.home_ou.shortname}}') %]
+                    </span>
+                </div>
+                <div class="form-group col-sm-6">
+                    <label for="edit-request-usr">[% l('User ID') %]</label>
+                    <input type="text" class="form-control" focus-me='focusMe'
+                        id="edit-request-usr" ng-model="request.usr"
+                        required ng-disabled="true"/>
+                    <span class="alert-info pull-right"
+                        ng-show="extra.barcode && !request.usr">
+                        [% l('Not Found') %]
+                    </span>
+                </div>
+            </div>
+            <div class="form-group" ng-show="request.cancel_reason">
+                <label for="edit-request-id">[% l('Cancel Reason') %]</label>
+                <div class="form-control" ng-disabled="true">
+                    {{request.cancel_reason.label()}}
+                </div>
+            </div>
+            <div class="row">
+                <div class="form-group col-sm-6">
+                    <label>[% l('Request Date/Time') %]</label>
+                    <div class="form-control" ng-disabled="true">
+                        {{request.request_date | date:$root.egDateAndTimeFormat}}
+                    </div>
+                </div>
+                <div class="form-group col-sm-6">
+                    <label for="edit-request-need-before">
+                        [% l('Need Before Date/Time') %]</label>
+                    <eg-date-input id="edit-request-need-before"
+                        show-time-picker ng-disabled="mode=='view'"
+                        ng-model="request.need_before" min-date="minDate"/>
+                </div>
+            </div>
+            <div class="row" ng-show="mode=='view'">
+                <div class="form-group col-sm-6">
+                    <label for="edit-request-bib-record">
+                        [% l('Bib Record') %]</label>
+                    <input type="text" class="form-control" focus-me='focusMe'
+                        id="edit-request-bib-record" ng-disabled="true"
+                        ng-model="request.eg_bib"/>
+                </div>
+                <div class="form-group col-sm-6">
+                    <label for="edit-request-lineitem">
+                        [% l('PO Line Item') %]</label>
+                    <input type="text" class="form-control" focus-me='focusMe'
+                        id="edit-request-lineitem" ng-disabled="true"
+                        ng-model="request.lineitem.id"/>
+                </div>
+            </div>
+            <div class="row">
+                <div class="form-group col-sm-6">
+                    <label for="edit-request-place-hold">
+                        <input type="checkbox" id="edit-request-place-hold"
+                            ng-disabled="mode=='view'" ng-model="request.hold"/>
+                        [% l('Place Hold?') %]
+                    </label>
+                </div>
+                <div class="form-group col-sm-6">
+                    <label for="edit-request-pickup-lib">
+                        [% l('Pickup Library') %]</label>
+                    <eg-org-selector id="edit-request-pickup-lib"
+                        ng-hide="mode=='view'" selected="request.pickup_lib"
+                        disable-test="cant_have_vols"/>
+                    <span ng-show="mode=='view'">
+                        {{request.pickup_lib.shortname()}}
+                    </span>
+                </div>
+            </div>
+            <div class="row">
+                <div class="form-group col-sm-6">
+                    <label for="edit-request-email-notify">
+                        <input type="checkbox" id="edit-request-email-notify"
+                            ng-disabled="mode=='view'"
+                            ng-model="request.email_notify"/>
+                        [% l('Notify By Email When Hold Ready?') %]
+                    </label>
+                </div>
+                <div class="form-group col-sm-6">
+                    <label for="edit-extra-phone-notify">
+                        <input type="checkbox" id="edit-extra-phone-notify"
+                            ng-disabled="mode=='view'"
+                            ng-model="extra.phone_notify"/>
+                        [% l('Notify By Phone When Hold Ready?') %]
+                    </label>
+                    <input type="text" class="form-control" focus-me='focusMe'
+                        id="edit-request-phone-notify"
+                        ng-disabled="mode=='view'"
+                        ng-model="request.phone_notify"/>
+                </div>
+            </div>
+        </div>
+        <div class="modal-body">
+            <div class="row" ng-if="mode!='create'">
+                <div class="form-group col-sm-6"">
+                    <label for="edit-request-id">[% l('Request ID') %]</label>
+                    <input type="text" class="form-control" focus-me='focusMe'
+                        id="edit-request-id" ng-model="request.id" ng-disabled="true"/>
+                </div>
+                <div class="form-group col-sm-6"">
+                    <label for="edit-request-status">[% l('Request Status') %]</label>
+                    <input type="text" class="form-control" focus-me='focusMe'
+                        id="edit-request-status" ng-model="request.request_status.label" ng-disabled="true"/>
+                </div>
+            </div>
+            <div class="form-group">
+                <label for="request-type-selector">[% l('Request Type') %]</label>
+                <select id="request-type-selector" class="form-control" required
+                    ng-model="extra.selected_request_type"
+                    ng-disabled="mode=='view'"
+                    ng-options="rt.label() for rt in request_types"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-isxn">[% l('ISxN') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-isxn" ng-model="request.isxn"
+                    ng-disabled="mode=='view'" placeholder="[% l('ISxN...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-upc">[% l('UPC') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-upc" ng-model="request.upc"
+                    ng-disabled="mode=='view'" placeholder="[% l('UPC...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-title">[% l('Title') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-title" ng-model="request.title"
+                    ng-disabled="mode=='view'" placeholder="[% l('Title...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-volume">[% l('Volume') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-volume" ng-model="request.volume"
+                    ng-disabled="mode=='view'" placeholder="[% l('Volume...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-author">[% l('Author') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-author" ng-model="request.author"
+                    ng-disabled="mode=='view'" placeholder="[% l('Author...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-publisher">[% l('Publisher') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-publisher" ng-model="request.publisher"
+                    ng-disabled="mode=='view'" placeholder="[% l('Publisher...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-publication-location">
+                    [% l('Publication Location') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-publication-location"
+                    ng-model="request.location"
+                    ng-disabled="mode=='view'"
+                    placeholder="[% l('Publication Location...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-publication-date">
+                    [% l('Publication Date') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-publication-date"
+                    ng-model="request.pubdate"
+                    ng-disabled="mode=='view'"
+                    placeholder="[% l('Publication Date...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-article-title">
+                    [% l('Article Title') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    ng-disabled="mode=='view' || request.request_type != '2'"
+                    id="edit-request-article-title" ng-model="request.article_title"
+                    placeholder="[% l('Article Title...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-article-pages">
+                    [% l('Article Pages') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    ng-disabled="mode=='view' || request.request_type != '2'"
+                    id="edit-request-article-pages" ng-model="request.article_pages"
+                    placeholder="[% l('Article Pages...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-mentioned-in">
+                    [% l('Mentioned In') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-mentioned-in"
+                    ng-model="request.mentioned"
+                    ng-disabled="mode=='view'"
+                    placeholder="[% l('Mentioned In...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-other-info">
+                    [% l('Other Info') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-other-info"
+                    ng-model="request.other_info"
+                    ng-disabled="mode=='view'"
+                    placeholder="[% l('Other Info...') %]"/>
+            </div>
+        </div>
+        <div class="modal-footer">
+            <input type="submit" ng-hide="mode=='view'" ng-disabled="form.$invalid"
+                class="btn btn-primary" value="[% l('Save') %]"/>
+            <button class="btn btn-warning"
+                ng-click="cancel()">[% l('Cancel') %]</button>
+        </div>
+    </div> <!-- modal-content -->
+</form>
diff --git a/Open-ILS/src/templates/staff/acq/requests/t_list.tt2 b/Open-ILS/src/templates/staff/acq/requests/t_list.tt2
new file mode 100644
index 0000000000..5c279b4faa
--- /dev/null
+++ b/Open-ILS/src/templates/staff/acq/requests/t_list.tt2
@@ -0,0 +1,82 @@
+<div class="container-fluid" style="text-align:center">
+  <div class="alert alert-info alert-less-pad strong-text-2">
+    <span>[% l('Acquisition Patron Requests') %]</span>
+  </div>
+</div>
+
+<div>
+  <div class="form-group">
+    <div class="row">
+      <span ng-hide="context_user || context_lineitem">
+        <label for="select-request-ou">[% l('Patron Home Library: ' ) %]</label>
+        <eg-org-selector id="select-request-ou" selected="context_ou"></eg-org-selector>
+        <span>&nbsp;</span>
+      </span>
+      <span ng-show="context_user">[% l('User ID: [_1]','{{context_user}}') %]</span>
+      <span ng-show="context_lineitem">[% l('PO Line Item ID: [_1]','{{context_lineitem}}') %]</span>
+    </div>
+  </div>
+</div>
+
+<hr/>
+
+<eg-grid
+  id-field="id"
+  idl-class="aurs"
+  features="-sort,-multisort"
+  grid-controls="grid_controls"
+  persist-key="acq.requests.list"
+  dateformat="{{$root.egDateAndTimeFormat}}">
+
+  <eg-grid-menu-item handler="create_request"
+    label="[% l('Create Request') %]"></eg-grid-menu-item>
+
+  <eg-grid-menu-item handler="canceled_requests_checkbox_handler"
+    label="[% l('Show Canceled Requests') %]"
+    checkbox="requests_show_canceled"
+    checked="requests_show_canceled"/>
+
+  <eg-grid-menu-item handler="clear_requests" disabled="need_one_and_all_uncanceled"
+    label="[% l('Clear Completed Requests') %]"></eg-grid-menu-item>
+
+  <eg-grid-action handler="edit_request" disabled="need_one_uncanceled"
+    label="[% l('Edit Request') %]"></eg-grid-action>
+  <eg-grid-action handler="view_request" disabled="need_one_selected"
+    label="[% l('View Request') %]"></eg-grid-action>
+  <eg-grid-action handler="retrieve_user" disabled="need_one_selected"
+    label="[% l('Retrieve Patron') %]"></eg-grid-action>
+  <eg-grid-action handler="add_request_to_picklist" disabled="need_one_uncanceled_no_lineitem"
+    label="[% l('Add Request to Selection List') %]"></eg-grid-action>
+  <eg-grid-action handler="view_picklist" disabled="need_one_lineitem"
+    label="[% l('View Selection List') %]"></eg-grid-action>
+  <eg-grid-action handler="set_yes_hold_requests" disabled="need_one_and_all_new_or_pending"
+    label="[% l('Set Hold on Requests') %]"></eg-grid-action>
+  <eg-grid-action handler="set_no_hold_requests" disabled="need_one_and_all_new_or_pending"
+    label="[% l('Set No Hold on Requests') %]"></eg-grid-action>
+  <eg-grid-action handler="cancel_requests" disabled="need_one_and_all_uncanceled"
+    label="[% l('Cancel Requests') %]"></eg-grid-action>
+
+  <eg-grid-field path='id' hidden required sortable></eg-grid-field>
+  <eg-grid-field path='request_status.label' sortable label="[% l('Request Status') %]"></eg-grid-field>
+  <eg-grid-field path='request_status.id' required hidden sortable label="[% l('Request Status ID') %]"></eg-grid-field>
+  <eg-grid-field path='request_date' sortable label="[% l('Request Date/Time') %]"
+    datatype="timestamp"></eg-grid-field>
+  <eg-grid-field path='need_before' sortable label="[% l('Need Before Date/Time') %]"
+    datatype="timestamp"></eg-grid-field>
+  <eg-grid-field path='request_type.label' required sortable label="[% l('Request Type') %]"></eg-grid-field>
+  <eg-grid-field path='hold' sortable></eg-grid-field>
+  <eg-grid-field path='pickup_lib.shortname' required sortable label="[% l('Pickup Lib') %]"></eg-grid-field>
+  <eg-grid-field path='isxn' sortable></eg-grid-field>
+  <eg-grid-field path='upc' sortable></eg-grid-field>
+  <eg-grid-field path='title' sortable></eg-grid-field>
+  <eg-grid-field path='article_title' sortable></eg-grid-field>
+  <eg-grid-field path='lineitem.id' required sortable label="[% l('Lineitem ID') %]" hidden></eg-grid-field>
+  <eg-grid-field path='lineitem.picklist' sortable required label="[% l('Selection List ID') %]" hidden></eg-grid-field>
+  <eg-grid-field path='usr.id' required sortable label="[% l('User ID') %]" hidden></eg-grid-field>
+  <eg-grid-field path='usr.card.barcode' sortable required label="[% l('User Barcode') %]"></eg-grid-field>
+  <eg-grid-field path='usr.family_name' sortable required label="[% l('User Family Name') %]" hidden></eg-grid-field>
+  <eg-grid-field path='usr.home_ou.shortname' required sortable label="[% l('User Home Library') %]" hidden></eg-grid-field>
+  <eg-grid-field path='cancel_reason.label' sortable required label="[% l('Cancel Reason') %]" hidden></eg-grid-field>
+  <eg-grid-field path='*' required hidden></eg-grid-field>
+</eg-grid>
+
diff --git a/Open-ILS/src/templates/staff/acq/requests/t_set_no_hold.tt2 b/Open-ILS/src/templates/staff/acq/requests/t_set_no_hold.tt2
new file mode 100644
index 0000000000..77c5d4e39c
--- /dev/null
+++ b/Open-ILS/src/templates/staff/acq/requests/t_set_no_hold.tt2
@@ -0,0 +1,25 @@
+[% ctx.page_title = l('Set "No Hold" on Selected Patron Requests'); %]
+<!-- use <form> so we get submit-on-enter for free -->
+<form class="form-validated" novalidate name="form" ng-submit="ok(true)">
+    <div> <!-- modal-content -->
+        <div class="modal-header">
+            <button type="button" class="close" ng-click="cancel()"
+                aria-hidden="true">&times;</button>
+            <h4 class="modal-title">
+                [% l('Set "No Hold" on Selected Patron Requests') %]</h4>
+        </div>
+        <div class="modal-body">
+            <div class="form-group">
+                <label for="ids">[% l('Request IDs') %]</label>
+                <input type="text" class="form-control"
+                    id="ids" ng-model="ids" ng-disabled="true"/>
+            </div>
+        </div>
+        <div class="modal-footer">
+            <input type="submit" ng-disabled="form.$invalid"
+                class="btn btn-primary" value="[% l('Update Requests') %]"/>
+            <button class="btn btn-warning"
+                ng-click="cancel()">[% l('Abort Update') %]</button>
+        </div>
+    </div> <!-- modal-content -->
+</form>
diff --git a/Open-ILS/src/templates/staff/acq/requests/t_set_yes_hold.tt2 b/Open-ILS/src/templates/staff/acq/requests/t_set_yes_hold.tt2
new file mode 100644
index 0000000000..45acd4ef1f
--- /dev/null
+++ b/Open-ILS/src/templates/staff/acq/requests/t_set_yes_hold.tt2
@@ -0,0 +1,25 @@
+[% ctx.page_title = l('Set "Hold" on Selected Patron Requests'); %]
+<!-- use <form> so we get submit-on-enter for free -->
+<form class="form-validated" novalidate name="form" ng-submit="ok(true)">
+    <div> <!-- modal-content -->
+        <div class="modal-header">
+            <button type="button" class="close" ng-click="cancel()"
+                aria-hidden="true">&times;</button>
+            <h4 class="modal-title">
+                [% l('Set "Hold" on Selected Patron Requests') %]</h4>
+        </div>
+        <div class="modal-body">
+            <div class="form-group">
+                <label for="ids">[% l('Request IDs') %]</label>
+                <input type="text" class="form-control"
+                    id="ids" ng-model="ids" ng-disabled="true"/>
+            </div>
+        </div>
+        <div class="modal-footer">
+            <input type="submit" ng-disabled="form.$invalid"
+                class="btn btn-primary" value="[% l('Update Requests') %]"/>
+            <button class="btn btn-warning"
+                ng-click="cancel()">[% l('Abort Update') %]</button>
+        </div>
+    </div> <!-- modal-content -->
+</form>
diff --git a/Open-ILS/src/templates/staff/circ/patron/index.tt2 b/Open-ILS/src/templates/staff/circ/patron/index.tt2
index 6724ca45d2..5ebbe0eb9b 100644
--- a/Open-ILS/src/templates/staff/circ/patron/index.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/index.tt2
@@ -209,6 +209,11 @@ angular.module('egCoreMod').run(['egStrings', function(s) {
             </a>
           </li>
           <li>
+            <a href="./acq/requests/user/{{patron().id()}}" target="_top">
+              [% l('Acquisition Patron Requests') %]
+            </a>
+          </li>
+          <li>
             <a href="./booking/legacy/booking/reservation?patron_barcode={{patron().card().barcode()}}" target="_top">
               [% l('Booking: Create or Cancel Reservations') %]
             </a>
diff --git a/Open-ILS/src/templates/staff/navbar.tt2 b/Open-ILS/src/templates/staff/navbar.tt2
index d3033d7c7b..a5d988839a 100644
--- a/Open-ILS/src/templates/staff/navbar.tt2
+++ b/Open-ILS/src/templates/staff/navbar.tt2
@@ -359,7 +359,7 @@
             </a>
           </li>
           <li>
-            <a href="./acq/legacy/picklist/user_request" target="_self">
+            <a href="./acq/requests/list" target="_self">
               <span class="glyphicon glyphicon-thumbs-up"></span>
               [% l('Patron Requests') %]
             </a>
diff --git a/Open-ILS/web/js/ui/default/acq/common/li_table.js b/Open-ILS/web/js/ui/default/acq/common/li_table.js
index 40347fc849..8c59f4e7c3 100644
--- a/Open-ILS/web/js/ui/default/acq/common/li_table.js
+++ b/Open-ILS/web/js/ui/default/acq/common/li_table.js
@@ -787,9 +787,15 @@ function AcqLiTable() {
             oilsBasePath + "/acq/lineitem/worksheet/" + li.id() + 
             '?source=' + encodeURIComponent(location.pathname + location.search)
 
-        nodeByName("show_requests_link", row).href =
-            oilsBasePath + "/acq/picklist/user_request?lineitem=" + li.id() + 
-            '?source=' + encodeURIComponent(location.pathname + location.search)
+        if (!IAMBROWSER) {
+            nodeByName("show_requests_link", row).href =
+                oilsBasePath + "/acq/picklist/user_request?lineitem=" + li.id() +
+                '?source=' + encodeURIComponent(location.pathname + location.search);
+        } else {
+            nodeByName("show_requests_link", row).href =
+                "/eg/staff/acq/requests/lineitem/" + li.id();
+            nodeByName("show_requests_link", row).setAttribute('target','_top');
+        }
 
         dojo.query('[attr=title]', row)[0].onclick = function() {self.drawInfo(li.id())};
         dojo.query('[name=copieslink]', row)[0].onclick = function() {self.drawCopies(li.id())};
diff --git a/Open-ILS/web/js/ui/default/acq/picklist/brief_record.js b/Open-ILS/web/js/ui/default/acq/picklist/brief_record.js
index f59b93b157..93d2a3f237 100644
--- a/Open-ILS/web/js/ui/default/acq/picklist/brief_record.js
+++ b/Open-ILS/web/js/ui/default/acq/picklist/brief_record.js
@@ -223,7 +223,11 @@ function compileBriefRecord(fields, editMarc) {
                     pcrud.update( aur_obj, {
                         'oncomplete' : function(r, cudResults) {
                             // Goes back to the list view
-                            location.href = oilsBasePath + '/acq/picklist/user_request';
+                            if (!window.IAMBROWSER) {
+                                location.href = oilsBasePath + '/acq/picklist/user_request';
+                            } else {
+                                window.top.location.href = '/eg/staff/acq/requests/list';
+                            }
                         }
                     });
                 } else {
diff --git a/Open-ILS/web/js/ui/default/staff/acq/requests/list.js b/Open-ILS/web/js/ui/default/staff/acq/requests/list.js
new file mode 100644
index 0000000000..0191ef30d0
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/staff/acq/requests/list.js
@@ -0,0 +1,239 @@
+angular.module('egAcqRequestsApp',
+    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUserMod', 'egUiMod', 'egGridMod'])
+
+.config(function($routeProvider, $locationProvider, $compileProvider) {
+    $locationProvider.html5Mode(true);
+    // grid export
+    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|blob):/);
+
+    var resolver = {delay :
+        ['egStartup', function(egStartup) {return egStartup.go()}]}
+
+    $routeProvider.when('/acq/requests/list', {
+        templateUrl: './acq/requests/t_list',
+        controller: 'AcqRequestsCtrl',
+        resolve : resolver
+    });
+
+    $routeProvider.when('/acq/requests/user/:user', {
+        templateUrl: './acq/requests/t_list',
+        controller: 'AcqRequestsCtrl',
+        resolve : resolver
+    });
+
+    $routeProvider.when('/acq/requests/lineitem/:lineitem', {
+        templateUrl: './acq/requests/t_list',
+        controller: 'AcqRequestsCtrl',
+        resolve : resolver
+    });
+
+    $routeProvider.otherwise({redirectTo : '/acq/requests/list'});
+})
+
+.controller('AcqRequestsCtrl',
+       ['$scope','$q','$routeParams','$window','egCore','egAcqRequests','egUser',
+        'egGridDataProvider','$uibModal','$timeout',
+function($scope , $q , $routeParams , $window , egCore , egAcqRequests , egUser ,
+         egGridDataProvider , $uibModal , $timeout) {
+
+    var cancel_age;
+    var cancel_count;
+    $scope.context_user = $routeParams.user;
+    $scope.context_lineitem = $routeParams.lineitem;
+
+    egCore.startup.go().then(function() {
+        // org settings for constraining display of canceled requests
+        egCore.org.settings([
+            'circ.holds.canceled.display_age',
+            'circ.holds.canceled.display_count' // FIXME Don't know how to use this with egGrid
+        ]).then(function(set) {
+            cancel_age = set['circ.holds.canceled.display_age'];
+            cancel_count = set['circ.holds.canceled.display_count'];
+            if (!cancel_age && !cancel_count) {
+                cancel_count = 10; // default to last 10 canceled requests
+            }
+        });
+    });
+
+    $scope.need_one_selected = function() {
+        var requests = $scope.grid_controls.selectedItems();
+        if (requests.length == 1) return false;
+        return true;
+    }
+
+    $scope.need_one_uncanceled = function() {
+        var requests = $scope.grid_controls.selectedItems();
+        if (requests.length == 1) {
+            return requests[0]['cancel_reason.label'] ? true : false;
+        }
+        return true;
+    }
+
+    $scope.need_one_lineitem = function() {
+        var requests = $scope.grid_controls.selectedItems();
+        if (requests.length == 1) {
+            return ! requests[0]['lineitem.id'];
+        }
+        return true;
+    }
+
+    $scope.need_one_uncanceled_no_lineitem = function() {
+        var requests = $scope.grid_controls.selectedItems();
+        if (requests.length == 1) {
+            if (! requests[0]['lineitem.id']) {
+                return requests[0]['cancel_reason.label'] ? true : false;
+            }
+        }
+        return true;
+    }
+
+    $scope.need_one_and_all_uncanceled = function() {
+        var requests = $scope.grid_controls.selectedItems();
+        if (requests.length == 0) return true;
+        var found_canceled = false;
+        angular.forEach(requests,function(v,k) {
+            if (v['cancel_reason.label']) { found_canceled = true; }
+        });
+        return found_canceled;
+    }
+
+    $scope.need_one_and_all_new_or_pending = function() {
+        var requests = $scope.grid_controls.selectedItems();
+        if (requests.length == 0) return true;
+        var found_bad = false;
+        angular.forEach(requests,function(v,k) {
+            if (v['request_status.id'] != 2         // Pending
+                && v['request_status.id'] != 1) {   // New
+                found_bad = true;
+            }
+        });
+        return found_bad;
+    }
+
+    $scope.create_request = function(rows) {
+        var row = {};
+        if ($scope.context_user) {
+            row.usr = $scope.context_user;
+        }
+        egAcqRequests.handle_request(row,'create',$scope.context_ou,refresh_page);
+    }
+
+    $scope.edit_request = function(rows) {
+        if (!rows) return;
+        if (!angular.isArray(rows)) rows = [rows];
+        if (rows.length == 0) return;
+        egAcqRequests.handle_request(rows[0],'edit',$scope.context_ou,refresh_page);
+    }
+
+    $scope.view_request = function(rows) {
+        if (!rows) return;
+        if (!angular.isArray(rows)) rows = [rows];
+        if (rows.length == 0) return;
+        egAcqRequests.handle_request(rows[0],'view',$scope.context_ou,refresh_page);
+    }
+
+    $scope.add_request_to_picklist = function(rows) {
+        if (!rows) return;
+        if (!angular.isArray(rows)) rows = [rows];
+        if (rows.length == 0) return;
+        egAcqRequests.add_request_to_picklist(rows[0]);
+    }
+
+    $scope.view_picklist = function(rows) {
+        if (!rows) return;
+        if (!angular.isArray(rows)) rows = [rows];
+        if (rows.length == 0) return;
+        egAcqRequests.view_picklist(rows[0]);
+    }
+
+    $scope.retrieve_user = function(rows) {
+        if (!rows) return;
+        if (!angular.isArray(rows)) rows = [rows];
+        if (rows.length == 0) return;
+        location.href = "/eg/staff/circ/patron/" + rows[0]['usr.id'] + "/checkout";
+    }
+
+    $scope.clear_requests = function(rows) {
+        rows = $scope.grid_controls.selectedItems(); // remove this if we move the grid action into the menu
+        if (!rows) return;
+        if (!angular.isArray(rows)) rows = [rows];
+        if (rows.length == 0) return;
+        egAcqRequests.clear_requests( rows, refresh_page );
+    }
+
+    $scope.set_no_hold_requests = function(rows) {
+        if (!rows) return;
+        if (!angular.isArray(rows)) rows = [rows];
+        if (rows.length == 0) return;
+        egAcqRequests.set_no_hold_requests( rows, refresh_page );
+    }
+
+    $scope.set_yes_hold_requests = function(rows) {
+        if (!rows) return;
+        if (!angular.isArray(rows)) rows = [rows];
+        if (rows.length == 0) return;
+        egAcqRequests.set_yes_hold_requests( rows, refresh_page );
+    }
+
+    $scope.cancel_requests = function(rows) {
+        if (!rows) return;
+        if (!angular.isArray(rows)) rows = [rows];
+        if (rows.length == 0) return;
+        egAcqRequests.cancel_requests( rows, refresh_page );
+    }
+
+    $scope.canceled_requests_checkbox_handler = function (item) {
+        $scope.canceled_requests_cb_changed(item.checkbox,item.checked);
+    }
+
+    $scope.canceled_requests_cb_changed = function(cb,newVal,norefresh) {
+        $scope[cb] = newVal;
+        egCore.hatch.setItem('eg.acq.' + cb, newVal);
+        if (!norefresh) {
+            refresh_page();
+        }
+    }
+
+    function current_query() {
+        var filter = {}
+        if ($scope.context_user) {
+            filter.usr = $scope.context_user;
+        } else if ($scope.context_lineitem)  {
+            filter.lineitem = $scope.context_lineitem;
+        } else {
+            filter.home_ou = egCore.org.descendants($scope.context_ou.id(), true)
+        }
+        if ($scope['requests_show_canceled']) {
+            filter.cancel_reason = { '!=' : null };
+            if (cancel_age) {
+                var seconds = egCore.date.intervalToSeconds(cancel_age);
+                var now_epoch = new Date().getTime();
+                var cancel_date = new Date(
+                    now_epoch - (seconds * 1000 /* milliseconds */)
+                );
+                filter.cancel_time = { '>=' : cancel_date.toISOString() };
+            }
+
+        } else {
+            filter.cancel_reason = { '=' : null };
+        }
+        return filter;
+    }
+
+    $scope.grid_controls = {
+        activateItem : $scope.view_request,
+        setQuery : current_query
+    }
+
+    function refresh_page() {
+        $scope.grid_controls.setQuery(current_query());
+        $scope.grid_controls.refresh();
+    }
+
+    $scope.context_ou = egCore.org.get(egCore.auth.user().ws_ou());
+    $scope.$watch('context_ou', function(newVal, oldVal) {
+        if (newVal && newVal != oldVal) refresh_page();
+    });
+
+}])
+
diff --git a/Open-ILS/web/js/ui/default/staff/acq/services/requests.js b/Open-ILS/web/js/ui/default/staff/acq/services/requests.js
new file mode 100644
index 0000000000..013b083598
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/staff/acq/services/requests.js
@@ -0,0 +1,582 @@
+/**
+ * AcqRequests, yo
+ */
+
+angular.module('egCoreMod')
+
+.factory('egAcqRequests',
+
+       ['$uibModal','$q','egCore','egOrg','ngToast',
+function($uibModal , $q , egCore , egOrg , ngToast) {
+
+    var service = {};
+
+    var aur_fleshing = {
+
+        flesh : 2,
+        // aur   ->  cancel_reason
+        // aur   ->  lineitem
+        // aur   ->  pickup_lib
+        // aur   ->  request_type
+        // aur   ->  usr
+        // aur   ->  usr            -> card
+
+        flesh_fields : {
+             'aur' : [
+                 'cancel_reason'
+                ,'lineitem'
+                ,'pickup_lib'
+                ,'request_type'
+                ,'usr'
+            ]
+            ,'au'  : [
+                 'card'
+                ,'home_ou'
+                ,'mailing_address'
+                ,'billing_address'
+                ,'settings'
+            ]
+        }
+    };
+
+    var aurs_fleshing = {
+
+        flesh : 2,
+        // aurs   ->  cancel_reason
+        // aurs   ->  lineitem
+        // aurs   ->  pickup_lib
+        // aurs   ->  request_type
+        // aurs   ->  request_status
+        // aurs   ->  usr
+        // aurs   ->  usr            -> card
+
+        flesh_fields : {
+             'aurs' : [
+                 'cancel_reason'
+                ,'lineitem'
+                ,'pickup_lib'
+                ,'request_type'
+                ,'request_status'
+                ,'usr'
+            ]
+            ,'au'  : [
+                 'card'
+                ,'home_ou'
+                ,'mailing_address'
+                ,'billing_address'
+                ,'settings'
+            ]
+        }
+    };
+
+    service.aur_fleshing = function(newvalue) {
+        if (newvalue) {
+            aur_fleshing = newvalue;
+        }
+        return angular.copy(aur_fleshing);
+    }
+
+    service.aurs_fleshing = function(newvalue) {
+        if (newvalue) {
+            aurs_fleshing = newvalue;
+        }
+        return angular.copy(aurs_fleshing);
+    }
+
+    service.fetch_request = function(aur_id) {
+        var deferred = $q.defer();
+        egCore.pcrud.search(
+            'aur', { id : aur_id }, aur_fleshing, { atomic : true, authoritative : true }
+        ).then(function(requests) {
+            deferred.resolve(requests[0]);
+        });
+        return deferred.promise;
+    }
+
+    service.fetch_request_with_status = function(aur_id) {
+        var deferred = $q.defer();
+        egCore.pcrud.search(
+            'aurs', { id : aur_id }, aurs_fleshing, { atomic : true, authoritative : true }
+        ).then(function(requests) {
+            deferred.resolve(requests[0]);
+        });
+        return deferred.promise;
+    }
+
+    service.fetch_cancel_reasons = function() {
+        var deferred = $q.defer();
+        egCore.pcrud.retrieveAll(
+            'acqcr', {}, {atomic : true, authoritative : true}
+        ).then(function(cancel_reasons) {
+            deferred.resolve(cancel_reasons);
+        });
+        return deferred.promise;
+    }
+
+    service.fetch_request_types = function() {
+        var deferred = $q.defer();
+        egCore.pcrud.retrieveAll(
+            'aurt', {}, {atomic : true, authoritative : true}
+        ).then(function(request_types) {
+            deferred.resolve(request_types);
+        });
+        return deferred.promise;
+    }
+
+    service.fetch_request_status_types = function() {
+        var deferred = $q.defer();
+        egCore.pcrud.retrieveAll(
+            'aurst', {}, {atomic : true, authoritative : true}
+        ).then(function(request_status_types) {
+            deferred.resolve(request_status_types);
+        });
+        return deferred.promise;
+    }
+
+    service.add_request_to_picklist = function (row) {
+        egCore.pcrud.search('aurs', {
+                id : row['id']
+            }, aurs_fleshing, {
+                atomic : true
+            }
+        ).then(function(requests) {
+            var aur_obj = requests[0];
+            var prepop = { // based on acq.lineitem_marc_attr_definition
+                "1": [aur_obj.title(), aur_obj.article_title(), aur_obj.volume()].join(' '),
+                "2": aur_obj.author(),
+                "4": aur_obj.article_pages(),
+                "7": aur_obj.upc(),
+                "10": aur_obj.publisher(),
+                "11": aur_obj.pubdate()
+            }
+            if (aur_obj.request_type().id() == "2") { /* Articles */
+                prepop["6"] = aur_obj.isxn();
+            } else {
+                prepop["5"] = aur_obj.isxn();
+            }
+            location.href = "/eg/staff/acq/legacy/picklist/brief_record?ur="
+                + aur_obj.id() + "&prepop=" + encodeURIComponent(js2JSON(prepop));
+        });
+    }
+
+    service.view_picklist = function (row) {
+        location.href = "/eg/staff/acq/legacy/picklist/view/" + row['lineitem.picklist'];
+    }
+
+    service.handle_request = function(row,mode,context_ou,callback) {
+        if (mode!='create' && !row) { return; }
+        return $uibModal.open({
+            templateUrl: './acq/requests/t_edit',
+            backdrop: 'static',
+            controller: ['$scope',  '$uibModalInstance','egCore',
+                         'request_and_extra','request_types','request_status_types',
+                 function($m_scope , $uibModalInstance , egCore ,
+                          request_and_extra , request_types , request_status_types ) {
+                    var request = request_and_extra.request;
+                    var extra = request_and_extra.extra || {};
+                    var today = new Date();
+                    today.setHours(0);
+                    today.setMinutes(0);
+                    today.setSeconds(0);
+                    today.setMilliseconds(0);
+                    $m_scope.minDate = today;
+                    $m_scope.mode = mode;
+                    $m_scope.request = request;
+                    $m_scope.request_types = request_types;
+                    $m_scope.extra = extra;
+                    $m_scope.extra.user_obj = request.usr;
+                    angular.forEach(['hold', 'email_notify'], function(field) {
+                        if (request[field] == 't') {
+                            request[field] = true;
+                        } else if (request[field] == 'f' || typeof request[field] == 'undefined') {
+                            request[field] = false;
+                        }
+                    });
+                    if (request.request_type) {
+                        if (typeof request.request_type.id != 'undefined') {
+                            request.request_type = request.request_type.id;
+                        }
+                        angular.forEach(request_types,function(v,k) {
+                            if (v.id() == request.request_type) {
+                                $m_scope.extra.selected_request_type = v;
+                            }
+                        });
+                    }
+                    if (request.need_before) {
+                        request.need_before = new Date(request.need_before);
+                    }
+                    if (request.pickup_lib) {
+                        $m_scope.request.pickup_lib =
+                            egCore.idl.fromHash('aou',request.pickup_lib);
+                    } else {
+                        $m_scope.request.pickup_lib =
+                            egOrg.CanHaveVolumes(context_ou)
+                            ? context_ou
+                            : egOrg.get( egCore.auth.user().ws_ou() );
+                    }
+                    if (request.cancel_reason) {
+                        $m_scope.request.cancel_reason =
+                            egCore.idl.fromHash('acqcr',request.cancel_reason);
+                        $m_scope.mode = 'view'; // TODO: want explicit uncancel?
+                    }
+                    if (request.request_status && request.request_status.id != 1) { // New
+                        $m_scope.mode = 'view';
+                    }
+                    if (request.usr) {
+                        if (typeof request.usr.id != 'undefined') {
+                            $m_scope.extra.barcode = request.usr.card.barcode;
+                            request.usr = request.usr.id;
+                        }
+                    }
+                    $m_scope.cancel = function () {
+                        $uibModalInstance.dismiss('canceled');
+                    }
+                    $m_scope.ok = function(request2,extra2) {
+                        $uibModalInstance.close({
+                             'request':request2
+                            ,'extra':extra2
+                        });
+                    }
+                    $m_scope.model_has_changed = false;
+                    $m_scope.cant_have_vols = function (id) {
+                        return !egCore.org.CanHaveVolumes(id);
+                    }
+                    $m_scope.find_user = function () {
+
+                        $m_scope.request.usr = null;
+                        $m_scope.extra.user_obj = null;
+                        if (!$m_scope.extra.barcode) return;
+
+                        egCore.net.request(
+                            'open-ils.actor',
+                            'open-ils.actor.get_barcodes',
+                            egCore.auth.token(), egCore.auth.user().ws_ou(),
+                            'actor', $m_scope.extra.barcode)
+
+                        .then(function(resp) { // get_barcodes
+
+                            if (evt = egCore.evt.parse(resp)) {
+                                console.error(evt.toString());
+                                return;
+                            }
+
+                            if (!resp || !resp[0]) {
+                                $m_scope.request.usr = null;
+                                return;
+                            }
+
+                            egCore.pcrud.search('au', {
+                                    id : resp[0].id
+                                }, {
+                                    flesh : 1,
+                                    flesh_fields : {
+                                        'au'  : [
+                                             'card'
+                                            ,'home_ou'
+                                            ,'mailing_address'
+                                            ,'billing_address'
+                                            ,'settings'
+                                        ]
+                                    }
+                                },
+                                { atomic : true }
+                            ).then(function(users) {
+                                var usr = egCore.idl.toHash(users[0]);
+                                $m_scope.extra.user_obj = usr;
+                                $m_scope.request.usr = usr.id;
+                                $m_scope.request.pickup_lib = egOrg.get(usr.home_ou.id);
+                                $m_scope.request.phone_notify = usr.day_phone;
+                                angular.forEach(usr.settings, function(s) {
+                                    if (s.name == 'opac.hold_notify') {
+                                        if (s.value.match('phone')) {
+                                            $m_scope.extra.phone_notify = true;
+                                        }
+                                        if (s.value.match('email')) {
+                                            $m_scope.request.email_notify = true;
+                                        }
+                                    }
+                                    if (s.name == 'opac.default_phone') {
+                                        $m_scope.request.phone_notify = s.value.replace(/^"/,'').replace(/"$/,'');
+                                    }
+                                    if (s.name == 'opac.default_pickup_location') {
+                                        $m_scope.request.pickup_lib =
+                                            egOrg.get(s.value);
+                                    }
+                                });
+                                return $m_scope.request;
+                            });
+                        });
+                    }
+                    $m_scope.$watch("extra.barcode", function(newVal, oldVal) {
+                        if (newVal && newVal != oldVal) {
+                            $m_scope.find_user();
+                        }
+                    });
+                    $m_scope.$watch("extra.selected_request_type",
+                        function(newVal, oldVal) {
+                            if (newVal && newVal != oldVal) {
+                                $m_scope.request.request_type = newVal.id();
+                            }
+                        }
+                    );
+            }],
+            resolve : {
+                 request_and_extra : function() {
+                    if (mode=='create') {
+                        var aur_obj = egCore.idl.toHash(new egCore.idl.aurs());
+                        var extra = {};
+                        if (row['usr']) {
+                            return egCore.pcrud.search('au', {
+                                    id : row['usr']
+                                }, {
+                                    flesh : 1,
+                                    flesh_fields : {
+                                        'au'  : [
+                                             'card'
+                                            ,'home_ou'
+                                            ,'mailing_address'
+                                            ,'billing_address'
+                                            ,'settings'
+                                        ]
+                                    }
+                                },
+                                { atomic : true }
+                            ).then(function(users) {
+                                if (users.length > 0) {
+                                    var usr = egCore.idl.toHash(users[0]);
+                                    aur_obj.usr = usr.id;
+                                    aur_obj.pickup_lib = egCore.idl.toHash(
+                                        egOrg.get(usr.home_ou.id)
+                                    );
+                                    aur_obj.phone_notify = usr.day_phone;
+                                    angular.forEach(usr.settings, function(s) {
+                                        if (s.name == 'opac.hold_notify') {
+                                            if (s.value.match('phone')) {
+                                                extra.phone_notify = true;
+                                            }
+                                            if (s.value.match('email')) {
+                                                aur_obj.email_notify = true;
+                                            }
+                                        }
+                                        if (s.name == 'opac.default_phone') {
+                                            aur_obj.phone_notify = s.value.replace(/^"/,'').replace(/"$/,'');
+                                        }
+                                        if (s.name == 'opac.default_pickup_location') {
+                                            aur_obj.pickup_lib = egCore.idl.toHash(
+                                                egOrg.get(s.value)
+                                            );
+                                        }
+                                    });
+                                }
+                                return { 'request' : aur_obj, 'extra' : extra };
+                            });
+                        } else {
+                            console.log('here');
+                            return { 'request' : aur_obj, 'extra': extra };
+                        }
+                    } else {
+                        return egCore.pcrud.search('aurs', {
+                                id : row['id']
+                            }, aurs_fleshing, {
+                                atomic : true
+                            }
+                        ).then(function(requests) {
+                            var aur_obj = egCore.idl.toHash(requests[0]);
+                            var extra = {};
+                            if (aur_obj.phone_notify) {
+                                extra.phone_notify = true;
+                            }
+                            return { 'request' : aur_obj, 'extra' : extra };
+                        });
+                    }
+                }
+                ,request_types : function() {
+                    return service.fetch_request_types();
+                }
+                ,request_status_types : function() {
+                    return service.fetch_request_status_types();
+                }
+            }
+        }).result.then(function(data) {
+            delete data.request.request_status;
+            delete data.request.home_ou;
+            var aur_obj = new egCore.idl.fromHash('aur',data.request);
+            if (aur_obj.need_before() && typeof aur_obj.need_before() == 'object') {
+                aur_obj.need_before( aur_obj.need_before().toISOString() );
+            }
+            if (!data.extra.phone_notify) {
+                aur_obj.phone_notify(null);
+            }
+            if (mode=='create') {
+                aur_obj.isnew('t');
+                aur_obj.pickup_lib( aur_obj.pickup_lib().id() );
+                return egCore.net.request(
+                    'open-ils.acq',
+                    'open-ils.acq.user_request.create',
+                    egCore.auth.token(), egCore.idl.toHash(aur_obj)
+                ).then(function(resp) {
+                    var evt = egCore.evt.parse(resp);
+                    if (evt) {
+                        ngToast.danger(egCore.strings.CREATE_USER_REQUEST_FAIL + ' : ' + evt.desc);
+                    } else {
+                        ngToast.success(egCore.strings.CREATE_USER_REQUEST_SUCCESS);
+                    }
+                    callback(resp);
+                });
+            } else {
+                aur_obj.ischanged('t');
+                return egCore.pcrud.apply(aur_obj).then(function(resp) {
+                    var evt = egCore.evt.parse(resp);
+                    if (evt) {
+                        ngToast.danger(egCore.strings.EDIT_USER_REQUEST_FAIL + ' : ' + evt.desc);
+                    } else {
+                        ngToast.success(egCore.strings.EDIT_USER_REQUEST_SUCCESS);
+                    }
+                    callback(resp);
+                });
+            }
+        }).catch(function(e) {
+            console.log('caught',e);
+        });
+    }
+
+    service.set_no_hold_requests = function(rows,callback) {
+        var ids = rows.map(function(v,i,a) {
+            return v.id;
+        });
+        return $uibModal.open({
+            templateUrl: './acq/requests/t_set_no_hold',
+            backdrop: 'static',
+            controller: ['$scope',  '$uibModalInstance','egCore',
+                 function($m_scope , $uibModalInstance , egCore ) {
+                    $m_scope.ids = ids;
+                    $m_scope.cancel = function () {
+                        $uibModalInstance.dismiss('canceled');
+                    }
+                    $m_scope.ok = function(doit) {
+                        $uibModalInstance.close(doit);
+                    }
+            }],
+            resolve : {}
+        }).result.then(function(cancel_reason) {
+            return egCore.net.request(
+                'open-ils.acq',
+                'open-ils.acq.user_request.set_no_hold.batch',
+                egCore.auth.token(), ids
+            ).then(function(obj) {
+                if (callback) {
+                    callback(obj);
+                }
+            });
+        }).catch(function(e) {
+            console.log('caught',e);
+        });
+    }
+
+    service.set_yes_hold_requests = function(rows,callback) {
+        var ids = rows.map(function(v,i,a) {
+            return v.id;
+        });
+        return $uibModal.open({
+            templateUrl: './acq/requests/t_set_yes_hold',
+            backdrop: 'static',
+            controller: ['$scope',  '$uibModalInstance','egCore',
+                 function($m_scope , $uibModalInstance , egCore ) {
+                    $m_scope.ids = ids;
+                    $m_scope.cancel = function () {
+                        $uibModalInstance.dismiss('canceled');
+                    }
+                    $m_scope.ok = function(doit) {
+                        $uibModalInstance.close(doit);
+                    }
+            }],
+            resolve : {}
+        }).result.then(function(cancel_reason) {
+            return egCore.net.request(
+                'open-ils.acq',
+                'open-ils.acq.user_request.set_yes_hold.batch',
+                egCore.auth.token(), ids
+            ).then(function(obj) {
+                if (callback) {
+                    callback(obj);
+                }
+            });
+        }).catch(function(e) {
+            console.log('caught',e);
+        });
+    }
+
+    service.cancel_requests = function(rows,callback) {
+        var ids = rows.map(function(v,i,a) {
+            return v.id;
+        });
+        return $uibModal.open({
+            templateUrl: './acq/requests/t_cancel',
+            backdrop: 'static',
+            controller: ['$scope',  '$uibModalInstance','egCore','cancel_reasons',
+                 function($m_scope , $uibModalInstance , egCore , cancel_reasons ) {
+                    $m_scope.ids = ids;
+                    $m_scope.cancel_reasons = cancel_reasons;
+                    $m_scope.cancel = function () {
+                        $uibModalInstance.dismiss('canceled');
+                    }
+                    $m_scope.ok = function(cancel_reason) {
+                        $uibModalInstance.close(cancel_reason);
+                    }
+            }],
+            resolve : {
+                cancel_reasons : function() {
+                    return service.fetch_cancel_reasons();
+                }
+            }
+        }).result.then(function(cancel_reason) {
+            return egCore.net.request(
+                'open-ils.acq',
+                'open-ils.acq.user_request.cancel.batch.atomic',
+                egCore.auth.token(), ids, cancel_reason.id()
+            ).then(function(obj) {
+                if (callback) {
+                    callback(obj);
+                }
+            });
+        }).catch(function(e) {
+            console.log('caught',e);
+        });
+    }
+
+    service.clear_requests = function(rows,callback) {
+        var ids = rows.map(function(v,i,a) {
+            return v.id;
+        });
+        return $uibModal.open({
+            templateUrl: './acq/requests/t_clear',
+            backdrop: 'static',
+            controller: ['$scope',  '$uibModalInstance','egCore',
+                 function($m_scope , $uibModalInstance , egCore) {
+                    $m_scope.ids = ids;
+                    $m_scope.cancel = function () {
+                        $uibModalInstance.dismiss('canceled');
+                    }
+                    $m_scope.ok = function(cancel_reason) {
+                        $uibModalInstance.close(true);
+                    }
+            }],
+            resolve : {}
+        }).result.then(function(doit) {
+            return egCore.net.request(
+                'open-ils.acq',
+                'open-ils.acq.clear_completed_user_requests',
+                egCore.auth.token(), ids
+            ).then(function(obj) {
+                if (callback) {
+                    callback(obj);
+                }
+            });
+        }).catch(function(e) {
+            console.log('caught',e);
+        });
+    }
+
+    return service;
+}])
+;
-- 
2.11.0