X-Git-Url: http://git.rot13.org/?a=blobdiff_plain;f=C4%2FHoldsQueue.pm;h=d55e7e3161ef7499f486de971e29d407741b2d41;hb=ca28c83abbd449c7e5f1d7b86760b7fb53741988;hp=7d389634f4e1ae71f0eb8bdc81ee67e3a8bdd2ce;hpb=94f361f9ea554b660974abc37a7eacb1c2ddfd0d;p=koha.git diff --git a/C4/HoldsQueue.pm b/C4/HoldsQueue.pm index 7d389634f4..d55e7e3161 100755 --- a/C4/HoldsQueue.pm +++ b/C4/HoldsQueue.pm @@ -4,18 +4,18 @@ package C4::HoldsQueue; # # This file is part of Koha. # -# Koha is free software; you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software -# Foundation; either version 2 of the License, or (at your option) any later -# version. +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. # -# Koha is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. # -# You should have received a copy of the GNU General Public License along -# with Koha; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . # FIXME: expand perldoc, explain intended logic @@ -25,18 +25,19 @@ use warnings; use C4::Context; use C4::Search; use C4::Items; -use C4::Branch; use C4::Circulation; use C4::Members; use C4::Biblio; -use C4::Dates qw/format_date/; +use Koha::DateUtils; +use Koha::Items; +use Koha::Patrons; use List::Util qw(shuffle); +use List::MoreUtils qw(any); use Data::Dumper; -use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); +use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); BEGIN { - $VERSION = 3.03; require Exporter; @ISA = qw(Exporter); @EXPORT_OK = qw( @@ -63,13 +64,25 @@ sub TransportCostMatrix { my $dbh = C4::Context->dbh; my $transport_costs = $dbh->selectall_arrayref("SELECT * FROM transport_cost",{ Slice => {} }); + my $today = dt_from_string(); + my $calendars; my %transport_cost_matrix; foreach (@$transport_costs) { - my $from = $_->{frombranch}; - my $to = $_->{tobranch}; - my $cost = $_->{cost}; + my $from = $_->{frombranch}; + my $to = $_->{tobranch}; + my $cost = $_->{cost}; my $disabled = $_->{disable_transfer}; - $transport_cost_matrix{$to}{$from} = { cost => $cost, disable_transfer => $disabled }; + $transport_cost_matrix{$to}{$from} = { + cost => $cost, + disable_transfer => $disabled + }; + + if ( C4::Context->preference("HoldsQueueSkipClosed") ) { + $calendars->{$from} ||= Koha::Calendar->new( branchcode => $from ); + $transport_cost_matrix{$to}{$from}{disable_transfer} ||= + $calendars->{$from}->is_holiday( $today ); + } + } return \%transport_cost_matrix; } @@ -89,7 +102,7 @@ sub UpdateTransportCostMatrix { my $sth = $dbh->prepare("INSERT INTO transport_cost (frombranch, tobranch, cost, disable_transfer) VALUES (?, ?, ?, ?)"); - $dbh->do("TRUNCATE TABLE transport_cost"); + $dbh->do("DELETE FROM transport_cost"); foreach (@$records) { my $cost = $_->{cost}; my $from = $_->{frombranch}; @@ -98,7 +111,7 @@ sub UpdateTransportCostMatrix { $cost ||= 0; } elsif ( !defined ($cost) || ($cost !~ m/(0|[1-9][0-9]*)(\.[0-9]*)?/o) ) { - warn "Invalid $from -> $to cost $cost - must be a number >= 0, disablig"; + warn "Invalid $from -> $to cost $cost - must be a number >= 0, disabling"; $cost = 0; $_->{disable_transfer} = 1; } @@ -119,7 +132,7 @@ sub GetHoldsQueueItems { my $dbh = C4::Context->dbh; my @bind_params = (); - my $query = q/SELECT tmp_holdsqueue.*, biblio.author, items.ccode, items.location, items.enumchron, items.cn_sort, biblioitems.publishercode,biblio.copyrightdate,biblioitems.publicationyear,biblioitems.pages,biblioitems.size,biblioitems.publicationyear,biblioitems.isbn,items.copynumber + my $query = q/SELECT tmp_holdsqueue.*, biblio.author, items.ccode, items.itype, biblioitems.itemtype, items.location, items.enumchron, items.cn_sort, biblioitems.publishercode,biblio.copyrightdate,biblioitems.publicationyear,biblioitems.pages,biblioitems.size,biblioitems.publicationyear,biblioitems.isbn,items.copynumber FROM tmp_holdsqueue JOIN biblio USING (biblionumber) LEFT JOIN biblioitems USING (biblionumber) @@ -134,13 +147,19 @@ sub GetHoldsQueueItems { $sth->execute(@bind_params); my $items = []; while ( my $row = $sth->fetchrow_hashref ){ - $row->{reservedate} = format_date($row->{reservedate}); - my $record = GetMarcBiblio($row->{biblionumber}); + my $record = GetMarcBiblio({ biblionumber => $row->{biblionumber} }); if ($record){ - $row->{subtitle} = GetRecordValue('subtitle',$record,'')->[0]->{subfield}; + $row->{subtitle} = [ map { $_->{subfield} } @{ GetRecordValue( 'subtitle', $record, '' ) } ]; $row->{parts} = GetRecordValue('parts',$record,'')->[0]->{subfield}; $row->{numbers} = GetRecordValue('numbers',$record,'')->[0]->{subfield}; } + + # return the bib-level or item-level itype per syspref + if (!C4::Context->preference('item-level_itypes')) { + $row->{itype} = $row->{itemtype}; + } + delete $row->{itemtype}; + push @$items, $row; } return $items; @@ -222,7 +241,9 @@ sub GetBibsWithPendingHoldRequests { FROM reserves WHERE found IS NULL AND priority > 0 - AND reservedate <= CURRENT_DATE()"; + AND reservedate <= CURRENT_DATE() + AND suspend = 0 + "; my $sth = $dbh->prepare($bib_query); $sth->execute(); @@ -258,13 +279,14 @@ sub GetPendingHoldRequestsForBib { my $dbh = C4::Context->dbh; my $request_query = "SELECT biblionumber, borrowernumber, itemnumber, priority, reserves.branchcode, - reservedate, reservenotes, borrowers.branchcode AS borrowerbranch + reservedate, reservenotes, borrowers.branchcode AS borrowerbranch, itemtype FROM reserves JOIN borrowers USING (borrowernumber) WHERE biblionumber = ? AND found IS NULL AND priority > 0 AND reservedate <= CURRENT_DATE() + AND suspend = 0 ORDER BY priority"; my $sth = $dbh->prepare($request_query); $sth->execute($biblionumber); @@ -288,6 +310,7 @@ to fill a hold request if and only if: * it is not currently in transit * it is not lost * it is not sitting on the hold shelf + * it is not damaged (unless AllowHoldsOnDamagedItems is on) =cut @@ -307,7 +330,7 @@ sub GetItemsAvailableToFillHoldRequestsForBib { $items_query .= "WHERE items.notforloan = 0 AND holdingbranch IS NOT NULL AND itemlost = 0 - AND wthdrawn = 0"; + AND withdrawn = 0"; $items_query .= " AND damaged = 0" unless C4::Context->preference('AllowHoldsOnDamagedItems'); $items_query .= " AND items.onloan IS NULL AND (itemtypes.notforloan IS NULL OR itemtypes.notforloan = 0) @@ -319,8 +342,6 @@ sub GetItemsAvailableToFillHoldRequestsForBib { AND (found IS NOT NULL OR priority = 0) ) AND items.biblionumber = ?"; - $items_query .= " AND damaged = 0 " - unless C4::Context->preference('AllowHoldsOnDamagedItems'); my @params = ($biblionumber, $biblionumber); if ($branches_to_use && @$branches_to_use) { @@ -334,7 +355,8 @@ sub GetItemsAvailableToFillHoldRequestsForBib { my @items = grep { ! scalar GetTransfers($_->{itemnumber}) } @$itm; return [ grep { my $rule = GetBranchItemRule($_->{homebranch}, $_->{itype}); - $_->{holdallowed} = $rule->{holdallowed} != 0 + $_->{holdallowed} = $rule->{holdallowed}; + $_->{hold_fulfillment_policy} = $rule->{hold_fulfillment_policy}; } @items ]; } @@ -351,13 +373,15 @@ sub MapItemsToHoldRequests { return unless scalar(@$hold_requests) > 0; return unless scalar(@$available_items) > 0; - my $automatic_return = C4::Context->preference("AutomaticItemReturn"); - # identify item-level requests my %specific_items_requested = map { $_->{itemnumber} => 1 } grep { defined($_->{itemnumber}) } @$hold_requests; + map { $_->{_object} = Koha::Items->find( $_->{itemnumber} ) } @$available_items; + my $libraries = {}; + map { $libraries->{$_->id} = $_ } Koha::Libraries->search(); + # group available items by itemnumber my %items_by_itemnumber = map { $_->{itemnumber} => $_ } @$available_items; @@ -369,24 +393,96 @@ sub MapItemsToHoldRequests { # figure out which item-level requests can be filled my $num_items_remaining = scalar(@$available_items); + + # Look for Local Holds Priority matches first + if ( C4::Context->preference('LocalHoldsPriority') ) { + my $LocalHoldsPriorityPatronControl = + C4::Context->preference('LocalHoldsPriorityPatronControl'); + my $LocalHoldsPriorityItemControl = + C4::Context->preference('LocalHoldsPriorityItemControl'); + + foreach my $request (@$hold_requests) { + next if (defined($request->{itemnumber})); #skip item level holds in local priority checking + last if $num_items_remaining == 0; + + my $local_hold_match; + foreach my $item (@$available_items) { + next + if ( !$item->{holdallowed} ) + || ( $item->{holdallowed} == 1 + && $item->{homebranch} ne $request->{borrowerbranch} ); + + next unless $item->{_object}->can_be_transferred( { to => $libraries->{ $request->{branchcode} } } ); + + my $local_holds_priority_item_branchcode = + $item->{$LocalHoldsPriorityItemControl}; + + my $local_holds_priority_patron_branchcode = + ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' ) + ? $request->{branchcode} + : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' ) + ? $request->{borrowerbranch} + : undef; + + $local_hold_match = + $local_holds_priority_item_branchcode eq + $local_holds_priority_patron_branchcode; + + if ($local_hold_match) { + if ( exists $items_by_itemnumber{ $item->{itemnumber} } + and not exists $allocated_items{ $item->{itemnumber} } + and not $request->{allocated}) + { + $item_map{ $item->{itemnumber} } = { + borrowernumber => $request->{borrowernumber}, + biblionumber => $request->{biblionumber}, + holdingbranch => $item->{holdingbranch}, + pickup_branch => $request->{branchcode} + || $request->{borrowerbranch}, + item_level => 0, + reservedate => $request->{reservedate}, + reservenotes => $request->{reservenotes}, + }; + $allocated_items{ $item->{itemnumber} }++; + $request->{allocated} = 1; + $num_items_remaining--; + } + } + } + } + } + foreach my $request (@$hold_requests) { last if $num_items_remaining == 0; + next if $request->{allocated}; # is this an item-level request? if (defined($request->{itemnumber})) { # fill it if possible; if not skip it - if (exists $items_by_itemnumber{$request->{itemnumber}} and - not exists $allocated_items{$request->{itemnumber}}) { - $item_map{$request->{itemnumber}} = { + if ( + exists $items_by_itemnumber{ $request->{itemnumber} } + and not exists $allocated_items{ $request->{itemnumber} } + and ( # Don't fill item level holds that contravene the hold pickup policy at this time + ( $items_by_itemnumber{ $request->{itemnumber} }->{hold_fulfillment_policy} eq 'any' ) + || ( $request->{branchcode} eq $items_by_itemnumber{ $request->{itemnumber} }->{ $items_by_itemnumber{ $request->{itemnumber} }->{hold_fulfillment_policy} } ) + and ( !$request->{itemtype} # If hold itemtype is set, item's itemtype must match + || $items_by_itemnumber{ $request->{itemnumber} }->{itype} eq $request->{itemtype} ) + ) + and $items_by_itemnumber{ $request->{itemnumber} }->{_object}->can_be_transferred( { to => $libraries->{ $request->{branchcode} } } ) + + ) + { + + $item_map{ $request->{itemnumber} } = { borrowernumber => $request->{borrowernumber}, - biblionumber => $request->{biblionumber}, - holdingbranch => $items_by_itemnumber{$request->{itemnumber}}->{holdingbranch}, - pickup_branch => $request->{branchcode} || $request->{borrowerbranch}, - item_level => 1, - reservedate => $request->{reservedate}, - reservenotes => $request->{reservenotes}, + biblionumber => $request->{biblionumber}, + holdingbranch => $items_by_itemnumber{ $request->{itemnumber} }->{holdingbranch}, + pickup_branch => $request->{branchcode} || $request->{borrowerbranch}, + item_level => 1, + reservedate => $request->{reservedate}, + reservenotes => $request->{reservenotes}, }; - $allocated_items{$request->{itemnumber}}++; + $allocated_items{ $request->{itemnumber} }++; $num_items_remaining--; } } else { @@ -400,33 +496,41 @@ sub MapItemsToHoldRequests { foreach my $item (@$available_items) { next unless $item->{holdallowed}; - push @{ $items_by_branch{ $automatic_return ? $item->{homebranch} - : $item->{holdingbranch} } }, $item + push @{ $items_by_branch{ $item->{holdingbranch} } }, $item unless exists $allocated_items{ $item->{itemnumber} }; } - return unless keys %items_by_branch; + return \%item_map unless keys %items_by_branch; # now handle the title-level requests $num_items_remaining = scalar(@$available_items) - scalar(keys %allocated_items); my $pull_branches; foreach my $request (@$hold_requests) { last if $num_items_remaining == 0; + next if $request->{allocated}; next if defined($request->{itemnumber}); # already handled these # look for local match first my $pickup_branch = $request->{branchcode} || $request->{borrowerbranch}; my ($itemnumber, $holdingbranch); - my $holding_branch_items = $automatic_return ? undef : $items_by_branch{$pickup_branch}; + my $holding_branch_items = $items_by_branch{$pickup_branch}; if ( $holding_branch_items ) { foreach my $item (@$holding_branch_items) { - if ( $request->{borrowerbranch} eq $item->{homebranch} ) { + next unless $items_by_itemnumber{ $item->{itemnumber} }->{_object}->can_be_transferred( { to => $libraries->{ $request->{branchcode} } } ); + + if ( + $request->{borrowerbranch} eq $item->{homebranch} + && ( ( $item->{hold_fulfillment_policy} eq 'any' ) # Don't fill item level holds that contravene the hold pickup policy at this time + || $request->{branchcode} eq $item->{ $item->{hold_fulfillment_policy} } ) + && ( !$request->{itemtype} # If hold itemtype is set, item's itemtype must match + || $items_by_itemnumber{ $request->{itemnumber} }->{itype} eq $request->{itemtype} ) + ) + { $itemnumber = $item->{itemnumber}; last; } } $holdingbranch = $pickup_branch; - $itemnumber ||= $holding_branch_items->[0]->{itemnumber}; } elsif ($transport_cost_matrix) { $pull_branches = [keys %items_by_branch]; @@ -436,14 +540,22 @@ sub MapItemsToHoldRequests { my $holding_branch_items = $items_by_branch{$holdingbranch}; foreach my $item (@$holding_branch_items) { next if $request->{borrowerbranch} ne $item->{homebranch}; + next unless $items_by_itemnumber{ $item->{itemnumber} }->{_object}->can_be_transferred( { to => $libraries->{ $request->{branchcode} } } ); + + # Don't fill item level holds that contravene the hold pickup policy at this time + next unless $item->{hold_fulfillment_policy} eq 'any' + || $request->{branchcode} eq $item->{ $item->{hold_fulfillment_policy} }; + + # If hold itemtype is set, item's itemtype must match + next unless ( !$request->{itemtype} + || $item->{itype} eq $request->{itemtype} ); $itemnumber = $item->{itemnumber}; last; } - $itemnumber ||= $holding_branch_items->[0]->{itemnumber}; } else { - warn "No transport costs for $pickup_branch"; + next; } } @@ -454,6 +566,8 @@ sub MapItemsToHoldRequests { } else { $pull_branches = [keys %items_by_branch]; } + + # Try picking items where the home and pickup branch match first PULL_BRANCHES: foreach my $branch (@$pull_branches) { my $holding_branch_items = $items_by_branch{$branch} @@ -462,14 +576,70 @@ sub MapItemsToHoldRequests { $holdingbranch ||= $branch; foreach my $item (@$holding_branch_items) { next if $pickup_branch ne $item->{homebranch}; + next if ( $item->{holdallowed} == 1 && $item->{homebranch} ne $request->{borrowerbranch} ); + next unless $items_by_itemnumber{ $item->{itemnumber} }->{_object}->can_be_transferred( { to => $libraries->{ $request->{branchcode} } } ); + + # Don't fill item level holds that contravene the hold pickup policy at this time + next unless $item->{hold_fulfillment_policy} eq 'any' + || $request->{branchcode} eq $item->{ $item->{hold_fulfillment_policy} }; + + # If hold itemtype is set, item's itemtype must match + next unless ( !$request->{itemtype} + || $item->{itype} eq $request->{itemtype} ); $itemnumber = $item->{itemnumber}; $holdingbranch = $branch; last PULL_BRANCHES; } } - $itemnumber ||= $items_by_branch{$holdingbranch}->[0]->{itemnumber} - if $holdingbranch; + + # Now try items from the least cost branch based on the transport cost matrix or StaticHoldsQueueWeight + unless ( $itemnumber ) { + foreach my $current_item ( @{ $items_by_branch{$holdingbranch} } ) { + if ( $holdingbranch && ( $current_item->{holdallowed} == 2 || $request->{borrowerbranch} eq $current_item->{homebranch} ) ) { + + # Don't fill item level holds that contravene the hold pickup policy at this time + next unless $current_item->{hold_fulfillment_policy} eq 'any' + || $request->{branchcode} eq $current_item->{ $current_item->{hold_fulfillment_policy} }; + + # If hold itemtype is set, item's itemtype must match + next unless ( !$request->{itemtype} + || $current_item->{itype} eq $request->{itemtype} ); + + next unless $items_by_itemnumber{ $current_item->{itemnumber} }->{_object}->can_be_transferred( { to => $libraries->{ $request->{branchcode} } } ); + + $itemnumber = $current_item->{itemnumber}; + last; # quit this loop as soon as we have a suitable item + } + } + } + + # Now try for items for any item that can fill this hold + unless ( $itemnumber ) { + PULL_BRANCHES2: + foreach my $branch (@$pull_branches) { + my $holding_branch_items = $items_by_branch{$branch} + or next; + + foreach my $item (@$holding_branch_items) { + next if ( $item->{holdallowed} == 1 && $item->{homebranch} ne $request->{borrowerbranch} ); + + # Don't fill item level holds that contravene the hold pickup policy at this time + next unless $item->{hold_fulfillment_policy} eq 'any' + || $request->{branchcode} eq $item->{ $item->{hold_fulfillment_policy} }; + + # If hold itemtype is set, item's itemtype must match + next unless ( !$request->{itemtype} + || $item->{itype} eq $request->{itemtype} ); + + next unless $items_by_itemnumber{ $item->{itemnumber} }->{_object}->can_be_transferred( { to => $libraries->{ $request->{branchcode} } } ); + + $itemnumber = $item->{itemnumber}; + $holdingbranch = $branch; + last PULL_BRANCHES2; + } + } + } } if ($itemnumber) { @@ -519,18 +689,18 @@ sub CreatePicklistFromItemMap { my $reservenotes = $mapped_item->{reservenotes}; my $item_level = $mapped_item->{item_level}; - my $item = GetItem($itemnumber); - my $barcode = $item->{barcode}; - my $itemcallnumber = $item->{itemcallnumber}; + my $item = Koha::Items->find($itemnumber); + my $barcode = $item->barcode; + my $itemcallnumber = $item->itemcallnumber; - my $borrower = GetMember('borrowernumber'=>$borrowernumber); - my $cardnumber = $borrower->{'cardnumber'}; - my $surname = $borrower->{'surname'}; - my $firstname = $borrower->{'firstname'}; - my $phone = $borrower->{'phone'}; + my $patron = Koha::Patrons->find( $borrowernumber ); + my $cardnumber = $patron->cardnumber; + my $surname = $patron->surname; + my $firstname = $patron->firstname; + my $phone = $patron->phone; - my $bib = GetBiblioData($biblionumber); - my $title = $bib->{title}; + my $biblio = Koha::Biblios->find( $biblionumber ); + my $title = $biblio->title; $sth_load->execute($biblionumber, $itemnumber, $barcode, $surname, $firstname, $phone, $borrowernumber, $cardnumber, $reservedate, $title, $itemcallnumber, @@ -570,12 +740,27 @@ sub _trim { } sub load_branches_to_pull_from { - my $static_branch_list = C4::Context->preference("StaticHoldsQueueWeight") - or return; - - my @branches_to_use = map _trim($_), split /,/, $static_branch_list; - - @branches_to_use = shuffle(@branches_to_use) if C4::Context->preference("RandomizeHoldsQueueWeight"); + my @branches_to_use; + + my $static_branch_list = C4::Context->preference("StaticHoldsQueueWeight"); + @branches_to_use = map { _trim($_) } split( /,/, $static_branch_list ) + if $static_branch_list; + + @branches_to_use = + Koha::Database->new()->schema()->resultset('Branch') + ->get_column('branchcode')->all() + unless (@branches_to_use); + + @branches_to_use = shuffle(@branches_to_use) + if C4::Context->preference("RandomizeHoldsQueueWeight"); + + my $today = dt_from_string(); + if ( C4::Context->preference('HoldsQueueSkipClosed') ) { + @branches_to_use = grep { + !Koha::Calendar->new( branchcode => $_ ) + ->is_holiday( $today ) + } @branches_to_use; + } return \@branches_to_use; } @@ -585,9 +770,13 @@ sub least_cost_branch { #$from - arrayref my ($to, $from, $transport_cost_matrix) = @_; -# Nothing really spectacular: supply to branch, a list of potential from branches -# and find the minimum from - to value from the transport_cost_matrix - return $from->[0] if @$from == 1; + # Nothing really spectacular: supply to branch, a list of potential from branches + # and find the minimum from - to value from the transport_cost_matrix + return $from->[0] if ( @$from == 1 && $transport_cost_matrix->{$to}{$from->[0]}->{disable_transfer} != 1 ); + + # If the pickup library is in the list of libraries to pull from, + # return that library right away, it is obviously the least costly + return ($to) if any { $_ eq $to } @$from; my ($least_cost, @branch); foreach (@$from) {