Auto-retarget local holds on "new" copy checkin
authorThomas Berezansky <tsbere@mvlc.org>
Thu, 14 Jul 2011 20:49:05 +0000 (16:49 -0400)
committerJason Etheridge <jason@esilibrary.com>
Wed, 27 Jul 2011 20:36:40 +0000 (16:36 -0400)
When a new item is checked in local holds will be searched and re-targeted.

The search and retarget stops when a hold is found locally that the item can fill.

Already-captured holds will not be retargeted.

"New" is (currently) defined as "In progress" at checkin

Retarget occurs:
Only if the copy's circ library is the checkin library
Only if the checkin is an actual checkin (not a renewal)
Only if the checkin is capturing holds and transits

Signed-off-by: Thomas Berezansky <tsbere@mvlc.org>
Signed-off-by: Jason Etheridge <jason@esilibrary.com>

Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
Open-ILS/web/opac/locale/en-US/lang.dtd
Open-ILS/xul/staff_client/server/circ/checkin.js
Open-ILS/xul/staff_client/server/circ/checkin_overlay.xul
Open-ILS/xul/staff_client/server/skin/circ.css

index 41f90e2..dd114b1 100644 (file)
@@ -536,6 +536,7 @@ my @AUTOLOAD_FIELDS = qw/
     use_booking
     generate_lost_overdue
     clear_expired
+    retarget_mode
 /;
 
 
@@ -2294,6 +2295,73 @@ sub check_transit_checkin_interval {
         if $horizon > DateTime->now;
 }
 
