bug 2522 [3/3]: populate hold_fill_targets

This batch job now automatically populates the
holds request targeting table hold_fill_targets.

This patch is essentially a revamp of the job,
and includes fixes for the following bugs:

2281 (holds queue report including unavailable items)
2331 (holds queue report not working with item-level
      holds)
2332 (holds queue script should attempt to fill
      many requests as possible).

Several functions in this batch job are candidates
for being moved to C4::Reserves:

GetBibsWithPendingHoldRequests()
GetPendingHoldRequestsForBib()
GetItemsAvailableToFillHoldRequestsForBib()
MapItemsToHoldRequests()

Signed-off-by: Galen Charlton <galen.charlton@liblime.com>
This commit is contained in:
Galen Charlton 2008-08-14 20:59:51 -05:00 committed by Chris Cormack
parent 799a663ada
commit 5eafcf52de
1 changed files with 320 additions and 128 deletions

View File

@ -5,6 +5,7 @@
#-----------------------------------
use strict;
use warnings;
BEGIN {
# find Koha's Perl modules
# test carefully before changing this
@ -16,149 +17,340 @@ use C4::Context;
use C4::Search;
use C4::Items;
use C4::Branch;
use C4::Circulation;
use C4::Members;
use C4::Biblio;
use Data::Dumper;
# load the branches
my $branches = GetBranches();
# obtain the ranked list of weights for the case of static weighting
my $syspref = C4::Context->preference("StaticHoldsQueueWeight");
my @branch_loop;
#@branch_loop = split(/,/, $syspref) if $syspref;
# TODO: Add Randomization Option
# If no syspref is set, use system-order to determine priority
unless ($syspref) {
for my $branch_hash (sort keys %$branches) {
push @branch_loop, $branch_hash;
#{value => "$branch_hash" , branchname => $branches->{$branch_hash}->{'branchname'}, };
}
}
# if Randomization is enabled, randomize this array
@branch_loop = randarray(@branch_loop) if C4::Context->preference("RandomizeHoldsQueueWeight");;
my ($biblionumber,$itemnumber,$barcode,$holdingbranch,$pickbranch,$notes,$cardnumber,$surname,$firstname,$phone,$title,$callno,$rdate,$borrno);
my $bibs_with_pending_requests = GetBibsWithPendingHoldRequests();
my $dbh = C4::Context->dbh;
$dbh->do("DELETE FROM tmp_holdsqueue"); # clear the old table for new info
$dbh->do("DELETE FROM hold_fill_targets");
my $sth=$dbh->prepare("
SELECT biblionumber,itemnumber,reserves.branchcode,reservenotes,borrowers.borrowernumber,cardnumber,surname,firstname,phone,reservedate
FROM reserves,borrowers
WHERE reserves.found IS NULL
AND reserves.borrowernumber=borrowers.borrowernumber
AND priority=1
GROUP BY biblionumber");
my $sth_load=$dbh->prepare("
INSERT INTO tmp_holdsqueue (biblionumber,itemnumber,barcode,surname,firstname,phone,borrowernumber,cardnumber,reservedate,title,itemcallnumber,holdingbranch,pickbranch,notes)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
$sth->execute(); # get the list of biblionumbers for unfilled holds
GETIT:
while (my $data=$sth->fetchrow_hashref){
# get the basic hold info
$biblionumber = $data->{'biblionumber'};
$pickbranch = $data->{'branchcode'};
$notes = $data->{'reservenotes'};
$borrno = $data->{'borrowernumber'};
$cardnumber = $data->{'cardnumber'};
$surname = $data->{'surname'};
$firstname = $data->{'firstname'};
$phone = $data->{'phone'};
$rdate = $data->{'reservedate'};
my @items = GetItemsInfo($biblionumber,''); # get the items for this biblio
my @itemorder; # prepare a new array to hold re-ordered items
# Make sure someone(else) doesn't already have this item waiting for them
my $found_sth = $dbh->prepare("
SELECT found FROM reserves WHERE itemnumber=? AND found = ? AND cancellationdate IS NULL");
# The following lines take the retrieved items and run them through various
# tests to decide if they are to be used and then put them in the preferred
# 'pick' order.
foreach my $itm (@items) {
$found_sth->execute($itm->{itemnumber},"W");
my $found = $found_sth->fetchrow_hashref();
if ($found) {
$itm->{"found"} = $found->{"found"};
my $total_bibs = 0;
my $total_requests = 0;
my $total_available_items = 0;
my $num_items_mapped = 0;
foreach my $biblionumber (@$bibs_with_pending_requests) {
$total_bibs++;
my $hold_requests = GetPendingHoldRequestsForBib($biblionumber);
$total_requests += scalar(@$hold_requests);
my $available_items = GetItemsAvailableToFillHoldRequestsForBib($biblionumber);
$total_available_items += scalar(@$available_items);
my $item_map = MapItemsToHoldRequests($hold_requests, $available_items);
if (defined($item_map)) {
$num_items_mapped += scalar(keys %$item_map);
CreatePicklistFromItemMap($item_map);
AddToHoldTargetMap($item_map);
if ((scalar(keys %$item_map) < scalar(@$hold_requests)) and
(scalar(keys %$item_map) < scalar(@$available_items))) {
# DOUBLE CHECK, but this is probably OK - unfilled item-level requests
# FIXME
#warn "unfilled requests for $biblionumber";
#warn Dumper($hold_requests);
#warn Dumper($available_items);
#warn Dumper($item_map);
}
if ($itm->{"notforloan"}) {
# item is on order
next if $itm->{"notforloan"}== -1;
}
}
exit 0;
=head2 GetBibsWithPendingHoldRequests
=over 4
my $biblionumber_aref = GetBibsWithPendingHoldRequests();
=back
Return an arrayref of the biblionumbers of all bibs
that have one or more unfilled hold requests.
=cut
sub GetBibsWithPendingHoldRequests {
my $dbh = C4::Context->dbh;
my $bib_query = "SELECT DISTINCT biblionumber
FROM reserves
WHERE found IS NULL
AND priority > 0";
my $sth = $dbh->prepare($bib_query);
$sth->execute();
my $biblionumbers = $sth->fetchall_arrayref();
return [ map { $_->[0] } @$biblionumbers ];
}
=head2 GetPendingHoldRequestsForBib
=over 4
my $requests = GetPendingHoldRequestsForBib($biblionumber);
=back
Returns an arrayref of hashrefs to pending, unfilled hold requests
on the bib identified by $biblionumber. The following keys
are present in each hashref:
biblionumber
borrowernumber
itemnumber
priority
branchcode
reservedate
reservenotes
The arrayref is sorted in order of increasing priority.
=cut
sub GetPendingHoldRequestsForBib {
my $biblionumber = shift;
my $dbh = C4::Context->dbh;
my $request_query = "SELECT biblionumber, borrowernumber, itemnumber, priority, branchcode, reservedate, reservenotes
FROM reserves
WHERE biblionumber = ?
AND found IS NULL
AND priority > 0
ORDER BY priority";
my $sth = $dbh->prepare($request_query);
$sth->execute($biblionumber);
my $requests = $sth->fetchall_arrayref({});
return $requests;
}
=head2 GetItemsAvailableToFillHoldRequestsForBib
=over 4
my $available_items = GetItemsAvailableToFillHoldRequestsForBib($biblionumber);
=back
Returns an arrayref of items available to fill hold requests
for the bib identified by C<$biblionumber>. An item is available
to fill a hold request if and only if:
* it is not on loan
* it is not withdrawn
* it is not marked notforloan
* it is not currently in transit
* it is not lost
* it is not sitting on the hold shelf
=cut
sub GetItemsAvailableToFillHoldRequestsForBib {
my $biblionumber = shift;
my $dbh = C4::Context->dbh;
my $items_query = "SELECT itemnumber, homebranch, holdingbranch
FROM items ";
if (C4::Context->preference('item-level_itypes')) {
$items_query .= "LEFT JOIN itemtypes ON (itemtypes.itemtype = items.itype) ";
} else {
$items_query .= "JOIN biblioitems USING (biblioitemnumber)
LEFT JOIN itemtypes USING (itemtype) ";
}
$items_query .= "LEFT JOIN reserves USING (itemnumber)
WHERE items.notforloan = 0
AND holdingbranch IS NOT NULL
AND itemlost = 0
AND wthdrawn = 0
AND items.onloan IS NULL
AND (itemtypes.notforloan IS NULL OR itemtypes.notforloan = 0)
AND (priority IS NULL OR priority > 0)
AND found IS NULL
AND biblionumber = ?";
my $sth = $dbh->prepare($items_query);
$sth->execute($biblionumber);
my $items = $sth->fetchall_arrayref({});
return [ grep { my @transfers = GetTransfers($_->{itemnumber}); $#transfers == -1; } @$items ];
}
=head2 MapItemsToHoldRequests
=over 4
MapItemsToHoldRequests($hold_requests, $available_items);
=back
=cut
sub MapItemsToHoldRequests {
my $hold_requests = shift;
my $available_items = shift;
# handle trival cases
return unless scalar(@$hold_requests) > 0;
return unless scalar(@$available_items) > 0;
# identify item-level requests
my %specific_items_requested = map { $_->{itemnumber} => 1 }
grep { defined($_->{itemnumber}) }
@$hold_requests;
# group available items by itemnumber
my %items_by_itemnumber = map { $_->{itemnumber} => $_ } @$available_items;
# items already allocated
my %allocated_items = ();
# map of items to hold requests
my %item_map = ();
# figure out which item-level requests can be filled
my $num_items_remaining = scalar(@$available_items);
foreach my $request (@$hold_requests) {
last if $num_items_remaining == 0;
# 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}} = {
borrowernumber => $request->{borrowernumber},
biblionumber => $request->{biblionumber},
holdingbranch => $items_by_itemnumber{$request->{itemnumber}}->{holdingbranch},
pickup_branch => $request->{branchcode},
item_level => 1,
reservedate => $request->{reservedate},
reservenotes => $request->{reservenotes},
};
$allocated_items{$request->{itemnumber}}++;
$num_items_remaining--;
}
} else {
# it's title-level request that will take up one item
$num_items_remaining--;
}
if ( ( (!$itm->{"binding"}) ||
# Item is at not at bindery, not checked out, and not lost
($itm->{"binding"}<1)) && (!$itm->{"found"}) && (!$itm->{"datedue"}) && ( (!$itm->{"itemlost"}) ||
}
# Item is not lost and not notforloan
($itm->{"itemlost"}==0) ) && ( ($itm->{"notforloan"}==0) ||
# group available items by branch
my %items_by_branch = ();
foreach my $item (@$available_items) {
push @{ $items_by_branch{ $item->{holdingbranch} } }, $item unless exists $allocated_items{ $item->{itemnumber} };
}
# Item is not notforloan
(!$itm->{"notforloan"}) ) ) {
# now handle the title-level requests
$num_items_remaining = scalar(@$available_items) - scalar(keys %allocated_items);
foreach my $request (@$hold_requests) {
last if $num_items_remaining <= 0;
next if defined($request->{itemnumber}); # already handled these
warn "patron requested pickup at $pickbranch for item in ".$itm->{'holdingbranch'};
# This selects items for fulfilment, and weights them based on
# a static list
my $weight=0;
# always prefer a direct match
if ($itm->{'holdingbranch'} eq $pickbranch) {
warn "Found match in pickuplibrary";
$itemorder[$weight]=$itm;
}
else {
for my $branchcode (@branch_loop) {
$weight++;
if ($itm->{'homebranch'} eq $branchcode) {
warn "Match found with weight $weight in ".$branchcode;
$itemorder[$weight]=$itm;
}
}
# look for local match first
my $pickup_branch = $request->{branchcode};
if (exists $items_by_branch{$pickup_branch}) {
my $item = pop @{ $items_by_branch{$pickup_branch} };
delete $items_by_branch{$pickup_branch} if scalar(@{ $items_by_branch{$pickup_branch} }) == 0;
$item_map{$item->{itemnumber}} = {
borrowernumber => $request->{borrowernumber},
biblionumber => $request->{biblionumber},
holdingbranch => $pickup_branch,
pickup_branch => $pickup_branch,
item_level => 0,
reservedate => $request->{reservedate},
reservenotes => $request->{reservenotes},
};
$num_items_remaining--;
} else {
# FIXME implement static, random options
foreach my $branch (sort keys %items_by_branch) {
my $item = pop @{ $items_by_branch{$branch} };
delete $items_by_branch{$branch} if scalar(@{ $items_by_branch{$branch} }) == 0;
$item_map{$item->{itemnumber}} = {
borrowernumber => $request->{borrowernumber},
biblionumber => $request->{biblionumber},
holdingbranch => $branch,
pickup_branch => $pickup_branch,
item_level => 0,
reservedate => $request->{reservedate},
reservenotes => $request->{reservenotes},
};
$num_items_remaining--;
last;
}
}
}
my $count = @itemorder;
warn "Empty array" if $count<1;
next GETIT if $count<1; # if the re-ordered array is empty, skip to next
return \%item_map;
}
PREP:
foreach my $itmlist (@itemorder) {
if ($itmlist) {
$barcode = $itmlist->{'barcode'};
$itemnumber = $itmlist->{'itemnumber'};
$holdingbranch = $itmlist->{'holdingbranch'};
$title = $itmlist->{'title'};
$callno = $itmlist->{'itemcallnumber'};
last PREP; # we only want the first def item in the array
}
=head2 CreatePickListFromItemMap
=cut
sub CreatePicklistFromItemMap {
my $item_map = shift;
my $dbh = C4::Context->dbh;
my $sth_load=$dbh->prepare("
INSERT INTO tmp_holdsqueue (biblionumber,itemnumber,barcode,surname,firstname,phone,borrowernumber,
cardnumber,reservedate,title, itemcallnumber,
holdingbranch,pickbranch,notes, item_level_request)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
");
foreach my $itemnumber (sort keys %$item_map) {
my $mapped_item = $item_map->{$itemnumber};
my $biblionumber = $mapped_item->{biblionumber};
my $borrowernumber = $mapped_item->{borrowernumber};
my $pickbranch = $mapped_item->{pickup_branch};
my $holdingbranch = $mapped_item->{holdingbranch};
my $reservedate = $mapped_item->{reservedate};
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 $borrower = GetMember($borrowernumber);
my $cardnumber = $borrower->{'cardnumber'};
my $surname = $borrower->{'surname'};
my $firstname = $borrower->{'firstname'};
my $phone = $borrower->{'phone'};
my $bib = GetBiblioData($biblionumber);
my $title = $bib->{title};
$sth_load->execute($biblionumber, $itemnumber, $barcode, $surname, $firstname, $phone, $borrowernumber,
$cardnumber, $reservedate, $title, $itemcallnumber,
$holdingbranch, $pickbranch, $reservenotes, $item_level);
}
$sth_load->execute($biblionumber,$itemnumber,$barcode,$surname,$firstname,$phone,$borrno,$cardnumber,$rdate,$title,$callno,$holdingbranch,$pickbranch,$notes);
$sth_load->finish;
}
$sth->finish;
$dbh->disconnect;
sub randarray {
my @array = @_;
my @rand = undef;
my $seed = $#array + 1;
my $randnum = int(rand($seed));
$rand[$randnum] = shift(@array);
while (1) {
my $randnum = int(rand($seed));
if (!defined($rand[$randnum])) {
$rand[$randnum] = shift(@array);
}
last if ($#array == -1);
}
return @rand;
}
=head2 AddToHoldTargetMap
print "finished\n";
=cut
sub AddToHoldTargetMap {
my $item_map = shift;
my $dbh = C4::Context->dbh;
my $insert_sql = q(
INSERT INTO hold_fill_targets (borrowernumber, biblionumber, itemnumber, source_branchcode, item_level_request)
VALUES (?, ?, ?, ?, ?)
);
my $sth_insert = $dbh->prepare($insert_sql);
foreach my $itemnumber (keys %$item_map) {
my $mapped_item = $item_map->{$itemnumber};
$sth_insert->execute($mapped_item->{borrowernumber}, $mapped_item->{biblionumber}, $itemnumber,
$mapped_item->{holdingbranch}, $mapped_item->{item_level});
}
}