From 9aaf3351dc4f2dbf649ea1c2cb5711fc98637fbb Mon Sep 17 00:00:00 2001
From: Jason Etheridge <jason@esilibrary.com>
Date: Wed, 29 Jun 2011 12:12:31 -0400
Subject: [PATCH] Staff UI for batch holds on items.

"Request Item" action in Holdings Maintenance, Item Status, and Copy
Buckets is the entry-point.  Works on selected items in the first two
interfaces and all items in the bucket for the latter.  UI allows you
to place Copy type, Recall type, or Force type holds.  It reports the #
of successes and breaks down the failures by failure event.  You can
retry failures (optionally changing some of the request parameters
like Pickup Library) or "override" them.  Clicking the hyperlink for
a set of failures will show the items involved in a new Item Status tab.

More technical blurbs from squashed commits:

  * "open-ils.circ.holds.test_and_create.batch" Takes an argument hash and a list of targets.  All the holds created will be identical except for the targets.
  * retrieve and display Recall and Force holds like Copy holds
  * give the Item Status UI an inefficient way to handle being passed copy id's (via xulG) in addition to barcodes
  * wire-up item hold request ui
  * place hold UI

Signed-off-by: Jason Etheridge <jason@esilibrary.com>
Signed-off-by: Bill Erickson <berick@esilibrary.com>
---
 .../perlmods/lib/OpenILS/Application/Circ/Holds.pm | 117 +++++++-
 Open-ILS/web/opac/locale/en-US/lang.dtd            |  21 ++
 .../staff_client/chrome/content/main/constants.js  |   4 +
 .../xul/staff_client/server/cat/copy_browser.js    |  12 +
 .../xul/staff_client/server/cat/copy_browser.xul   |   3 +
 .../xul/staff_client/server/cat/copy_buckets.js    |  22 ++
 .../xul/staff_client/server/cat/copy_buckets.xul   |   1 +
 .../server/cat/copy_buckets_overlay.xul            |   1 +
 Open-ILS/xul/staff_client/server/cat/util.js       |  30 +-
 .../xul/staff_client/server/circ/copy_status.js    |  16 ++
 .../xul/staff_client/server/circ/copy_status.xul   |  14 +
 .../server/circ/copy_status_overlay.xul            |   2 +
 .../server/locale/en-US/patron.properties          |  12 +
 Open-ILS/xul/staff_client/server/patron/holds.js   |   4 +
 .../xul/staff_client/server/patron/place_hold.js   | 320 +++++++++++++++++++++
 .../xul/staff_client/server/patron/place_hold.xul  | 105 +++++++
 16 files changed, 681 insertions(+), 3 deletions(-)
 create mode 100644 Open-ILS/xul/staff_client/server/patron/place_hold.js
 create mode 100644 Open-ILS/xul/staff_client/server/patron/place_hold.xul

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
index 6d47aeb281..651e9b52ad 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
@@ -39,6 +39,105 @@ use OpenSRF::Utils::Cache;
 my $apputils = "OpenILS::Application::AppUtils";
 my $U = $apputils;
 
