toward acq requests
authorJason Etheridge <jason@EquinoxInitiative.org>
Mon, 12 Mar 2018 22:02:47 +0000 (18:02 -0400)
committerJason Etheridge <jason@EquinoxInitiative.org>
Thu, 31 May 2018 15:06:39 +0000 (11:06 -0400)
Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
18 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Order.pm
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 b272b9f..8820cc7 100644 (file)
@@ -6085,6 +6085,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"/>
@@ -6103,6 +6104,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>
@@ -8147,6 +8149,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"/>
@@ -8174,6 +8177,85 @@ 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 p.state = 'received' THEN 5 -- Received
+                        WHEN p.id IS NOT NULL AND h.id IS NOT NULL THEN 4 -- Ordered, Hold Placed
+                        WHEN p.id IS NOT NULL 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="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 />
+            </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 3eae860..54a30fc 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];
@@ -3570,6 +3582,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) = @_;
@@ -3602,7 +3629,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;
             }
         }
@@ -3610,6 +3644,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 {
@@ -3625,6 +3660,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 => {
index 60088d1..f956abc 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
@@ -970,9 +988,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 7f7687d..0a2709b 100644 (file)
@@ -1903,7 +1903,9 @@ INSERT INTO permission.perm_list ( id, code, description ) VALUES
  ( 605, 'UPDATE_COPY_ALERT', oils_i18n_gettext( 605,
     'Update copy alerts', 'ppl', 'description' )),
  ( 606, 'DELETE_COPY_ALERT', oils_i18n_gettext( 606,
-    'Delete copy alerts', 'ppl', 'description' ))
+    'Delete copy alerts', 'ppl', 'description' )),
+ ( 607, 'CLEAR_PURCHASE_REQUEST', oils_i18n_gettext(607,
+    'Clear Completed User Purchase Requests', 'ppl', 'description'))
 ;
 
 SELECT SETVAL('permission.perm_list_id_seq'::TEXT, 1000);
@@ -2578,6 +2580,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',
@@ -2951,12 +2954,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
@@ -3471,19 +3475,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)
 
@@ -11895,6 +11899,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..1ba02b5
--- /dev/null
@@ -0,0 +1,82 @@
+BEGIN;
+
+--SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+ALTER TABLE acq.user_request ADD COLUMN cancel_time TIMESTAMPTZ;
+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
+ ( 607, 'CLEAR_PURCHASE_REQUEST', oils_i18n_gettext(607,
+    '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);
+
+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..b1efd40
--- /dev/null
@@ -0,0 +1,230 @@
+[% 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" ng-show="mode=='view'">
+                <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('Email Notify?') %]
+                    </label>
+                </div>
+                <div class="form-group col-sm-6">
+                    <label for="edit-request-phone-notify">
+                        [% l('Phone Notify?') %]</label>
+                    <input type="text" class="form-control" focus-me='focusMe'
+                        id="edit-request-phone-notify"
+                        ng-model="request.phone_notify"
+                        ng-disabled="true"/>
+                </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-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..1bc1956
--- /dev/null
@@ -0,0 +1,81 @@
+<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='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 94849f2..e4e8acf 100644 (file)
@@ -207,6 +207,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 a6a65ad..2621973 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 7c19fef..70d1c2f 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..0af28e7
--- /dev/null
@@ -0,0 +1,486 @@
+/**
+ * AcqRequests, yo
+ */
+
+angular.module('egCoreMod')
+
+.factory('egAcqRequests',
+
+       ['$uibModal','$q','egCore','ngToast',
+function($uibModal , $q , egCore , 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']
+        }
+    };
+
+    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']
+        }
+    };
+
+    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 = {
+                "1": [aur_obj.title(), aur_obj.article_title(), aur_obj.volume()].join(' '),
+                "2": aur_obj.author(),
+                "4": aur_obj.article_pages(),
+                "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','request_types','request_status_types',
+                 function($m_scope , $uibModalInstance , egCore ,
+                          request , request_types , request_status_types ) {
+                    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 = {};
+                    $m_scope.extra.user_obj = request.usr;
+                    angular.forEach(['hold', 'email_notify'], function(field) {
+                        request[field] = request[field] == 't';
+                    });
+                    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 = context_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'  : [
+                                             'mailing_address'
+                                            ,'billing_address'
+                                            ,'home_ou'
+                                        ]
+                                    }
+                                },
+                                { atomic : true }
+                            ).then(function(usr) {
+                                $m_scope.extra.user_obj =
+                                    egCore.idl.toHash(usr[0]);
+                                $m_scope.request.usr =
+                                    $m_scope.extra.user_obj.id;
+                            });
+                        });
+                    }
+                    $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 : function() {
+                    if (mode=='create') {
+                        var aur_obj = egCore.idl.toHash(new egCore.idl.aurs());
+                        if (row['usr']) {
+                            aur_obj.usr = row['usr'];
+                        }
+                        return aur_obj;
+                    } else {
+                        return egCore.pcrud.search('aurs', {
+                                id : row['id']
+                            }, aurs_fleshing, {
+                                atomic : true
+                            }
+                        ).then(function(requests) {
+                            return egCore.idl.toHash(requests[0]);
+                        });
+                    }
+                }
+                ,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 (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) {
+                console.log('obj',obj);
+                if (callback) {
+                    callback(obj);
+                }
+            });
+        }).catch(function(e) {
+            console.log('caught',e);
+        });
+    }
+
+    return service;
+}])
+;