Add P-type holds for monograph parts, similar to issuance holds
authorMike Rylander <mrylander@gmail.com>
Tue, 15 Feb 2011 21:12:10 +0000 (16:12 -0500)
committerMike Rylander <mrylander@gmail.com>
Tue, 15 Feb 2011 21:12:10 +0000 (16:12 -0500)
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
Open-ILS/src/perlmods/lib/OpenILS/Const.pm

index 7498f4b..4bf3f9c 100644 (file)
@@ -183,6 +183,8 @@ sub create_hold {
         return $e->die_event unless $e->allowed('TITLE_HOLDS',  $porg);
     } elsif ( $t eq OILS_HOLD_TYPE_VOLUME ) {
         return $e->die_event unless $e->allowed('VOLUME_HOLDS', $porg);
+    } elsif ( $t eq OILS_HOLD_TYPE_MONOPART ) {
+        return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
     } elsif ( $t eq OILS_HOLD_TYPE_ISSUANCE ) {
         return $e->die_event unless $e->allowed('ISSUANCE_HOLDS', $porg);
     } elsif ( $t eq OILS_HOLD_TYPE_COPY ) {
@@ -1947,6 +1949,7 @@ The named fields in the hash are:
  pickup_lib   - destination for hold, fallback value for selection_ou
  selection_ou - ID of org_unit establishing hard and soft hold boundary settings
  issuanceid   - ID of the issuance to be held, required for Issuance level hold
+ partid       - ID of the monograph part to be held, required for monograph part level hold
  titleid      - ID (BRN) of the title to be held, required for Title level hold
  volume_id    - required for Volume level hold
  copy_id      - required for Copy level hold
@@ -2038,6 +2041,7 @@ sub do_possibility_checks {
     my($e, $patron, $request_lib, $depth, %params) = @_;
 
     my $issuanceid   = $params{issuanceid}      || "";
+    my $partid       = $params{partid}      || "";
     my $titleid      = $params{titleid}      || "";
     my $volid        = $params{volume_id};
     my $copyid       = $params{copy_id};
@@ -2082,6 +2086,12 @@ sub do_possibility_checks {
                        $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
         );
 
+       } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
+
+               return _check_monopart_hold_is_possible(
+                       $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
+        );
+
        } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
 
                my $maps = $e->search_metabib_metarecord_source_map({metarecord=>$mrid});
@@ -2379,6 +2389,140 @@ sub _check_issuance_hold_is_possible {
     return @status;
 }
 
