lp1774277 Improvements to Patron Acquisition Request
authorJason Etheridge <jason@EquinoxInitiative.org>
Mon, 12 Mar 2018 22:02:47 +0000 (18:02 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 5 Sep 2018 00:20:14 +0000 (20:20 -0400)
Squashed and rebased against master, this is an Angular reimplementation of the
Patron Acquisition Request user interface with some improvements.  It still
reaches into the Dojo-based Acquisition interfaces.

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

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
4-status-not-updating-to-recieved-unless-all-items-in-order-are-recieved

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

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

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

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

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

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

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

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

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

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
6-upc-not-on-patron-request-form

Adds a UPC column to the patron acq request table

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
2-hold-request-fields-that-make-use-of-user-preferences

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

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
5-pick-up-library-not-defaulting-to-patrons-home-library

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

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

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
5-pick-up-library-not-defaulting-to-patrons-home-library

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

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

to the IDL and for the email_notify checkbox.

some refactoring to avoid using foreign fields in the request object

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
7-retrieve-patron-fails-to-load-patron-record

give the user_request.view permission some parity with VIEW_USER

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

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
handle undefined values for email/hold checkboxes

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
remove acq.holds.allow_holds_from_purchase_request

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

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
match pcrud perm for aur with aurs

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

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
Signed-off-by: Bill Erickson <berickxx@gmail.com>
19 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Order.pm
Open-ILS/src/perlmods/live_t/22-acq-requests.t [new file with mode: 0644]
Open-ILS/src/sql/Pg/200.schema.acq.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.schema.acq.patron_requests.sql [new file with mode: 0644]
Open-ILS/src/templates/staff/acq/requests/index.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/acq/requests/t_cancel.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/acq/requests/t_clear.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/acq/requests/t_edit.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/acq/requests/t_list.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/acq/requests/t_set_no_hold.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/acq/requests/t_set_yes_hold.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/circ/patron/index.tt2
Open-ILS/src/templates/staff/navbar.tt2
Open-ILS/web/js/ui/default/acq/common/li_table.js
Open-ILS/web/js/ui/default/acq/picklist/brief_record.js
Open-ILS/web/js/ui/default/staff/acq/requests/list.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/acq/services/requests.js [new file with mode: 0644]

index 300c910..4a10387 100644 (file)
@@ -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'/>
index 42fbebb..feacb2f 100644 (file)
@@ -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 (file)
index 0000000..2fc3d89
--- /dev/null
@@ -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');
+    }
+}
+
index c820e74..68efce9 100644 (file)
@@ -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
 
