2 #-----------------------------------
3 # Script Name: build_holds_queue.pl
4 # Description: builds a holds queue in the tmp_holdsqueue table
5 #-----------------------------------
6 # FIXME: add command-line options for verbosity and summary
7 # FIXME: expand perldoc, explain intended logic
8 # FIXME: refactor all subroutines into C4 for testability
13 # find Koha's Perl modules
14 # test carefully before changing this
16 eval { require "$FindBin::Bin/../kohalib.pl" };
27 use List::Util qw(shuffle);
29 my $bibs_with_pending_requests = GetBibsWithPendingHoldRequests();
31 my $dbh = C4::Context->dbh;
32 $dbh->do("DELETE FROM tmp_holdsqueue"); # clear the old table for new info
33 $dbh->do("DELETE FROM hold_fill_targets");
36 my $total_requests = 0;
37 my $total_available_items = 0;
38 my $num_items_mapped = 0;
40 my @branches_to_use = _get_branches_to_pull_from();
42 foreach my $biblionumber (@$bibs_with_pending_requests) {
44 my $hold_requests = GetPendingHoldRequestsForBib($biblionumber);
45 my $available_items = GetItemsAvailableToFillHoldRequestsForBib($biblionumber, @branches_to_use);
46 $total_requests += scalar(@$hold_requests);
47 $total_available_items += scalar(@$available_items);
48 my $item_map = MapItemsToHoldRequests($hold_requests, $available_items, @branches_to_use);
50 (defined($item_map)) or next;
52 my $item_map_size = scalar(keys %$item_map);
53 $num_items_mapped += $item_map_size;
54 CreatePicklistFromItemMap($item_map);
55 AddToHoldTargetMap($item_map);
56 if (($item_map_size < scalar(@$hold_requests )) and
57 ($item_map_size < scalar(@$available_items))) {
58 # DOUBLE CHECK, but this is probably OK - unfilled item-level requests
60 #warn "unfilled requests for $biblionumber";
61 #warn Dumper($hold_requests), Dumper($available_items), Dumper($item_map);
67 =head2 GetBibsWithPendingHoldRequests
71 my $biblionumber_aref = GetBibsWithPendingHoldRequests();
75 Return an arrayref of the biblionumbers of all bibs
76 that have one or more unfilled hold requests.
80 sub GetBibsWithPendingHoldRequests {
81 my $dbh = C4::Context->dbh;
83 my $bib_query = "SELECT DISTINCT biblionumber
87 AND reservedate <= CURRENT_DATE()";
88 my $sth = $dbh->prepare($bib_query);
91 my $biblionumbers = $sth->fetchall_arrayref();
93 return [ map { $_->[0] } @$biblionumbers ];
96 =head2 GetPendingHoldRequestsForBib
100 my $requests = GetPendingHoldRequestsForBib($biblionumber);
104 Returns an arrayref of hashrefs to pending, unfilled hold requests
105 on the bib identified by $biblionumber. The following keys
106 are present in each hashref:
117 The arrayref is sorted in order of increasing priority.
121 sub GetPendingHoldRequestsForBib {
122 my $biblionumber = shift;
124 my $dbh = C4::Context->dbh;
126 my $request_query = "SELECT biblionumber, borrowernumber, itemnumber, priority, reserves.branchcode,
127 reservedate, reservenotes, borrowers.branchcode AS borrowerbranch
129 JOIN borrowers USING (borrowernumber)
130 WHERE biblionumber = ?
133 AND reservedate <= CURRENT_DATE()
135 my $sth = $dbh->prepare($request_query);
136 $sth->execute($biblionumber);
138 my $requests = $sth->fetchall_arrayref({});
143 =head2 GetItemsAvailableToFillHoldRequestsForBib
147 my $available_items = GetItemsAvailableToFillHoldRequestsForBib($biblionumber);
151 Returns an arrayref of items available to fill hold requests
152 for the bib identified by C<$biblionumber>. An item is available
153 to fill a hold request if and only if:
156 * it is not withdrawn
157 * it is not marked notforloan
158 * it is not currently in transit
160 * it is not sitting on the hold shelf
164 sub GetItemsAvailableToFillHoldRequestsForBib {
165 my $biblionumber = shift;
166 my @branches_to_use = @_;
168 my $dbh = C4::Context->dbh;
169 my $items_query = "SELECT itemnumber, homebranch, holdingbranch, itemtypes.itemtype AS itype
172 if (C4::Context->preference('item-level_itypes')) {
173 $items_query .= "LEFT JOIN itemtypes ON (itemtypes.itemtype = items.itype) ";
175 $items_query .= "JOIN biblioitems USING (biblioitemnumber)
176 LEFT JOIN itemtypes USING (itemtype) ";
178 $items_query .= "WHERE items.notforloan = 0
179 AND holdingbranch IS NOT NULL
182 AND items.onloan IS NULL
183 AND (itemtypes.notforloan IS NULL OR itemtypes.notforloan = 0)
184 AND itemnumber NOT IN (
187 WHERE biblionumber = ?
188 AND itemnumber IS NOT NULL
189 AND (found IS NOT NULL OR priority = 0)
191 AND biblionumber = ?";
192 my @params = ($biblionumber, $biblionumber);
193 if ($#branches_to_use > -1) {
194 $items_query .= " AND holdingbranch IN (" . join (",", map { "?" } @branches_to_use) . ")";
195 push @params, @branches_to_use;
197 my $sth = $dbh->prepare($items_query);
198 $sth->execute(@params);
200 my $items = $sth->fetchall_arrayref({});
201 $items = [ grep { my @transfers = GetTransfers($_->{itemnumber}); $#transfers == -1; } @$items ];
202 map { my $rule = GetBranchItemRule($_->{homebranch}, $_->{itype}); $_->{holdallowed} = $rule->{holdallowed}; $rule->{holdallowed} != 0 } @$items;
203 return [ grep { $_->{holdallowed} != 0 } @$items ];
206 =head2 MapItemsToHoldRequests
210 MapItemsToHoldRequests($hold_requests, $available_items);
216 sub MapItemsToHoldRequests {
217 my $hold_requests = shift;
218 my $available_items = shift;
219 my @branches_to_use = @_;
221 # handle trival cases
222 return unless scalar(@$hold_requests) > 0;
223 return unless scalar(@$available_items) > 0;
225 # identify item-level requests
226 my %specific_items_requested = map { $_->{itemnumber} => 1 }
227 grep { defined($_->{itemnumber}) }
230 # group available items by itemnumber
231 my %items_by_itemnumber = map { $_->{itemnumber} => $_ } @$available_items;
233 # items already allocated
234 my %allocated_items = ();
236 # map of items to hold requests
239 # figure out which item-level requests can be filled
240 my $num_items_remaining = scalar(@$available_items);
241 foreach my $request (@$hold_requests) {
242 last if $num_items_remaining == 0;
244 # is this an item-level request?
245 if (defined($request->{itemnumber})) {
246 # fill it if possible; if not skip it
247 if (exists $items_by_itemnumber{$request->{itemnumber}} and
248 not exists $allocated_items{$request->{itemnumber}}) {
249 $item_map{$request->{itemnumber}} = {
250 borrowernumber => $request->{borrowernumber},
251 biblionumber => $request->{biblionumber},
252 holdingbranch => $items_by_itemnumber{$request->{itemnumber}}->{holdingbranch},
253 pickup_branch => $request->{branchcode},
255 reservedate => $request->{reservedate},
256 reservenotes => $request->{reservenotes},
258 $allocated_items{$request->{itemnumber}}++;
259 $num_items_remaining--;
262 # it's title-level request that will take up one item
263 $num_items_remaining--;
267 # group available items by branch
268 my %items_by_branch = ();
269 foreach my $item (@$available_items) {
270 push @{ $items_by_branch{ $item->{holdingbranch} } }, $item unless exists $allocated_items{ $item->{itemnumber} };
273 # now handle the title-level requests
274 $num_items_remaining = scalar(@$available_items) - scalar(keys %allocated_items);
275 foreach my $request (@$hold_requests) {
276 last if $num_items_remaining <= 0;
277 next if defined($request->{itemnumber}); # already handled these
279 # look for local match first
280 my $pickup_branch = $request->{branchcode};
281 if (exists $items_by_branch{$pickup_branch} and
282 not ($items_by_branch{$pickup_branch}->[0]->{holdallowed} == 1 and
283 $request->{borrowerbranch} ne $items_by_branch{$pickup_branch}->[0]->{homebranch})
285 my $item = pop @{ $items_by_branch{$pickup_branch} };
286 delete $items_by_branch{$pickup_branch} if scalar(@{ $items_by_branch{$pickup_branch} }) == 0;
287 $item_map{$item->{itemnumber}} = {
288 borrowernumber => $request->{borrowernumber},
289 biblionumber => $request->{biblionumber},
290 holdingbranch => $pickup_branch,
291 pickup_branch => $pickup_branch,
293 reservedate => $request->{reservedate},
294 reservenotes => $request->{reservenotes},
296 $num_items_remaining--;
298 my @pull_branches = ();
299 if ($#branches_to_use > -1) {
300 @pull_branches = @branches_to_use;
302 @pull_branches = sort keys %items_by_branch;
304 foreach my $branch (@pull_branches) {
305 next unless exists $items_by_branch{$branch} and
306 not ($items_by_branch{$branch}->[0]->{holdallowed} == 1 and
307 $request->{borrowerbranch} ne $items_by_branch{$branch}->[0]->{homebranch});
308 my $item = pop @{ $items_by_branch{$branch} };
309 delete $items_by_branch{$branch} if scalar(@{ $items_by_branch{$branch} }) == 0;
310 $item_map{$item->{itemnumber}} = {
311 borrowernumber => $request->{borrowernumber},
312 biblionumber => $request->{biblionumber},
313 holdingbranch => $branch,
314 pickup_branch => $pickup_branch,
316 reservedate => $request->{reservedate},
317 reservenotes => $request->{reservenotes},
319 $num_items_remaining--;
327 =head2 CreatePickListFromItemMap
331 sub CreatePicklistFromItemMap {
332 my $item_map = shift;
334 my $dbh = C4::Context->dbh;
336 my $sth_load=$dbh->prepare("
337 INSERT INTO tmp_holdsqueue (biblionumber,itemnumber,barcode,surname,firstname,phone,borrowernumber,
338 cardnumber,reservedate,title, itemcallnumber,
339 holdingbranch,pickbranch,notes, item_level_request)
340 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
343 foreach my $itemnumber (sort keys %$item_map) {
344 my $mapped_item = $item_map->{$itemnumber};
345 my $biblionumber = $mapped_item->{biblionumber};
346 my $borrowernumber = $mapped_item->{borrowernumber};
347 my $pickbranch = $mapped_item->{pickup_branch};
348 my $holdingbranch = $mapped_item->{holdingbranch};
349 my $reservedate = $mapped_item->{reservedate};
350 my $reservenotes = $mapped_item->{reservenotes};
351 my $item_level = $mapped_item->{item_level};
353 my $item = GetItem($itemnumber);
354 my $barcode = $item->{barcode};
355 my $itemcallnumber = $item->{itemcallnumber};
357 my $borrower = GetMember($borrowernumber);
358 my $cardnumber = $borrower->{'cardnumber'};
359 my $surname = $borrower->{'surname'};
360 my $firstname = $borrower->{'firstname'};
361 my $phone = $borrower->{'phone'};
363 my $bib = GetBiblioData($biblionumber);
364 my $title = $bib->{title};
366 $sth_load->execute($biblionumber, $itemnumber, $barcode, $surname, $firstname, $phone, $borrowernumber,
367 $cardnumber, $reservedate, $title, $itemcallnumber,
368 $holdingbranch, $pickbranch, $reservenotes, $item_level);
372 =head2 AddToHoldTargetMap
376 sub AddToHoldTargetMap {
377 my $item_map = shift;
379 my $dbh = C4::Context->dbh;
382 INSERT INTO hold_fill_targets (borrowernumber, biblionumber, itemnumber, source_branchcode, item_level_request)
383 VALUES (?, ?, ?, ?, ?)
385 my $sth_insert = $dbh->prepare($insert_sql);
387 foreach my $itemnumber (keys %$item_map) {
388 my $mapped_item = $item_map->{$itemnumber};
389 $sth_insert->execute($mapped_item->{borrowernumber}, $mapped_item->{biblionumber}, $itemnumber,
390 $mapped_item->{holdingbranch}, $mapped_item->{item_level});
394 =head2 _get_branches_to_pull_from
396 Query system preferences to get ordered list of
397 branches to use to fill hold requests.
401 sub _get_branches_to_pull_from {
402 my @branches_to_use = ();
404 my $static_branch_list = C4::Context->preference("StaticHoldsQueueWeight");
405 if ($static_branch_list) {
406 @branches_to_use = map { s/^\s+//; s/\s+$//; $_; } split /,/, $static_branch_list;
409 @branches_to_use = shuffle(@branches_to_use) if C4::Context->preference("RandomizeHoldsQueueWeight");
411 return @branches_to_use;