+# Retarget local holds at checkin
+sub checkin_retarget {
+    my $self = shift;
+    return unless $self->retarget_mode =~ m/retarget/; # Retargeting?
+    return unless $self->is_checkin; # Renewals need not be checked
+    return if $self->capture eq 'nocapture'; # Not capturing holds anyway? Move on.
+    return if $self->is_precat; # No holds for precats
+    return unless $self->circ_lib == $self->copy->circ_lib; # Item isn't "home"? Don't check.
+    return unless $self->copy->holdable; # Not holdable, shouldn't capture holds.
+    # Specifically target items that are likely new (by status ID)
+    unless ($self->retarget_mode =~ m/\.all/) {
+        my $status = $U->copy_status($self->copy->status)->id;
+        return unless $status == OILS_COPY_STATUS_IN_PROCESS;
+    }
+
+    # Fetch holds for the bib
+    my ($result) = $holdcode->method_lookup('open-ils.circ.holds.retrieve_all_from_title')->run(
+                    $self->editor->authtoken,
+                    $self->title->id,
+                    {
+                        capture_time => undef, # No touching captured holds
+                        frozen => 'f', # Don't bother with frozen holds
+                        pickup_lib => $self->circ_lib # Only holds actually here
+                    }); 
+
+    # Error? Skip the step.
+    return if exists $result->{"ilsevent"};
+
+    # Assemble holds
+    my $holds = [];
+    foreach my $holdlist (keys %{$result}) {
+        push @$holds, @{$result->{$holdlist}};
+    }
+
+    return if scalar(@$holds) == 0; # No holds, no retargeting
+
+    # Loop over holds in request-ish order
+    # Stage 1: Get them into request-ish order
+    # Also grab type and target for skipping low hanging ones
+    $result = $self->editor->json_query({
+        "select" => { "ahr" => ["id", "hold_type", "target"] },
+        "from" => { "ahr" => { "au" => { "fkey" => "usr",  "join" => "pgt"} } },
+        "where" => { "id" => $holds },
+        "order_by" => [
+            { "class" => "pgt", "field" => "hold_priority"},
+            { "class" => "ahr", "field" => "cut_in_line", "direction" => "desc", "transform" => "coalesce", "params" => ['f']},
+            { "class" => "ahr", "field" => "selection_depth", "direction" => "desc"},
+            { "class" => "ahr", "field" => "request_time"}
+        ]
+    });
+
+    # Stage 2: Loop!
+    if (ref $result eq "ARRAY" and scalar @$result) {
+        foreach (@{$result}) {
+            # Copy level, but not this copy?
+            next if ($_->{hold_type} eq 'C' or $_->{hold_type} eq 'R' or $_->{hold_type} eq 'F'
+                and $_->{target} != $self->copy->id);
+            # Volume level, but not this volume?
+            next if ($_->{hold_type} eq 'V' and $_->{target} != $self->volume->id);
+            # So much for easy stuff, attempt a retarget!
+            my $tresult = $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $_->{id}, $self->copy->id);
+            if(ref $tresult eq "ARRAY" and scalar @$tresult) {
+                last if(exists $tresult->[0]->{found_copy} and $tresult->[0]->{found_copy});
+            }
+        }
+    }
+}
 
 sub do_checkin {
     my $self = shift;
@@ -2304,6 +2372,7 @@ sub do_checkin {
         unless $self->copy;
 
     $self->check_transit_checkin_interval;
+    $self->checkin_retarget;
 
     # the renew code and mk_env should have already found our circulation object
     unless( $self->circ ) {
index 23717a7..d54575d 100644 (file)
@@ -1042,6 +1042,7 @@ sub new_hold_copy_targeter {
        my $client = shift;
        my $check_expire = shift;
        my $one_hold = shift;
+    my $find_copy = shift;
 
        local $OpenILS::Application::Storage::WRITE = 1;
 
@@ -1288,6 +1289,8 @@ sub new_hold_copy_targeter {
                        }
 
                        my $copy_count = @$all_copies;
+            my $found_copy = undef;
+            $found_copy = 1 if($find_copy and grep $_ == $find_copy, @$all_copies);
 
                        # map the potentials, so that we can pick up checkins
                        # XXX Loop-based targeting may require that /only/ copies from this loop should be added to
@@ -1489,7 +1492,8 @@ sub new_hold_copy_targeter {
                                { hold => $hold->id,
                                  old_target => ($old_best ? $old_best->id : undef),
                                  eligible_copies => $copy_count,
-                                 target => ($best ? $best->id : undef) };
+                                 target => ($best ? $best->id : undef),
+                  found_copy => $found_copy };
 
                } otherwise {
                        my $e = shift;
index 5be57e4..822311f 100644 (file)
 <!ENTITY staff.circ.checkin_overlay.checkin_modifiers_btn.accesskey "M">
 <!ENTITY staff.circ.checkin_overlay.checkin_clear_shelf_expired.label "Clear Shelf-Expired Holds">
 <!ENTITY staff.circ.checkin_overlay.checkin_clear_shelf_expired.accesskey "C">
+<!ENTITY staff.circ.checkin_overlay.checkin_auto_retarget.label "Retarget Local Holds">
+<!ENTITY staff.circ.checkin_overlay.checkin_auto_retarget.accesskey "R">
+<!ENTITY staff.circ.checkin_overlay.checkin_auto_retarget_all.label "Retarget All Statuses">
+<!ENTITY staff.circ.checkin_overlay.checkin_auto_retarget_all.accesskey "e">
+<!ENTITY staff.circ.checkin_overlay.checkin_auto_retarget_all_ind.label "Always Retarget Local Holds">
 <!ENTITY staff.circ.renew_overlay.background_text "Renew Item">
 <!ENTITY staff.circ.renew_overlay.sel_clip.label "Copy to Clipboard">
 <!ENTITY staff.circ.renew_overlay.sel_clip.accesskey "C">
index 9241083..6984150 100644 (file)
@@ -418,6 +418,36 @@ circ.checkin.prototype = {
                         ind.hidden = cb.getAttribute('checked') != 'true'; 
                         document.getElementById('checkin_barcode_entry_textbox').focus();
                         return true;
+                    } ],
+                    'cmd_checkin_auto_retarget' : [ ['command'], function(ev) {
+                        dump('in cmd_checkin_auto_retarget\n');
+                        var bg = document.getElementById('background');
+                        var cb = document.getElementById('checkin_auto_retarget');
+                        var cb2 = document.getElementById('checkin_auto_retarget_all');
+                        var ind = document.getElementById('checkin_auto_retarget_indicator');
+                        var ind2 = document.getElementById('checkin_auto_retarget_all_indicator');
+                        var cn = 'checkin_screen_checkin_auto_retarget';
+                        var cn2 = 'checkin_screen_checkin_auto_retarget_all';
+                        if (cb.getAttribute('checked') == 'true') {
+                            if(cb2.getAttribute('checked') == 'true') {
+                                removeCSSClass(bg,cn);
+                                addCSSClass(bg,cn2);
+                                ind.hidden = true;
+                                ind2.hidden = false;
+                            } else {
+                                addCSSClass(bg,cn);
+                                removeCSSClass(bg,cn2);
+                                ind.hidden = false;
+                                ind2.hidden = true;
+                            }
+                        } else {
+                            removeCSSClass(bg,cn);
+                            removeCSSClass(bg,cn2);
+                            ind.hidden = true;
+                            ind2.hidden = true;
+                        }
+                        document.getElementById('checkin_barcode_entry_textbox').focus();
+                        return true;
                     } ]
                 }
             }
@@ -580,6 +610,14 @@ circ.checkin.prototype = {
             var clear_shelf_expired_holds = document.getElementById('checkin_clear_shelf_expired');
             if (clear_shelf_expired_holds) clear_shelf_expired_holds = clear_shelf_expired_holds.getAttribute('checked') == 'true';
             if (clear_shelf_expired_holds) params.clear_expired = 1;
+            var auto_retarget = document.getElementById('checkin_auto_retarget');
+            if (auto_retarget) auto_retarget = auto_retarget.getAttribute('checked') == 'true';
+            if (auto_retarget) {
+                var retarget_all = document.getElementById('checkin_auto_retarget_all');
+                if (retarget_all) retarget_all = retarget_all.getAttribute('checked') == 'true';
+                if (retarget_all) params.retarget_mode = 'retarget.all';
+                else params.retarget_mode = 'retarget';
+            }
             circ.util.checkin_via_barcode(
                 ses(), 
                 params,
index 4ea1a74..18dff0e 100644 (file)
@@ -36,6 +36,7 @@
     <command id="cmd_amnesty_mode" />
     <command id="cmd_checkin_auto_print_slips" />
     <command id="cmd_checkin_clear_shelf_expired" />
+    <command id="cmd_checkin_auto_retarget" />
 </commandset>
 
 
@@ -76,6 +77,8 @@
                 <description id="amnesty_mode_indicator" hidden="true">&staff.circ.checkin_overlay.amnesty_mode.label;</description>
                 <description id="checkin_auto_print_slips_indicator" hidden="true">&staff.circ.checkin_overlay.checkin_auto_print_slips.label;</description>
                 <description id="checkin_clear_shelf_expired_indicator" hidden="true">&staff.circ.checkin_overlay.checkin_clear_shelf_expired.label;</description>
+                <description id="checkin_auto_retarget_indicator" hidden="true">&staff.circ.checkin_overlay.checkin_auto_retarget.label;</description>
+                <description id="checkin_auto_retarget_all_indicator" hidden="true">&staff.circ.checkin_overlay.checkin_auto_retarget_all_ind.label;</description>
             </vbox>
         </vbox>
         <spacer flex="1"/>
                 label="&staff.circ.checkin_overlay.checkin_auto_print_slips.label;" accesskey="&staff.circ.checkin_overlay.checkin_auto_print_slips.accesskey;"/>
             <menuitem type="checkbox" id="checkin_clear_shelf_expired" oils_persist="checked" command="cmd_checkin_clear_shelf_expired"
                 label="&staff.circ.checkin_overlay.checkin_clear_shelf_expired.label;" accesskey="&staff.circ.checkin_overlay.checkin_clear_shelf_expired.accesskey;"/>
+            <menuitem type="checkbox" id="checkin_auto_retarget" oils_persist="checked" command="cmd_checkin_auto_retarget"
+                label="&staff.circ.checkin_overlay.checkin_auto_retarget.label;" accesskey="&staff.circ.checkin_overlay.checkin_auto_retarget.accesskey;"/>
+            <menuitem type="checkbox" id="checkin_auto_retarget_all" oils_persist="checked" command="cmd_checkin_auto_retarget"
+                label="&staff.circ.checkin_overlay.checkin_auto_retarget_all.label;" accesskey="&staff.circ.checkin_overlay.checkin_auto_retarget_all.accesskey;"/>
         </menupopup>
     </button>
 </hbox>
index 049e4fb..d73071f 100644 (file)
@@ -46,6 +46,8 @@ treechildren::-moz-tree-row(backdate_failed,selected) {
 .checkin_screen_amnesty_mode { }
 .checkin_screen_checkin_auto_print_slips { }
 .checkin_screen_checkin_clear_shelf_expired { }
+.checkin_screen_checkin_auto_retarget { }
+.checkin_screen_checkin_auto_retarget_all { }
 
 #background-text { font-size: x-large; font-weight: bold; }
 #do_not_alert_on_precat_indicator { background-color: -moz-dialog; color: -moz-dialog-text; font-size: large; font-weight: bold; }
@@ -53,6 +55,8 @@ treechildren::-moz-tree-row(backdate_failed,selected) {
 #amnesty_mode_indicator { border: thick solid white; background-color: red; color: white; font-size: large; font-weight: bold; padding: 10px; padding-bottom: 25px; margin: 10px; }
 #checkin_auto_print_slips_indicator { background-color: -moz-dialog; color: -moz-dialog-text; font-size: large; font-weight: bold; }
 #checkin_clear_shelf_expired_indicator { background-color: -moz-dialog; color: -moz-dialog-text; font-size: large; font-weight: bold; }
+#checkin_auto_retarget_indicator { background-color: -moz-dialog; color: -moz-dialog-text; font-size: large; font-weight: bold; }
+#checkin_auto_retarget_all_indicator { background-color: -moz-dialog; color: -moz-dialog-text; font-size: large; font-weight: bold; }
 
 .big_emphasis1 { font-weight: bold; font-size: x-large; }
 .big_emphasis2 { font-weight: bold; font-size: large; }