[open-ils-commits] [GIT] Evergreen ILS branch master updated. cb8a47c99b93ff479b40eeba6ac24af4e2c4157e

Evergreen Git git at git.evergreen-ils.org
Tue Sep 4 21:00:08 EDT 2018


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  cb8a47c99b93ff479b40eeba6ac24af4e2c4157e (commit)
       via  df507ca0fef135133221499c6c338621be16418f (commit)
       via  e9a9875da27e85e8437c082b6e40ad39ff9a1ba6 (commit)
      from  c68550260497e050307b49b0743e339f96417b53 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit cb8a47c99b93ff479b40eeba6ac24af4e2c4157e
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Sep 4 17:37:32 2018 -0400

    LP#1774277 Stamping upgrade for patron acq reqs
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index c7771c0..c80ab9c 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -92,7 +92,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1126', :eg_version); -- berick/gmcharlt
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1127', :eg_version); -- phasefx/berick
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.schema.acq.patron_requests.sql b/Open-ILS/src/sql/Pg/upgrade/1127.data.schema.acq.patron_requests.sql
similarity index 97%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.data.schema.acq.patron_requests.sql
rename to Open-ILS/src/sql/Pg/upgrade/1127.data.schema.acq.patron_requests.sql
index 5035008..936c31c 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.schema.acq.patron_requests.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1127.data.schema.acq.patron_requests.sql
@@ -1,6 +1,6 @@
 BEGIN;
 
---SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('1127', :eg_version);
 
 ALTER TABLE acq.user_request ADD COLUMN cancel_time TIMESTAMPTZ;
 ALTER TABLE acq.user_request ADD COLUMN upc TEXT;

commit df507ca0fef135133221499c6c338621be16418f
Author: Jason Etheridge <jason at EquinoxInitiative.org>
Date:   Thu Aug 23 15:18:05 2018 -0400

    lp1774277 release notes for patron acq requests
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/docs/RELEASE_NOTES_NEXT/Acquisitions/PatronRequests.adoc b/docs/RELEASE_NOTES_NEXT/Acquisitions/PatronRequests.adoc
new file mode 100644
index 0000000..043ccac
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Acquisitions/PatronRequests.adoc
@@ -0,0 +1,6 @@
+
+Patron Acquisitions Requests
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The existing interface for staff-mediated patron acquisition requests has been replaced in the web staff client with a re-implementation written in AngularJS, with some minor bug fixes (including access from the Patron interface) and other improvements.
+

commit e9a9875da27e85e8437c082b6e40ad39ff9a1ba6
Author: Jason Etheridge <jason at EquinoxInitiative.org>
Date:   Mon Mar 12 18:02:47 2018 -0400

    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 at EquinoxInitiative.org>
    
    toward acq requests
    
    Signed-off-by: Jason Etheridge <jason at 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 at 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 at 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 at 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 at 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 at 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 at 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 at EquinoxInitiative.org>
    
    handle undefined values for email/hold checkboxes
    
    Signed-off-by: Jason Etheridge <jason at 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 at EquinoxInitiative.org>
    
    match pcrud perm for aur with aurs
    
    Signed-off-by: Jason Etheridge <jason at EquinoxInitiative.org>
    
    live_t/ test
    
    Signed-off-by: Jason Etheridge <jason at EquinoxInitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 300c910..4a10387 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 42fbebb..feacb2f 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 0000000..2fc3d89
--- /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 c820e74..68efce9 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 d9e75eb..e0e02c6 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 0000000..5035008
--- /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 0000000..c6ae3d3
--- /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 0000000..ba1db9d
--- /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">×</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 0000000..b15206a
--- /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">×</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 0000000..a62a228
--- /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">×</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 0000000..5c279b4
--- /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> </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 0000000..77c5d4e
--- /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">×</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 0000000..45acd4e
--- /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">×</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 6724ca4..5ebbe0e 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 d3033d7..a5d9888 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 40347fc..8c59f4e 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 f59b93b..93d2a3f 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 0000000..0191ef3
--- /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 0000000..013b083
--- /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;
+}])
+;

-----------------------------------------------------------------------

Summary of changes:
 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/002.schema.config.sql          |    2 +-
 Open-ILS/src/sql/Pg/200.schema.acq.sql             |   23 +-
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |   29 +-
 .../1127.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 ++++++++++++++++++++
 .../Acquisitions/PatronRequests.adoc               |    6 +
 21 files changed, 2000 insertions(+), 27 deletions(-)
 create mode 100644 Open-ILS/src/perlmods/live_t/22-acq-requests.t
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1127.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
 create mode 100644 docs/RELEASE_NOTES_NEXT/Acquisitions/PatronRequests.adoc


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list