index d9e75eb..e0e02c6 100644 (file)
@@ -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 (file)
index 0000000..5035008
--- /dev/null
@@ -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 (file)
index 0000000..c6ae3d3
--- /dev/null
@@ -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 (file)
index 0000000..ba1db9d
--- /dev/null
@@ -0,0 +1,31 @@
+[% ctx.page_title = l("Cancel Selected Patron Requests"); %]
+<!-- use <form> so we get submit-on-enter for free -->
+<form class="form-validated" novalidate name="form" ng-submit="ok(cancel_reason)">
+    <div> <!-- modal-content -->
+        <div class="modal-header">
+            <button type="button" class="close" ng-click="cancel()"
+                aria-hidden="true">&times;</button>
+            <h4 class="modal-title">
+                [% l('Cancel Selected Patron Requests') %]</h4>
+        </div>
+        <div class="modal-body">
+            <div class="form-group">
+                <label for="ids">[% l('Request IDs') %]</label>
+                <input type="text" class="form-control"
+                    id="ids" ng-model="ids" ng-disabled="true"/>
+            </div>
+            <div class="form-group">
+                <label for="cancel-reason-selector">[% l('Cancel Reason') %]</label>
+                <select id="cancel-reason-selector" class="form-control" required
+                    ng-model="cancel_reason"
+                    ng-options="rt.label() for rt in cancel_reasons"/>
+            </div>
+        </div>
+        <div class="modal-footer">
+            <input type="submit" ng-disabled="form.$invalid"
+                class="btn btn-primary" value="[% l('Cancel Requests') %]"/>
+            <button class="btn btn-warning"
+                ng-click="cancel()">[% l('Abort Cancellation') %]</button>
+        </div>
+    </div> <!-- modal-content -->
+</form>
diff --git a/Open-ILS/src/templates/staff/acq/requests/t_clear.tt2 b/Open-ILS/src/templates/staff/acq/requests/t_clear.tt2
new file mode 100644 (file)
index 0000000..b15206a
--- /dev/null
@@ -0,0 +1,25 @@
+[% ctx.page_title = l("Clear Completed Patron Requests"); %]
+<!-- use <form> so we get submit-on-enter for free -->
+<form class="form-validated" novalidate name="form" ng-submit="ok(true)">
+    <div> <!-- modal-content -->
+        <div class="modal-header">
+            <button type="button" class="close" ng-click="cancel()"
+                aria-hidden="true">&times;</button>
+            <h4 class="modal-title">
+                [% l('Clear Completed Patron Requests') %]</h4>
+        </div>
+        <div class="modal-body">
+            <div class="form-group">
+                <label for="ids">[% l('Request IDs') %]</label>
+                <input type="text" class="form-control"
+                    id="ids" ng-model="ids" ng-disabled="true"/>
+            </div>
+        </div>
+        <div class="modal-footer">
+            <input type="submit" ng-disabled="form.$invalid"
+                class="btn btn-primary" value="[% l('Clear Requests') %]"/>
+            <button class="btn btn-warning"
+                ng-click="cancel()">[% l('Abort Clear Requests') %]</button>
+        </div>
+    </div> <!-- modal-content -->
+</form>
diff --git a/Open-ILS/src/templates/staff/acq/requests/t_edit.tt2 b/Open-ILS/src/templates/staff/acq/requests/t_edit.tt2
new file mode 100644 (file)
index 0000000..a62a228
--- /dev/null
@@ -0,0 +1,240 @@
+[% ctx.page_title = l("Create/Edit/View patron Request"); %]
+<!-- use <form> so we get submit-on-enter for free -->
+<form class="form-validated" novalidate name="form"
+      ng-submit="ok(request,extra)">
+    <div> <!-- modal-content -->
+        <div class="modal-header">
+            <button type="button" class="close" ng-click="cancel()"
+                aria-hidden="true">&times;</button>
+            <h4 ng-if="mode=='create'" class="modal-title">
+                [% l('Create Patron Request') %]</h4>
+            <h4 ng-if="mode=='edit'" class="modal-title">
+                [% l('Edit Patron Request') %]</h4>
+            <h4 ng-if="mode=='view'" class="modal-title">
+                [% l('View Patron Request') %]</h4>
+        </div>
+        <div class="modal-header">
+            <div class="row">
+                <div class="form-group col-sm-6">
+                    <label for="edit-request-usr">
+                        [% l('User Barcode') %]</label>
+                    <input type="text" ng-model="extra.barcode" id="barcode"
+                        class="form-control" focus-me="focusMe"
+                        ng-model-options="{ debounce: 1000 }"
+                        ng-disabled="mode=='view'"
+                        placeholder="[% l('Barcode...') %]"/>
+                    <span ng-show="extra.barcode && request.usr">
+                        [% l('[_1], [_2] [_3] : [_4]',
+                          '{{extra.user_obj.family_name}}'
+                          '{{extra.user_obj.first_given_name}}'
+                          '{{extra.user_obj.second_given_name}}'
+                          '{{extra.user_obj.home_ou.shortname}}') %]
+                    </span>
+                </div>
+                <div class="form-group col-sm-6">
+                    <label for="edit-request-usr">[% l('User ID') %]</label>
+                    <input type="text" class="form-control" focus-me='focusMe'
+                        id="edit-request-usr" ng-model="request.usr"
+                        required ng-disabled="true"/>
+                    <span class="alert-info pull-right"
+                        ng-show="extra.barcode && !request.usr">
+                        [% l('Not Found') %]
+                    </span>
+                </div>
+            </div>
+            <div class="form-group" ng-show="request.cancel_reason">
+                <label for="edit-request-id">[% l('Cancel Reason') %]</label>
+                <div class="form-control" ng-disabled="true">
+                    {{request.cancel_reason.label()}}
+                </div>
+            </div>
+            <div class="row">
+                <div class="form-group col-sm-6">
+                    <label>[% l('Request Date/Time') %]</label>
+                    <div class="form-control" ng-disabled="true">
+                        {{request.request_date | date:$root.egDateAndTimeFormat}}
+                    </div>
+                </div>
+                <div class="form-group col-sm-6">
+                    <label for="edit-request-need-before">
+                        [% l('Need Before Date/Time') %]</label>
+                    <eg-date-input id="edit-request-need-before"
+                        show-time-picker ng-disabled="mode=='view'"
+                        ng-model="request.need_before" min-date="minDate"/>
+                </div>
+            </div>
+            <div class="row" ng-show="mode=='view'">
+                <div class="form-group col-sm-6">
+                    <label for="edit-request-bib-record">
+                        [% l('Bib Record') %]</label>
+                    <input type="text" class="form-control" focus-me='focusMe'
+                        id="edit-request-bib-record" ng-disabled="true"
+                        ng-model="request.eg_bib"/>
+                </div>
+                <div class="form-group col-sm-6">
+                    <label for="edit-request-lineitem">
+                        [% l('PO Line Item') %]</label>
+                    <input type="text" class="form-control" focus-me='focusMe'
+                        id="edit-request-lineitem" ng-disabled="true"
+                        ng-model="request.lineitem.id"/>
+                </div>
+            </div>
+            <div class="row">
+                <div class="form-group col-sm-6">
+                    <label for="edit-request-place-hold">
+                        <input type="checkbox" id="edit-request-place-hold"
+                            ng-disabled="mode=='view'" ng-model="request.hold"/>
+                        [% l('Place Hold?') %]
+                    </label>
+                </div>
+                <div class="form-group col-sm-6">
+                    <label for="edit-request-pickup-lib">
+                        [% l('Pickup Library') %]</label>
+                    <eg-org-selector id="edit-request-pickup-lib"
+                        ng-hide="mode=='view'" selected="request.pickup_lib"
+                        disable-test="cant_have_vols"/>
+                    <span ng-show="mode=='view'">
+                        {{request.pickup_lib.shortname()}}
+                    </span>
+                </div>
+            </div>
+            <div class="row">
+                <div class="form-group col-sm-6">
+                    <label for="edit-request-email-notify">
+                        <input type="checkbox" id="edit-request-email-notify"
+                            ng-disabled="mode=='view'"
+                            ng-model="request.email_notify"/>
+                        [% l('Notify By Email When Hold Ready?') %]
+                    </label>
+                </div>
+                <div class="form-group col-sm-6">
+                    <label for="edit-extra-phone-notify">
+                        <input type="checkbox" id="edit-extra-phone-notify"
+                            ng-disabled="mode=='view'"
+                            ng-model="extra.phone_notify"/>
+                        [% l('Notify By Phone When Hold Ready?') %]
+                    </label>
+                    <input type="text" class="form-control" focus-me='focusMe'
+                        id="edit-request-phone-notify"
+                        ng-disabled="mode=='view'"
+                        ng-model="request.phone_notify"/>
+                </div>
+            </div>
+        </div>
+        <div class="modal-body">
+            <div class="row" ng-if="mode!='create'">
+                <div class="form-group col-sm-6"">
+                    <label for="edit-request-id">[% l('Request ID') %]</label>
+                    <input type="text" class="form-control" focus-me='focusMe'
+                        id="edit-request-id" ng-model="request.id" ng-disabled="true"/>
+                </div>
+                <div class="form-group col-sm-6"">
+                    <label for="edit-request-status">[% l('Request Status') %]</label>
+                    <input type="text" class="form-control" focus-me='focusMe'
+                        id="edit-request-status" ng-model="request.request_status.label" ng-disabled="true"/>
+                </div>
+            </div>
+            <div class="form-group">
+                <label for="request-type-selector">[% l('Request Type') %]</label>
+                <select id="request-type-selector" class="form-control" required
+                    ng-model="extra.selected_request_type"
+                    ng-disabled="mode=='view'"
+                    ng-options="rt.label() for rt in request_types"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-isxn">[% l('ISxN') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-isxn" ng-model="request.isxn"
+                    ng-disabled="mode=='view'" placeholder="[% l('ISxN...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-upc">[% l('UPC') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-upc" ng-model="request.upc"
+                    ng-disabled="mode=='view'" placeholder="[% l('UPC...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-title">[% l('Title') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-title" ng-model="request.title"
+                    ng-disabled="mode=='view'" placeholder="[% l('Title...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-volume">[% l('Volume') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-volume" ng-model="request.volume"
+                    ng-disabled="mode=='view'" placeholder="[% l('Volume...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-author">[% l('Author') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-author" ng-model="request.author"
+                    ng-disabled="mode=='view'" placeholder="[% l('Author...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-publisher">[% l('Publisher') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-publisher" ng-model="request.publisher"
+                    ng-disabled="mode=='view'" placeholder="[% l('Publisher...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-publication-location">
+                    [% l('Publication Location') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-publication-location"
+                    ng-model="request.location"
+                    ng-disabled="mode=='view'"
+                    placeholder="[% l('Publication Location...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-publication-date">
+                    [% l('Publication Date') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-publication-date"
+                    ng-model="request.pubdate"
+                    ng-disabled="mode=='view'"
+                    placeholder="[% l('Publication Date...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-article-title">
+                    [% l('Article Title') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    ng-disabled="mode=='view' || request.request_type != '2'"
+                    id="edit-request-article-title" ng-model="request.article_title"
+                    placeholder="[% l('Article Title...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-article-pages">
+                    [% l('Article Pages') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    ng-disabled="mode=='view' || request.request_type != '2'"
+                    id="edit-request-article-pages" ng-model="request.article_pages"
+                    placeholder="[% l('Article Pages...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-mentioned-in">
+                    [% l('Mentioned In') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-mentioned-in"
+                    ng-model="request.mentioned"
+                    ng-disabled="mode=='view'"
+                    placeholder="[% l('Mentioned In...') %]"/>
+            </div>
+            <div class="form-group">
+                <label for="edit-request-other-info">
+                    [% l('Other Info') %]</label>
+                <input type="text" class="form-control" focus-me='focusMe'
+                    id="edit-request-other-info"
+                    ng-model="request.other_info"
+                    ng-disabled="mode=='view'"
+                    placeholder="[% l('Other Info...') %]"/>
+            </div>
+        </div>
+        <div class="modal-footer">
+            <input type="submit" ng-hide="mode=='view'" ng-disabled="form.$invalid"
+                class="btn btn-primary" value="[% l('Save') %]"/>
+            <button class="btn btn-warning"
+                ng-click="cancel()">[% l('Cancel') %]</button>
+        </div>
+    </div> <!-- modal-content -->
+</form>
diff --git a/Open-ILS/src/templates/staff/acq/requests/t_list.tt2 b/Open-ILS/src/templates/staff/acq/requests/t_list.tt2
new file mode 100644 (file)
index 0000000..5c279b4
--- /dev/null
@@ -0,0 +1,82 @@
+<div class="container-fluid" style="text-align:center">
+  <div class="alert alert-info alert-less-pad strong-text-2">
+    <span>[% l('Acquisition Patron Requests') %]</span>
+  </div>
+</div>
+
+<div>
+  <div class="form-group">
+    <div class="row">
+      <span ng-hide="context_user || context_lineitem">
+        <label for="select-request-ou">[% l('Patron Home Library: ' ) %]</label>
+        <eg-org-selector id="select-request-ou" selected="context_ou"></eg-org-selector>
+        <span>&nbsp;</span>
+      </span>
+      <span ng-show="context_user">[% l('User ID: [_1]','{{context_user}}') %]</span>
+      <span ng-show="context_lineitem">[% l('PO Line Item ID: [_1]','{{context_lineitem}}') %]</span>
+    </div>
+  </div>
+</div>
+
+<hr/>
+
+<eg-grid
+  id-field="id"
+  idl-class="aurs"
+  features="-sort,-multisort"
+  grid-controls="grid_controls"
+  persist-key="acq.requests.list"
+  dateformat="{{$root.egDateAndTimeFormat}}">
+
+  <eg-grid-menu-item handler="create_request"
+    label="[% l('Create Request') %]"></eg-grid-menu-item>
+
+  <eg-grid-menu-item handler="canceled_requests_checkbox_handler"
+    label="[% l('Show Canceled Requests') %]"
+    checkbox="requests_show_canceled"
+    checked="requests_show_canceled"/>
+
+  <eg-grid-menu-item handler="clear_requests" disabled="need_one_and_all_uncanceled"
+    label="[% l('Clear Completed Requests') %]"></eg-grid-menu-item>
+
+  <eg-grid-action handler="edit_request" disabled="need_one_uncanceled"
+    label="[% l('Edit Request') %]"></eg-grid-action>
+  <eg-grid-action handler="view_request" disabled="need_one_selected"
+    label="[% l('View Request') %]"></eg-grid-action>
+  <eg-grid-action handler="retrieve_user" disabled="need_one_selected"
+    label="[% l('Retrieve Patron') %]"></eg-grid-action>
+  <eg-grid-action handler="add_request_to_picklist" disabled="need_one_uncanceled_no_lineitem"
+    label="[% l('Add Request to Selection List') %]"></eg-grid-action>
+  <eg-grid-action handler="view_picklist" disabled="need_one_lineitem"
+    label="[% l('View Selection List') %]"></eg-grid-action>
+  <eg-grid-action handler="set_yes_hold_requests" disabled="need_one_and_all_new_or_pending"
+    label="[% l('Set Hold on Requests') %]"></eg-grid-action>
+  <eg-grid-action handler="set_no_hold_requests" disabled="need_one_and_all_new_or_pending"
+    label="[% l('Set No Hold on Requests') %]"></eg-grid-action>
+  <eg-grid-action handler="cancel_requests" disabled="need_one_and_all_uncanceled"
+    label="[% l('Cancel Requests') %]"></eg-grid-action>
+
+  <eg-grid-field path='id' hidden required sortable></eg-grid-field>
+  <eg-grid-field path='request_status.label' sortable label="[% l('Request Status') %]"></eg-grid-field>
+  <eg-grid-field path='request_status.id' required hidden sortable label="[% l('Request Status ID') %]"></eg-grid-field>
+  <eg-grid-field path='request_date' sortable label="[% l('Request Date/Time') %]"
+    datatype="timestamp"></eg-grid-field>
+  <eg-grid-field path='need_before' sortable label="[% l('Need Before Date/Time') %]"
+    datatype="timestamp"></eg-grid-field>
+  <eg-grid-field path='request_type.label' required sortable label="[% l('Request Type') %]"></eg-grid-field>
+  <eg-grid-field path='hold' sortable></eg-grid-field>
+  <eg-grid-field path='pickup_lib.shortname' required sortable label="[% l('Pickup Lib') %]"></eg-grid-field>
+  <eg-grid-field path='isxn' sortable></eg-grid-field>
+  <eg-grid-field path='upc' sortable></eg-grid-field>
+  <eg-grid-field path='title' sortable></eg-grid-field>
+  <eg-grid-field path='article_title' sortable></eg-grid-field>
+  <eg-grid-field path='lineitem.id' required sortable label="[% l('Lineitem ID') %]" hidden></eg-grid-field>
+  <eg-grid-field path='lineitem.picklist' sortable required label="[% l('Selection List ID') %]" hidden></eg-grid-field>
+  <eg-grid-field path='usr.id' required sortable label="[% l('User ID') %]" hidden></eg-grid-field>
+  <eg-grid-field path='usr.card.barcode' sortable required label="[% l('User Barcode') %]"></eg-grid-field>
+  <eg-grid-field path='usr.family_name' sortable required label="[% l('User Family Name') %]" hidden></eg-grid-field>
+  <eg-grid-field path='usr.home_ou.shortname' required sortable label="[% l('User Home Library') %]" hidden></eg-grid-field>
+  <eg-grid-field path='cancel_reason.label' sortable required label="[% l('Cancel Reason') %]" hidden></eg-grid-field>
+  <eg-grid-field path='*' required hidden></eg-grid-field>
+</eg-grid>
+
diff --git a/Open-ILS/src/templates/staff/acq/requests/t_set_no_hold.tt2 b/Open-ILS/src/templates/staff/acq/requests/t_set_no_hold.tt2
new file mode 100644 (file)
index 0000000..77c5d4e
--- /dev/null
@@ -0,0 +1,25 @@
+[% ctx.page_title = l('Set "No Hold" on Selected Patron Requests'); %]
+<!-- use <form> so we get submit-on-enter for free -->
+<form class="form-validated" novalidate name="form" ng-submit="ok(true)">
+    <div> <!-- modal-content -->
+        <div class="modal-header">
+            <button type="button" class="close" ng-click="cancel()"
+                aria-hidden="true">&times;</button>
+            <h4 class="modal-title">
+                [% l('Set "No Hold" on Selected Patron Requests') %]</h4>
+        </div>
+        <div class="modal-body">
+            <div class="form-group">
+                <label for="ids">[% l('Request IDs') %]</label>
+                <input type="text" class="form-control"
+                    id="ids" ng-model="ids" ng-disabled="true"/>
+            </div>
+        </div>
+        <div class="modal-footer">
+            <input type="submit" ng-disabled="form.$invalid"
+                class="btn btn-primary" value="[% l('Update Requests') %]"/>
+            <button class="btn btn-warning"
+                ng-click="cancel()">[% l('Abort Update') %]</button>
+        </div>
+    </div> <!-- modal-content -->
+</form>
diff --git a/Open-ILS/src/templates/staff/acq/requests/t_set_yes_hold.tt2 b/Open-ILS/src/templates/staff/acq/requests/t_set_yes_hold.tt2
new file mode 100644 (file)
index 0000000..45acd4e
--- /dev/null
@@ -0,0 +1,25 @@
+[% ctx.page_title = l('Set "Hold" on Selected Patron Requests'); %]
+<!-- use <form> so we get submit-on-enter for free -->
+<form class="form-validated" novalidate name="form" ng-submit="ok(true)">
+    <div> <!-- modal-content -->
+        <div class="modal-header">
+            <button type="button" class="close" ng-click="cancel()"
+                aria-hidden="true">&times;</button>
+            <h4 class="modal-title">
+                [% l('Set "Hold" on Selected Patron Requests') %]</h4>
+        </div>
+        <div class="modal-body">
+            <div class="form-group">
+                <label for="ids">[% l('Request IDs') %]</label>
+                <input type="text" class="form-control"
+                    id="ids" ng-model="ids" ng-disabled="true"/>
+            </div>
+        </div>
+        <div class="modal-footer">
+            <input type="submit" ng-disabled="form.$invalid"
+                class="btn btn-primary" value="[% l('Update Requests') %]"/>
+            <button class="btn btn-warning"
+                ng-click="cancel()">[% l('Abort Update') %]</button>
+        </div>
+    </div> <!-- modal-content -->
+</form>
index 6724ca4..5ebbe0e 100644 (file)
@@ -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>
index d3033d7..a5d9888 100644 (file)
             </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>
index 40347fc..8c59f4e 100644 (file)
@@ -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())};
index f59b93b..93d2a3f 100644 (file)
@@ -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 (file)
index 0000000..0191ef3
--- /dev/null
@@ -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 (file)
index 0000000..013b083
--- /dev/null
@@ -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;
+}])
+;