+__PACKAGE__->register_method(
+    method    => "test_and_create_hold_batch",
+    api_name  => "open-ils.circ.holds.test_and_create.batch",
+    stream => 1,
+    signature => {
+        desc => q/This is for batch creating a set of holds where every field is identical except for the targets./,
+        params => [
+            { desc => 'Authentication token', type => 'string' },
+            { desc => 'Hash of named parameters.  Same as for open-ils.circ.title_hold.is_possible, though the pertinent target field is automatically populated based on the hold_type and the specified list of targets.', type => 'object'},
+            { desc => 'Array of target ids', type => 'array' }
+        ],
+        return => {
+            desc => 'Array of hold ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
+        },
+    }
+);
+
+__PACKAGE__->register_method(
+    method    => "test_and_create_hold_batch",
+    api_name  => "open-ils.circ.holds.test_and_create.batch.override",
+    stream => 1,
+    signature => {
+        desc  => '@see open-ils.circ.holds.test_and_create.batch',
+    }
+);
+
+
+sub test_and_create_hold_batch {
+	my( $self, $conn, $auth, $params, $target_list ) = @_;
+
+	my $override = 1 if $self->api_name =~ /override/;
+
+	my $e = new_editor(authtoken=>$auth);
+	return $e->die_event unless $e->checkauth;
+    $$params{'requestor'} = $e->requestor->id;
+
+    my $target_field;
+    if ($$params{'hold_type'} eq 'T') { $target_field = 'titleid'; }
+    elsif ($$params{'hold_type'} eq 'C') { $target_field = 'copy_id'; }
+    elsif ($$params{'hold_type'} eq 'R') { $target_field = 'copy_id'; }
+    elsif ($$params{'hold_type'} eq 'F') { $target_field = 'copy_id'; }
+    elsif ($$params{'hold_type'} eq 'I') { $target_field = 'issuanceid'; }
+    elsif ($$params{'hold_type'} eq 'V') { $target_field = 'volume_id'; }
+    elsif ($$params{'hold_type'} eq 'M') { $target_field = 'mrid'; }
+    elsif ($$params{'hold_type'} eq 'P') { $target_field = 'partid'; }
+    else { return undef; }
+
+    foreach (@$target_list) {
+        $$params{$target_field} = $_;
+        my $res;
+        if (! $override) {        
+            ($res) = $self->method_lookup(
+                'open-ils.circ.title_hold.is_possible')->run($auth, $params);
+        }
+        if ($override || $res->{'success'} == 1) {
+            my $ahr = construct_hold_request_object($params);
+            my ($res2) = $self->method_lookup(
+                $override
+                ? 'open-ils.circ.holds.create.override'
+                : 'open-ils.circ.holds.create'
+            )->run($auth, $ahr);
+            $res2 = {
+                'target' => $$params{$target_field},
+                'result' => $res2
+            };
+            $conn->respond($res2);
+        } else {
+            $res = {
+                'target' => $$params{$target_field},
+                'result' => $res
+            };
+            $conn->respond($res);
+        }
+    }
+    return undef;
+}
+
+sub construct_hold_request_object {
+    my ($params) = @_;
+
+    my $ahr = Fieldmapper::action::hold_request->new;
+    $ahr->isnew('1');
+
+    foreach my $field (keys %{ $params }) {
+        if ($field eq 'depth') { $ahr->selection_depth($$params{$field}); }
+        elsif ($field eq 'patronid') {
+            $ahr->usr($$params{$field}); }
+        elsif ($field eq 'titleid') { $ahr->target($$params{$field}); }
+        elsif ($field eq 'copy_id') { $ahr->target($$params{$field}); }
+        elsif ($field eq 'issuanceid') { $ahr->target($$params{$field}); }
+        elsif ($field eq 'volume_id') { $ahr->target($$params{$field}); }
+        elsif ($field eq 'mrid') { $ahr->target($$params{$field}); }
+        elsif ($field eq 'partid') { $ahr->target($$params{$field}); }
+        else {
+            $ahr->$field($$params{$field});
+        }
+    }
+    return $ahr;
+}
 
 __PACKAGE__->register_method(
     method    => "create_hold_batch",
@@ -2750,7 +2849,7 @@ sub all_rec_holds {
     $args->{fulfillment_time} = undef; #  we don't want to see old fulfilled holds
 	$args->{cancel_time} = undef;
 
-	my $resp = { volume_holds => [], copy_holds => [], metarecord_holds => [], part_holds => [], issuance_holds => [] };
+	my $resp = { volume_holds => [], copy_holds => [], recall_holds => [], force_holds => [], metarecord_holds => [], part_holds => [], issuance_holds => [] };
 
     my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
     if($mr_map) {
@@ -2826,6 +2925,20 @@ sub all_rec_holds {
 			%$args }, 
 		{idlist=>1} );
 
+	$resp->{recall_holds} = $e->search_action_hold_request(
+		{ 
+			hold_type => OILS_HOLD_TYPE_RECALL,
+			target => $copies,
+			%$args }, 
+		{idlist=>1} );
+
+	$resp->{force_holds} = $e->search_action_hold_request(
+		{ 
+			hold_type => OILS_HOLD_TYPE_FORCE,
+			target => $copies,
+			%$args }, 
+		{idlist=>1} );
+
 	return $resp;
 }
 
@@ -2965,7 +3078,7 @@ sub find_hold_mvr {
 
         $tid = $part->record;
 
-	} elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
+	} elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY || $hold->hold_type eq OILS_HOLD_TYPE_RECALL || $hold->hold_type eq OILS_HOLD_TYPE_FORCE ) {
 		$copy = $e->retrieve_asset_copy([
             $hold->target, 
             {flesh => 1, flesh_fields => {acp => ['call_number']}}
diff --git a/Open-ILS/web/opac/locale/en-US/lang.dtd b/Open-ILS/web/opac/locale/en-US/lang.dtd
index ffbce223ae..6b90ecd95c 100644
--- a/Open-ILS/web/opac/locale/en-US/lang.dtd
+++ b/Open-ILS/web/opac/locale/en-US/lang.dtd
@@ -2285,6 +2285,8 @@
 <!ENTITY staff.circ.copy_status_overlay.cmd_triggered_events.accesskey "T">
 <!ENTITY staff.circ.copy_status_overlay.cmd_book_item_now.label "Book Item Now">
 <!ENTITY staff.circ.copy_status_overlay.cmd_book_item_now.accesskey "N">
+<!ENTITY staff.circ.copy_status_overlay.cmd_request_items.label "Request Item">
+<!ENTITY staff.circ.copy_status_overlay.cmd_request_items.accesskey "R">
 <!ENTITY staff.circ.copy_status_overlay.cmd_create_brt.label "Make Item Bookable">
 <!ENTITY staff.circ.copy_status_overlay.cmd_create_brt.accesskey "K">
 <!ENTITY staff.circ.copy_status_overlay.cmd_find_acq_po.label "Find Originating Acquisition">
@@ -2553,6 +2555,8 @@
 <!ENTITY staff.cat.copy_browser.actions.cmd_transfer_volume.accesskey "T">
 <!ENTITY staff.cat.copy_browser.actions.cmd_delete_items.label "Delete Items">
 <!ENTITY staff.cat.copy_browser.actions.cmd_delete_volumes.label "Delete Volumes">
+<!ENTITY staff.cat.copy_browser.actions.cmd_request_items.label "Request Item">
+<!ENTITY staff.cat.copy_browser.actions.cmd_request_items.accesskey "R">
 <!ENTITY staff.cat.copy_browser.actions.sel_mark_items_damaged.label "Mark Item Damaged">
 <!ENTITY staff.cat.copy_browser.actions.sel_mark_items_damaged.accesskey "D">
 <!ENTITY staff.cat.copy_browser.actions.sel_mark_items_missing.label "Mark Item Missing">
@@ -2639,6 +2643,8 @@
 <!ENTITY staff.cat.copy_buckets_overlay.copy_buckets.cmd_copy_buckets_export.label "Export">
 <!ENTITY staff.cat.copy_buckets_overlay.copy_buckets.batch.label "Batch:">
 <!ENTITY staff.cat.copy_buckets_overlay.copy_buckets.cmd_export_to_copy_status.label "Show Status">
+<!ENTITY staff.cat.copy_buckets_overlay.copy_buckets.cmd_request_items.label "Request Items">
+<!ENTITY staff.cat.copy_buckets_overlay.copy_buckets.cmd_request_items.accesskey "R">
 <!ENTITY staff.cat.copy_buckets_overlay.copy_buckets.copy_buckets_transfer_to_volume.label "Transfer to Specific Volume">
 <!ENTITY staff.cat.copy_buckets_overlay.copy_buckets.copy_buckets_batch_copy_edit.label "Edit Item Attributes">
 <!ENTITY staff.cat.copy_buckets_overlay.copy_buckets.copy_buckets_batch_copy_delete.label "Delete All from Catalog">
@@ -3582,3 +3588,18 @@
 <!ENTITY staff.client.portal.receipts "Edit Receipt Templates">
 <!ENTITY staff.client.portal.copyright "Copyright &copy; 2006-2011 Georgia Public Library Service, and others">
 <!ENTITY staff.client.portal.poweredby "Powered by">
+<!ENTITY staff.item.batch.hold.groupbox_caption "Request Details">
+<!ENTITY staff.item.batch.hold.hold_type_menu.label "Hold Type">
+<!ENTITY staff.item.batch.hold.hold_type_menu.accesskey "T">
+<!ENTITY staff.item.batch.hold.hold_type_menuentry.copy_hold "Copy Hold">
+<!ENTITY staff.item.batch.hold.hold_type_menuentry.recall_hold "Recall Hold">
+<!ENTITY staff.item.batch.hold.hold_type_menuentry.force_hold "Force Hold">
+<!ENTITY staff.item.batch.hold.pick_up_lib_menu.label "Pickup Library">
+<!ENTITY staff.item.batch.hold.pick_up_lib_menu.accesskey "P">
+<!ENTITY staff.item.batch.hold.hold_usr.label "Hold User Barcode">
+<!ENTITY staff.item.batch.hold.hold_usr.accesskey "U">
+<!ENTITY staff.item.batch.hold.request_btn.label "Make Request">
+<!ENTITY staff.item.batch.hold.request_btn.accesskey "M">
+<!ENTITY staff.item.batch.hold.cancel_btn.label "Cancel">
+<!ENTITY staff.item.batch.hold.cancel_btn.accesskey "C">
+<!ENTITY staff.item.batch.hold.failures_and_settings "The settings above may be changed for retries and overrides.">
diff --git a/Open-ILS/xul/staff_client/chrome/content/main/constants.js b/Open-ILS/xul/staff_client/chrome/content/main/constants.js
index d02c17224f..4d61b10dec 100644
--- a/Open-ILS/xul/staff_client/chrome/content/main/constants.js
+++ b/Open-ILS/xul/staff_client/chrome/content/main/constants.js
@@ -100,6 +100,7 @@ var api = {
     'FM_ACP_RETRIEVE_VIA_BARCODE' : { 'app' : 'open-ils.search', 'method' : 'open-ils.search.asset.copy.fleshed2.find_by_barcode', 'secure' : false },
     'FM_ACP_RETRIEVE_VIA_BARCODE.authoritative' : { 'app' : 'open-ils.search', 'method' : 'open-ils.search.asset.copy.fleshed2.find_by_barcode.authoritative', 'secure' : false },
     'FM_ACP_FLESHED_BATCH_RETRIEVE' : { 'app' : 'open-ils.search', 'method' : 'open-ils.search.asset.copy.fleshed.batch.retrieve', 'secure' : false },
+    'FM_ACP_UNFLESHED_BATCH_RETRIEVE' : { 'app' : 'open-ils.search', 'method' : 'open-ils.search.asset.copy.batch.retrieve', 'secure' : false },
     'FM_ACP_FLESHED_BATCH_RETRIEVE.authoritative' : { 'app' : 'open-ils.search', 'method' : 'open-ils.search.asset.copy.fleshed.batch.retrieve.authoritative', 'secure' : false },
     'FM_ACP_FLESHED_BATCH_UPDATE' : { 'app' : 'open-ils.cat', 'method' : 'open-ils.cat.asset.copy.fleshed.batch.update' },
     'FM_ACP_COUNT' : { 'app' : 'open-ils.search', 'method' : 'open-ils.search.biblio.record.copy_count.staff', 'secure' : false },
@@ -117,6 +118,8 @@ var api = {
     'FM_AHN_CREATE' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.hold_notification.create' },
     'FM_AHN_RETRIEVE_VIA_AHR' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.hold_notification.retrieve_by_hold' },
     'FM_AHN_RETRIEVE_VIA_AHR.authoritative' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.hold_notification.retrieve_by_hold.authoritative' },
+    'FM_AHR_CHECK_AND_CREATE.batch' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.holds.test_and_create.batch' },
+    'FM_AHR_CHECK_AND_CREATE.batch.override' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.holds.test_and_create.batch.override' },
     'FM_AHR_RETRIEVE' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.holds.retrieve_by_id' },
     'FM_AHR_BLOB_RETRIEVE' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.hold.details.retrieve' },
     'FM_AHR_BLOB_RETRIEVE.authoritative' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.hold.details.retrieve.authoritative' },
@@ -416,6 +419,7 @@ var urls = {
     'XUL_HOLDS_BROWSER' : '/xul/server/patron/holds.xul',
     'XUL_HOLD_DETAILS' : '/xul/server/patron/hold_details.xul',
     'XUL_HOLD_CANCEL' : '/xul/server/patron/hold_cancel.xul',
+    'XUL_HOLD_PLACEMENT' : '/xul/server/patron/place_hold.xul',
     'XUL_IN_HOUSE_USE' : '/xul/server/circ/in_house_use.xul',
     'XUL_LIST_CLIPBOARD' : '/xul/server/util/list_clipboard.xul',
     'XUL_LOCAL_ADMIN' : '/xul/server/admin/index.xhtml',
diff --git a/Open-ILS/xul/staff_client/server/cat/copy_browser.js b/Open-ILS/xul/staff_client/server/cat/copy_browser.js
index 9122655808..2269e30de1 100644
--- a/Open-ILS/xul/staff_client/server/cat/copy_browser.js
+++ b/Open-ILS/xul/staff_client/server/cat/copy_browser.js
@@ -111,6 +111,18 @@ cat.copy_browser.prototype = {
                                 obj.list.clear();
                             }
                         ],
+                        'cmd_request_items' : [
+                            ['command'],
+                            function() {
+                                JSAN.use('cat.util'); JSAN.use('util.functional');
+
+                                var list = util.functional.filter_list( obj.sel_list, function (o) { return o.split(/_/)[0] == 'acp'; });
+
+                                list = util.functional.map_list( list, function (o) { return o.split(/_/)[1]; });
+
+                                cat.util.request_items( list );
+                            }
+                        ],
                         'sel_mark_items_damaged' : [
                             ['command'],
                             function() {
diff --git a/Open-ILS/xul/staff_client/server/cat/copy_browser.xul b/Open-ILS/xul/staff_client/server/cat/copy_browser.xul
index d161d121f5..79028aa95e 100644
--- a/Open-ILS/xul/staff_client/server/cat/copy_browser.xul
+++ b/Open-ILS/xul/staff_client/server/cat/copy_browser.xul
@@ -81,6 +81,7 @@ vim:noet:sw=4:ts=4:
         <command id="cmd_show_all_libs" />
         <command id="cmd_show_libs_with_copies" />
 
+        <command id="cmd_request_items" />
         <command id="sel_mark_items_damaged" />
         <command id="sel_mark_items_missing" />
         <command id="cmd_add_items"/>
@@ -126,6 +127,7 @@ vim:noet:sw=4:ts=4:
             <menuitem command="cmd_delete_items" label="&staff.cat.copy_browser.actions.cmd_delete_items.label;" accesskey=""/>
             <menuitem command="cmd_delete_volumes" label="&staff.cat.copy_browser.actions.cmd_delete_volumes.label;" accesskey=""/>
             <menuseparator/>
+            <menuitem command="cmd_request_items" label="&staff.cat.copy_browser.actions.cmd_request_items.label;" accesskey="&staff.cat.copy_browser.actions.cmd_request_items.accesskey;"/>
             <menuitem command="sel_mark_items_damaged" label="&staff.cat.copy_browser.actions.sel_mark_items_damaged.label;" accesskey="&staff.cat.copy_browser.actions.sel_mark_items_damaged.accesskey;"/>
             <menuitem command="sel_mark_items_missing" label="&staff.cat.copy_browser.actions.sel_mark_items_missing.label;" accesskey="&staff.cat.copy_browser.actions.sel_mark_items_missing.accesskey;"/>
             <menuseparator/>
@@ -181,6 +183,7 @@ vim:noet:sw=4:ts=4:
                         <menuitem command="cmd_delete_items" label="&staff.cat.copy_browser.holdings_maintenance.cmd_delete_items.label;" accesskey=""/>
                         <menuitem command="cmd_delete_volumes" label="&staff.cat.copy_browser.holdings_maintenance.cmd_delete_volumes.label;" accesskey=""/>
                         <menuseparator/>
+                        <menuitem command="cmd_request_items" label="&staff.cat.copy_browser.actions.cmd_request_items.label;" accesskey="&staff.cat.copy_browser.actions.cmd_request_items.accesskey;"/>
                         <menuitem command="sel_mark_items_damaged" label="&staff.cat.copy_browser.holdings_maintenance.sel_mark_items_damaged.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.sel_mark_items_damaged.accesskey;"/>
                         <menuitem command="sel_mark_items_missing" label="&staff.cat.copy_browser.holdings_maintenance.sel_mark_items_missing.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.sel_mark_items_missing.accesskey;"/>
                         <menuseparator/>
diff --git a/Open-ILS/xul/staff_client/server/cat/copy_buckets.js b/Open-ILS/xul/staff_client/server/cat/copy_buckets.js
index d9abdfbd21..cc7b5a9e80 100644
--- a/Open-ILS/xul/staff_client/server/cat/copy_buckets.js
+++ b/Open-ILS/xul/staff_client/server/cat/copy_buckets.js
@@ -533,6 +533,28 @@ cat.copy_buckets.prototype = {
                         }
                     ],
 
+                    'cmd_request_items' : [
+                        ['command'],
+                        function() {
+                            try {
+                                obj.list2.select_all();
+
+                                var copy_ids = util.functional.map_list(
+                                    obj.list2.dump_retrieve_ids(),
+                                    function (o) {
+                                        return JSON2js(o)[0]; // acp_id
+                                    }
+                                )
+
+                                JSAN.use('cat.util');
+                                cat.util.request_items(copy_ids); 
+
+                            } catch(E) {
+                                obj.error.standard_unexpected_error_alert($('catStrings').getString('staff.cat.copy_buckets.copy_buckets_transfer_to_volume.error'), E);
+                            }
+                        }
+                    ],
+
                     'copy_buckets_transfer_to_volume' : [
                         ['command'],
                         function() {
diff --git a/Open-ILS/xul/staff_client/server/cat/copy_buckets.xul b/Open-ILS/xul/staff_client/server/cat/copy_buckets.xul
index ac6b9f6089..90f2d19590 100644
--- a/Open-ILS/xul/staff_client/server/cat/copy_buckets.xul
+++ b/Open-ILS/xul/staff_client/server/cat/copy_buckets.xul
@@ -88,6 +88,7 @@
         <command id="copy_buckets_transfer_to_volume" />
         <command id="copy_buckets_batch_copy_edit" />
         <command id="copy_buckets_batch_copy_delete" />
+        <command id="cmd_request_items" />
     </commandset>
 
     <box id="copy_buckets_main" />
diff --git a/Open-ILS/xul/staff_client/server/cat/copy_buckets_overlay.xul b/Open-ILS/xul/staff_client/server/cat/copy_buckets_overlay.xul
index 192c49d268..57016047be 100644
--- a/Open-ILS/xul/staff_client/server/cat/copy_buckets_overlay.xul
+++ b/Open-ILS/xul/staff_client/server/cat/copy_buckets_overlay.xul
@@ -73,6 +73,7 @@
     <hbox style="background: grey">
         <vbox><spacer flex="1"/><label value="&staff.cat.copy_buckets_overlay.copy_buckets.batch.label;" style="font-weight: bold"/><spacer flex="1"/></vbox>
         <button label="&staff.cat.copy_buckets_overlay.copy_buckets.cmd_export_to_copy_status.label;" command="cmd_export_to_copy_status"/>
+        <button command="cmd_request_items" label="&staff.cat.copy_buckets_overlay.copy_buckets.cmd_request_items.label;" accesskey="&staff.cat.copy_buckets_overlay.copy_buckets.cmd_request_items.accesskey;"/>
         <button command="copy_buckets_transfer_to_volume" label="&staff.cat.copy_buckets_overlay.copy_buckets.copy_buckets_transfer_to_volume.label;"/>
         <button command="copy_buckets_batch_copy_edit" label="&staff.cat.copy_buckets_overlay.copy_buckets.copy_buckets_batch_copy_edit.label;" image="/xul/server/skin/media/images/grinder.gif"/>
         <button command="copy_buckets_batch_copy_delete" label="&staff.cat.copy_buckets_overlay.copy_buckets.copy_buckets_batch_copy_delete.label;" />
diff --git a/Open-ILS/xul/staff_client/server/cat/util.js b/Open-ILS/xul/staff_client/server/cat/util.js
index 64358a474b..19ee2ab417 100644
--- a/Open-ILS/xul/staff_client/server/cat/util.js
+++ b/Open-ILS/xul/staff_client/server/cat/util.js
@@ -11,7 +11,8 @@ cat.util.EXPORT_OK    = [
     'make_bookable', 'edit_new_brsrc', 'edit_new_bresv', 'batch_edit_volumes', 'render_fine_level',
     'render_loan_duration', 'mark_item_as_missing_pieces', 'render_callnumbers_for_bib_menu',
     'render_cn_prefix_menuitems', 'render_cn_suffix_menuitems', 'render_cn_class_menu',
-    'render_cn_prefix_menu', 'render_cn_suffix_menu', 'transfer_specific_title_holds'
+    'render_cn_prefix_menu', 'render_cn_suffix_menu', 'transfer_specific_title_holds',
+    'request_items'
 ];
 cat.util.EXPORT_TAGS    = { ':all' : cat.util.EXPORT_OK };
 
@@ -1118,4 +1119,31 @@ cat.util.render_cn_suffix_menu = function(ou_ids,extra_menuitems,menu_default) {
     }
 }
 
+cat.util.request_items = function(copy_ids) {
+    var error;
+    try {
+        JSAN.use('util.error');
+        error = new util.error();
+
+        JSAN.use('util.functional');
+        if (!copy_ids) { return; }
+        copy_ids = util.functional.filter_list(
+            copy_ids,
+            function(o) { return o != null; }
+        );
+        if (copy_ids.length < 1) { return; }
+
+        xulG.new_tab(
+            urls.XUL_HOLD_PLACEMENT,
+            {},
+            {
+                'copy_ids' : copy_ids
+            }
+        );
+
+    } catch(E) {
+        alert('Error in cat.util.request_items: ' + E);
+    }
+}
+
 dump('exiting cat/util.js\n');
diff --git a/Open-ILS/xul/staff_client/server/circ/copy_status.js b/Open-ILS/xul/staff_client/server/circ/copy_status.js
index f2816c12e7..bfd7b79326 100644
--- a/Open-ILS/xul/staff_client/server/circ/copy_status.js
+++ b/Open-ILS/xul/staff_client/server/circ/copy_status.js
@@ -67,6 +67,7 @@ circ.copy_status.prototype = {
                             obj.controller.view.cmd_triggered_events.setAttribute('disabled','true');
                             obj.controller.view.cmd_create_brt.setAttribute('disabled','true');
                             obj.controller.view.cmd_book_item_now.setAttribute('disabled','true');
+                            obj.controller.view.cmd_request_items.setAttribute('disabled','true');
                             obj.controller.view.cmd_find_acq_po.setAttribute('disabled','true');
                             obj.controller.view.sel_spine.setAttribute('disabled','true');
                             obj.controller.view.sel_transit_abort.setAttribute('disabled','true');
@@ -98,6 +99,7 @@ circ.copy_status.prototype = {
                             } else {
                                 obj.controller.view.cmd_book_item_now.setAttribute('disabled','true');
                             }
+                            obj.controller.view.cmd_request_items.setAttribute('disabled','false');
                             obj.controller.view.cmd_create_brt.setAttribute('disabled','false');
                             obj.controller.view.cmd_find_acq_po.setAttribute("disabled", obj.selection_list.length == 1 ? "false" : "true");
                             obj.controller.view.sel_spine.setAttribute('disabled','false');
@@ -232,6 +234,20 @@ circ.copy_status.prototype = {
                             }
                         }
                     ],
+                    'cmd_request_items' : [
+                        ['command'],
+                        function() {
+                            JSAN.use('cat.util'); JSAN.use('util.functional');
+
+                            var list = util.functional.map_list(
+                                obj.selection_list, function (o) {
+                                    return o.copy_id;
+                                }
+                            );
+
+                            cat.util.request_items( list );
+                        }
+                    ],
                     "cmd_find_acq_po" : [
                         ["command"],
                         function() {
diff --git a/Open-ILS/xul/staff_client/server/circ/copy_status.xul b/Open-ILS/xul/staff_client/server/circ/copy_status.xul
index 6cee443cc0..6a1411a966 100644
--- a/Open-ILS/xul/staff_client/server/circ/copy_status.xul
+++ b/Open-ILS/xul/staff_client/server/circ/copy_status.xul
@@ -72,6 +72,19 @@
                         }
                     ) || [];
                 }
+                if (xulG.copy_ids) {
+                    JSAN.use('util.functional');
+                    JSAN.use('util.network');
+                    var net = new util.network();
+                    g.barcodes = g.barcodes.concat(
+                        util.functional.map_list(
+                            net.simple_request('FM_ACP_UNFLESHED_BATCH_RETRIEVE',[xulG.copy_ids]),
+                            function(o) {
+                                return o.barcode();
+                            }
+                        )
+                    );
+                }
 
                 window.xulG.fetched_copy_details = {};
 
@@ -117,6 +130,7 @@
         <command id="cmd_find_acq_po" disabled="true"/>
         <command id="cmd_create_brt" disabled="true"/>
         <command id="cmd_book_item_now" disabled="true"/>
+        <command id="cmd_request_items" disabled="true"/>
         <command id="sel_copy_details" disabled="true"/>
         <command id="sel_mark_items_damaged" disabled="true"/>
         <command id="sel_mark_items_missing" disabled="true"/>
diff --git a/Open-ILS/xul/staff_client/server/circ/copy_status_overlay.xul b/Open-ILS/xul/staff_client/server/circ/copy_status_overlay.xul
index 770400a85b..d8ffd777a0 100644
--- a/Open-ILS/xul/staff_client/server/circ/copy_status_overlay.xul
+++ b/Open-ILS/xul/staff_client/server/circ/copy_status_overlay.xul
@@ -20,6 +20,7 @@
         <menuseparator/>
         <menuitem command="cmd_create_brt" label="&staff.circ.copy_status_overlay.cmd_create_brt.label;" accesskey="&staff.circ.copy_status_overlay.cmd_create_brt.accesskey;"/>
         <menuitem command="cmd_book_item_now" label="&staff.circ.copy_status_overlay.cmd_book_item_now.label;" accesskey="&staff.circ.copy_status_overlay.cmd_book_item_now.accesskey;"/>
+        <menuitem command="cmd_request_items" label="&staff.circ.copy_status_overlay.cmd_request_items.label;" accesskey="&staff.circ.copy_status_overlay.cmd_request_items.accesskey;"/>
         <menuseparator/>
         <menuitem command="cmd_find_acq_po" label="&staff.circ.copy_status_overlay.cmd_find_acq_po.label;" accesskey="&staff.circ.copy_status_overlay.cmd_find_acq_po.accesskey;"/>
         <menuseparator/>
@@ -161,6 +162,7 @@
             <menuseparator />
             <menuitem command="cmd_create_brt" label="&staff.circ.copy_status_overlay.cmd_create_brt.label;" accesskey="&staff.circ.copy_status_overlay.cmd_create_brt.accesskey;"/>
             <menuitem command="cmd_book_item_now" label="&staff.circ.copy_status_overlay.cmd_book_item_now.label;" accesskey="&staff.circ.copy_status_overlay.cmd_book_item_now.accesskey;"/>
+            <menuitem command="cmd_request_items" label="&staff.circ.copy_status_overlay.cmd_request_items.label;" accesskey="&staff.circ.copy_status_overlay.cmd_request_items.accesskey;"/>
             <menuseparator />
             <menuitem command="cmd_find_acq_po" label="&staff.circ.copy_status_overlay.cmd_find_acq_po.label;" accesskey="&staff.circ.copy_status_overlay.cmd_find_acq_po.accesskey;"/>
             <menuseparator/>
diff --git a/Open-ILS/xul/staff_client/server/locale/en-US/patron.properties b/Open-ILS/xul/staff_client/server/locale/en-US/patron.properties
index ba789c859d..9bbb2cc642 100644
--- a/Open-ILS/xul/staff_client/server/locale/en-US/patron.properties
+++ b/Open-ILS/xul/staff_client/server/locale/en-US/patron.properties
@@ -398,3 +398,15 @@ web.staff.patron.ue.uedit_show_addr_replacement=<div>Replaces address <b>%1$s</b
 
 # 1 - Staff Username  2 - Patron Family  3 - Patron Barcode
 staff.circ.work_log_patron_edit.message=%1$s edited %3$s (%2$s)
+
+# 1 - Number of hold requests created
+staff.item.batch.hold.x_holds_created=%1$s holds created.
+
+# 1 - Number of holds not created for a given reason  2 - the reason for failure
+staff.item.batch.hold.x_failed_holds=%1$s failed for %2$s
+
+staff.item.batch.hold.tab_name=Item Hold/Recall/Force
+staff.item.batch.hold.retry_btn_label=Retry
+staff.item.batch.hold.override_btn_label=Override
+staff.item.batch.hold.user_not_found=User Not Found
+
diff --git a/Open-ILS/xul/staff_client/server/patron/holds.js b/Open-ILS/xul/staff_client/server/patron/holds.js
index c48600917d..234b2b615e 100644
--- a/Open-ILS/xul/staff_client/server/patron/holds.js
+++ b/Open-ILS/xul/staff_client/server/patron/holds.js
@@ -1163,6 +1163,8 @@ patron.holds.prototype = {
                                             opac_url = xulG.url_prefix( urls.opac_rdetail) + '?r=' + my_acn.record();
                                         break;
                                         case 'C' :
+                                        case 'R' :
+                                        case 'F' :
                                             var my_acp = obj.network.simple_request( 'FM_ACP_RETRIEVE', [ htarget ]);
                                             var my_acn;
                                             if (typeof my_acp.call_number() == 'object') {
@@ -1539,6 +1541,8 @@ patron.holds.prototype = {
                 holds = [];
                 if (robj != null) {
                     holds = holds.concat( robj.copy_holds );
+                    holds = holds.concat( robj.recall_holds );
+                    holds = holds.concat( robj.force_holds );
                     holds = holds.concat( robj.volume_holds );
                     holds = holds.concat( robj.title_holds );
                     holds = holds.concat( robj.part_holds );
diff --git a/Open-ILS/xul/staff_client/server/patron/place_hold.js b/Open-ILS/xul/staff_client/server/patron/place_hold.js
new file mode 100644
index 0000000000..4c57453ab7
--- /dev/null
+++ b/Open-ILS/xul/staff_client/server/patron/place_hold.js
@@ -0,0 +1,320 @@
+var error;
+var data;
+var net;
+var hold_usr;
+
+function my_init() {
+    try {
+        ui_setup(); // JSAN, tab name, etc.
+        error.sdump('D_TRACE','my_init() for place_hold.xul');
+
+        JSAN.use('OpenILS.data');
+        data = new OpenILS.data();
+        data.stash_retrieve();
+
+        JSAN.use('util.network');
+        net = new util.network();
+
+        var copy_ids = xul_param('copy_ids');
+
+        populate_hold_usr_textbox();
+        populate_pickup_lib_menu();
+
+        $('request_btn').addEventListener(
+            'command',
+            function(ev) {
+                make_request(copy_ids,false);
+            },
+            false
+        );
+        
+        set_remaining_event_listeners();
+
+    } catch(E) {
+        alert('Error in place_hold.js, my_init(): ' + E);
+    }
+}
+
+function make_request(copy_ids,override) {
+    try {
+
+        if (!hold_usr) {
+            alert( $('patronStrings').getString('staff.item.batch.hold.user_not_found') );
+            return;
+        }
+
+        var args = {
+            'hold_type' : $('hold_type_menu').value,
+            'patronid' : hold_usr,
+            'depth' : 0, 
+            'pickup_lib' : $('pickup_lib_menu').value
+        };
+
+        oils_lock_page();
+        $('progress_meter').hidden = false;
+        $('request_btn').disabled = true;
+        $('cancel_btn').disabled = true;
+
+        net.simple_request(
+            override
+            ? 'FM_AHR_CHECK_AND_CREATE.batch.override'
+            : 'FM_AHR_CHECK_AND_CREATE.batch',
+            [ ses(), args, copy_ids ],
+            handle_results
+        );
+
+    } catch(E) {
+        alert('Error in place_hold.js, make_request(): ' + E);
+    }
+}
+
+function handle_results(req) {
+    try {
+        oils_unlock_page();
+        $('progress_meter').hidden = true;
+
+        var results = req.getResultObject();
+
+        var successes = [];
+        var failures = {};
+        var failed_targets = [];
+        var failure_count = 0;
+
+        for (var i = 0; i < results.length; i++) {
+            var payload = results[i];
+            var target = payload.target;
+            var result = payload.result;
+            if (typeof result.length != 'undefined') {
+                // Array; grab first exception for simplicity
+                result = result[0];
+            }
+
+            if (typeof result == 'string' || typeof result == 'number') {
+                successes.push( result ); // hold id's
+            } else {
+                failure_count++;
+                if (typeof failures[ result.textcode ] == 'undefined') {
+                    failures[ result.textcode ] = [];
+                }
+                failures[ result.textcode ].push( target );
+                failed_targets.push( target );
+            }
+        }
+
+        var msg = document.createElement('description');
+        msg.appendChild(
+            document.createTextNode(
+                $('patronStrings').getFormattedString('staff.item.batch.hold.x_holds_created',[ successes.length ])
+            )
+        );
+        $('msgs').appendChild(msg);
+
+        if (failure_count>0) {
+            $('desc').hidden = false;
+            handle_failures(failures,failed_targets);
+        }
+    } catch(E) {
+        alert('Error in place_hold.js, handle_results(): ' + E);
+    }
+}
+
+function handle_failures(failures,failed_targets) {
+    try {
+        for (k in failures) {
+            var err_box = document.createElement('hbox');
+            var err_msg = document.createElement('description');
+            err_box.appendChild(err_msg);
+            $('msgs').appendChild(err_box);
+            err_msg.appendChild(
+                document.createTextNode(
+                    $('patronStrings').getFormattedString('staff.item.batch.hold.x_failed_holds',[ failures[k].length, k ])
+                )
+            );
+            addCSSClass(err_msg,'click_link');
+            err_msg.addEventListener(
+                'click',
+                function(copy_ids) {
+                    return function(ev) {
+                        xulG.new_tab(
+                            urls.XUL_COPY_STATUS,
+                            {},
+                            {
+                                'copy_ids' : copy_ids
+                            }
+                        );
+                    }
+                }(failures[k]),
+                false
+            );
+            var retry_btn = document.createElement('button');
+            retry_btn.setAttribute(
+                'label',
+                $('patronStrings').getString('staff.item.batch.hold.retry_btn_label')
+            );
+            err_box.appendChild(retry_btn);
+
+            retry_btn.addEventListener(
+                'command',
+                function(copy_ids) {
+                    return function(ev) {
+                        ev.target.disabled = true;
+                        ev.target.hidden = true;
+                        ev.target.nextSibling.disabled = true;
+                        ev.target.nextSibling.hidden = true;
+                        make_request(copy_ids,false);
+                    }
+                }(failures[k]),
+                false
+            );
+
+            var override_btn = document.createElement('button');
+            override_btn.setAttribute(
+                'label',
+                $('patronStrings').getString('staff.item.batch.hold.override_btn_label')
+            );
+            err_box.appendChild(override_btn);
+
+            override_btn.addEventListener(
+                'command',
+                function(copy_ids) {
+                    return function(ev) {
+                        ev.target.disabled = true;
+                        ev.target.hidden = true;
+                        ev.target.previousSibling.disabled = true;
+                        ev.target.previousSibling.hidden = true;
+                        make_request(copy_ids,true);
+                    }
+                }(failures[k]),
+                false
+            );
+
+        }
+    } catch(E) {
+        alert('Error in place_hold.js, handle_failures(): ' + E);
+    }
+}
+
+function set_remaining_event_listeners() {
+    try {
+
+        $('hold_type_menu').addEventListener(
+            'command',
+            function(ev) { oils_lock_page(); },
+            false
+        );
+
+        $('cancel_btn').addEventListener(
+            'command',
+            function(ev) { xulG.close_tab(); },
+            false
+        );
+
+    } catch(E) {
+        alert('Error in place_hold.js, set_remaining_event_listeners(): ' + E);
+    } 
+}
+
+function populate_hold_usr_textbox() {
+    JSAN.use('patron.util');
+    hold_usr = ses('staff_id');
+    var au_obj = patron.util.retrieve_fleshed_au_via_id(
+        ses(),
+        hold_usr,
+        ["card"]);
+    $('hold_usr_textbox').value = au_obj.card().barcode();
+    $('hold_usr_textbox').select();
+    $('hold_usr_textbox').focus();
+    $('hold_usr_name').setAttribute(
+        'value',
+        patron.util.format_name(au_obj)
+    );
+    $('hold_usr_textbox').addEventListener(
+        'change',
+        function(ev) {
+            try {
+                oils_lock_page();
+                var au_obj = patron.util.retrieve_fleshed_au_via_barcode(
+                    ses(),
+                    ev.target.value
+                );
+                if (typeof au_obj.textcode == 'undefined') {
+                    hold_usr = au_obj.id();
+                    $('hold_usr_name').setAttribute(
+                        'value',
+                        patron.util.format_name(au_obj)
+                    );
+                    removeCSSClass($('hold_usr_name'),'failure_text');
+                } else {
+                    hold_usr = null;
+                    $('hold_usr_name').setAttribute(
+                        'value',
+                        $('patronStrings').getString('staff.item.batch.hold.user_not_found')
+                    );
+                    addCSSClass($('hold_usr_name'),'failure_text');
+                }
+            } catch(E) {
+                alert('Error in place_hold.js, hold_usr handler: ' + E);
+            }
+        },
+        false
+    );
+}
+
+function populate_pickup_lib_menu() {
+    try {
+        JSAN.use('util.widgets');
+        JSAN.use('util.functional');
+
+        util.widgets.remove_children('pickup_lib_menu_placeholder');
+
+        var list = util.functional.map_list(
+            data.list.aou,
+            function(o) {
+                var sname = o.shortname();
+                for (i = sname.length; i < 20; i++) sname += ' ';
+                return [
+                    o.name() ? sname + ' ' + o.name() : o.shortname(),
+                    o.id(),
+                    ( !isTrue(data.hash.aout[ o.ou_type() ].can_have_users()) ),
+                    ( data.hash.aout[ o.ou_type() ].depth() * 2),
+                ];
+            }
+        );
+        ml = util.widgets.make_menulist( list, data.list.au[0].ws_ou() );
+        ml.setAttribute('id','pickup_lib_menu');
+
+        $('pickup_lib_menu_placeholder').appendChild(ml);
+
+        ml.addEventListener(
+            'command',
+            function(ev) { oils_lock_page(); },
+            false
+        );
+
+    } catch(E) {
+        alert('Error in place_hold.js, populate_pickup_lib_menu(): ' + E);
+    } 
+}
+
+function ui_setup() {
+    netscape.security.PrivilegeManager.enablePrivilege(
+        "UniversalXPConnect");
+    if (typeof JSAN == 'undefined') {
+        throw( "The JSAN library object is missing.");
+    }
+    JSAN.errorLevel = "die"; // none, warn, or die
+    JSAN.addRepository('/xul/server/');
+    JSAN.use('util.error');
+    error = new util.error();
+
+    if (typeof xulG == 'object' && typeof xulG.set_tab_name == 'function') {
+        try {
+            xulG.set_tab_name(
+                $('patronStrings').getString('staff.item.batch.hold.tab_name')
+            );
+        } catch(E) {
+            alert(E);
+        }
+    }
+
+}
diff --git a/Open-ILS/xul/staff_client/server/patron/place_hold.xul b/Open-ILS/xul/staff_client/server/patron/place_hold.xul
new file mode 100644
index 0000000000..afa0a5539d
--- /dev/null
+++ b/Open-ILS/xul/staff_client/server/patron/place_hold.xul
@@ -0,0 +1,105 @@
+<?xml version="1.0"?>
+<!-- Application: Evergreen Staff Client -->
+<!-- Screen: Item Hold/Recall Placement -->
+
+<!-- /////////////////////////////////////////////////////////////////////// -->
+<!-- STYLESHEETS -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/global.css" type="text/css"?>
+
+<!-- /////////////////////////////////////////////////////////////////////// -->
+<!-- LOCALIZATION -->
+<!DOCTYPE window PUBLIC "" ""[
+    <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+
+<!-- /////////////////////////////////////////////////////////////////////// -->
+<!-- OVERLAYS -->
+<?xul-overlay href="/xul/server/OpenILS/util_overlay.xul"?>
+
+<window id="place_hold_win" 
+    onload="try{my_init();font_helper();persist_helper();}catch(E){alert(E);}"
+    xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+    <!-- /////////////////////////////////////////////////////////////////// -->
+    <!-- BEHAVIOR -->
+    <script type="text/javascript">
+        var myPackageDir = 'open_ils_staff_client'; var IAMXUL = true;
+    </script>
+    <scripts id="openils_util_scripts"/>
+
+    <messagecatalog id="patronStrings"
+        src='/xul/server/locale/<!--#echo var="locale"-->/patron.properties'/>
+
+    <script type="text/javascript" src="/xul/server/main/JSAN.js"/>
+    <script type="text/javascript" src="place_hold.js"/>
+
+    <vbox flex="1">
+    <groupbox>
+        <caption label="&staff.item.batch.hold.groupbox_caption;"/>
+
+        <grid>
+            <columns>
+                <column/>
+                <column/>
+            </columns>
+            <rows>
+                <row>
+                    <label control="hold_usr_textbox"
+                        value="&staff.item.batch.hold.hold_usr.label;"
+                        accesskey="&staff.item.batch.hold.hold_usr.accesskey;"/>
+                    <textbox id="hold_usr_textbox"/>
+                </row>
+                <row>
+                    <spacer/>
+                    <label id="hold_usr_name"/>
+                </row>
+                <row>
+                    <label control="hold_type_menu"
+                        value="&staff.item.batch.hold.hold_type_menu.label;"
+                        accesskey="&staff.item.batch.hold.hold_type_menu.accesskey;"/>
+                    <hbox>
+                        <menulist id="hold_type_menu" oils_persist="value">
+                            <menupopup>
+                                <menuitem value="C"
+                                    label="&staff.item.batch.hold.hold_type_menuentry.copy_hold;"/>
+                                <menuitem value="R"
+                                    label="&staff.item.batch.hold.hold_type_menuentry.recall_hold;"/>
+                                <menuitem value="F"
+                                    label="&staff.item.batch.hold.hold_type_menuentry.force_hold;"/>
+                            </menupopup>
+                        </menulist>
+                    </hbox>
+                </row>
+                <row>
+                    <label
+                        value="&staff.item.batch.hold.pick_up_lib_menu.label;"
+                        accesskey="&staff.item.batch.hold.pick_up_lib_menu.accesskey;"/>
+                    <hbox id="pickup_lib_menu_placeholder"/>
+                </row>
+                <row>
+                    <spacer/>
+                    <hbox>
+                        <button id="cancel_btn"
+                            label="&staff.item.batch.hold.cancel_btn.label;"
+                            accesskey="&staff.item.batch.hold.cancel_btn.accesskey;"/>
+                        <button id="request_btn"
+                            label="&staff.item.batch.hold.request_btn.label;"
+                            accesskey="&staff.item.batch.hold.request_btn.accesskey;"/>
+                        <progressmeter id="progress_meter"
+                            mode="undetermined"
+                            hidden="true" />
+                    </hbox>
+                </row>
+            </rows>
+        </grid>
+
+    </groupbox>
+    <description id="desc" hidden="true">
+        &staff.item.batch.hold.failures_and_settings;
+    </description>
+    <vbox id="msgs" flex="1" class="my_overflow"/>
+    </vbox>
+
+</window>
+
-- 
2.11.0