+sub _check_monopart_hold_is_possible {
+    my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
+   
+    my $e = new_editor();
+    my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
+
+    # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
+    my $copies = $e->json_query(
+        { 
+            select => { acp => ['id', 'circ_lib'] },
+              from => {
+                acp => {
+                    acpm => {
+                        field  => 'target_copy',
+                        fkey   => 'id',
+                        filter => { part => $partid }
+                    },
+                    acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
+                    ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   }
+                }
+            }, 
+            where => {
+                '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
+            },
+            distinct => 1
+        }
+    );
+
+    $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
+
+    my $empty_ok;
+    if (!@$copies) {
+        $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
+        $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
+
+        return (
+            0, 0, [
+                new OpenILS::Event(
+                    "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
+                    "payload" => {"fail_part" => "no_ultimate_items"}
+                )
+            ]
+        ) unless $empty_ok;
+
+        return (1, 0);
+    }
+
+    # -----------------------------------------------------------------------
+    # sort the copies into buckets based on their circ_lib proximity to 
+    # the patron's home_ou.  
+    # -----------------------------------------------------------------------
+
+    my $home_org = $patron->home_ou;
+    my $req_org = $request_lib->id;
+
+    $logger->info("prox cache $home_org " . $prox_cache{$home_org});
+
+    $prox_cache{$home_org} = 
+        $e->search_actor_org_unit_proximity({from_org => $home_org})
+        unless $prox_cache{$home_org};
+    my $home_prox = $prox_cache{$home_org};
+
+    my %buckets;
+    my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
+    push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
+
+    my @keys = sort { $a <=> $b } keys %buckets;
+
+
+    if( $home_org ne $req_org ) {
+      # -----------------------------------------------------------------------
+      # shove the copies close to the request_lib into the primary buckets 
+      # directly before the farthest away copies.  That way, they are not 
+      # given priority, but they are checked before the farthest copies.
+      # -----------------------------------------------------------------------
+        $prox_cache{$req_org} = 
+            $e->search_actor_org_unit_proximity({from_org => $req_org})
+            unless $prox_cache{$req_org};
+        my $req_prox = $prox_cache{$req_org};
+
+        my %buckets2;
+        my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
+        push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
+
+        my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
+        my $new_key = $highest_key - 0.5; # right before the farthest prox
+        my @keys2   = sort { $a <=> $b } keys %buckets2;
+        for my $key (@keys2) {
+            last if $key >= $highest_key;
+            push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
+        }
+    }
+
+    @keys = sort { $a <=> $b } keys %buckets;
+
+    my $title;
+    my %seen;
+    my @status;
+    OUTER: for my $key (@keys) {
+      my @cps = @{$buckets{$key}};
+
+      $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
+
+      for my $copyid (@cps) {
+
+         next if $seen{$copyid};
+         $seen{$copyid} = 1; # there could be dupes given the merged buckets
+         my $copy = $e->retrieve_asset_copy($copyid);
+         $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
+
+         unless($title) { # grab the title if we don't already have it
+            my $vol = $e->retrieve_asset_call_number(
+               [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
+            $title = $vol->record;
+         }
+   
+         @status = verify_copy_for_hold(
+            $patron, $requestor, $title, $copy, $pickup_lib, $request_lib);
+
+         last OUTER if $status[0];
+      }
+    }
+
+    if (!$status[0]) {
+        if (!defined($empty_ok)) {
+            $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
+            $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
+        }
+
+        return (1,0) if ($empty_ok);
+    }
+    return @status;
+}
+
 
 sub _check_volume_hold_is_possible {
        my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
@@ -2691,7 +2835,7 @@ sub uber_hold_impl {
        my $card = $e->retrieve_actor_card($user->card)
                or return $e->event;
 
-       my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
+       my( $mvr, $volume, $copy, $issuance, $part ) = find_hold_mvr($e, $hold);
 
        flesh_hold_notices([$hold], $e);
        flesh_hold_transits([$hold]);
@@ -2702,6 +2846,8 @@ sub uber_hold_impl {
         hold           => $hold,
         copy           => $copy,
         volume         => $volume,
+        issuance       => $issuance,
+        part           => $part,
         mvr            => $mvr,
         patron_first   => $user->first_given_name,
         patron_last    => $user->family_name,
@@ -2724,6 +2870,7 @@ sub find_hold_mvr {
        my $copy;
        my $volume;
     my $issuance;
+    my $part;
 
        if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
                my $mr = $e->retrieve_metabib_metarecord($hold->target)
@@ -2746,6 +2893,14 @@ sub find_hold_mvr {
 
         $tid = $issuance->subscription->record_entry;
 
+    } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
+        $part = $e->retrieve_biblio_monographic_part([
+            $hold->target,
+            {flesh => 1, flesh_fields => {bmp => [ qw/record/ ]}}
+        ]) or return $e->event;
+
+        $tid = $part->record;
+
        } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
                $copy = $e->retrieve_asset_copy([
             $hold->target, 
@@ -2767,7 +2922,7 @@ sub find_hold_mvr {
 
     # TODO return metarcord mvr for M holds
        my $title = $e->retrieve_biblio_record_entry($tid);
-       return ( $U->record_to_mvr($title), $volume, $copy, $issuance );
+       return ( $U->record_to_mvr($title), $volume, $copy, $issuance, $part );
 }
 
 __PACKAGE__->register_method(
@@ -3133,6 +3288,14 @@ sub hold_item_is_checked_out {
 
         $query->{where}->{'+acp'}->{call_number} = $hold_target;
 
+     } elsif($hold_type eq 'P') {
+
+        $query->{from}->{acp}->{acpm} = {
+            field  => 'target_copy',
+            fkey   => 'id',
+            filter => {part => $hold_target},
+        };
+
      } elsif($hold_type eq 'I') {
 
         $query->{from}->{acp}->{sitem} = {
@@ -3298,7 +3461,7 @@ sub rec_hold_count {
                 '-or' => [
                     {
                         '-and' => {
-                            hold_type => 'C',
+                            hold_type => [qw/C F R/],
                             target => {
                                 in => {
                                     select => {acp => ['id']},
index 6b96ffa..75f05ee 100644 (file)
@@ -1241,6 +1241,15 @@ sub new_hold_copy_targeter {
                                                { id => [map {$_->id} @{ $vtree->copies }],
                                                  deleted => 'f' }
                                        ) if ($vtree && @{ $vtree->copies });
+
+                       } elsif ($hold->hold_type eq 'P') {
+                               my @part_maps = asset::copy_part_map->search_where( { part => $hold->target } );
+                               $all_copies = [
+                                       asset::copy->search_where(
+                                               { id => [map {$_->target_copy} @part_maps],
+                                                 deleted => 'f' }
+                                       )
+                               ] if (@part_maps);
                                        
                        } elsif ($hold->hold_type eq 'I') {
                                my ($itree) = $self
index 281f465..6da2ed6 100644 (file)
@@ -102,6 +102,7 @@ econst OILS_HOLD_TYPE_ISSUANCE    => 'I';
 econst OILS_HOLD_TYPE_VOLUME      => 'V';
 econst OILS_HOLD_TYPE_TITLE       => 'T';
 econst OILS_HOLD_TYPE_METARECORD  => 'M';
+econst OILS_HOLD_TYPE_MONOPART    => 'P';
 
 
 econst OILS_BILLING_TYPE_OVERDUE_MATERIALS => 'Overdue materials';