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);
69 =head2 GetBibsWithPendingHoldRequests
71 my $biblionumber_aref = GetBibsWithPendingHoldRequests();
73 Return an arrayref of the biblionumbers of all bibs
74 that have one or more unfilled hold requests.
78 sub GetBibsWithPendingHoldRequests {
79 my $dbh = C4::Context->dbh;
81 my $bib_query = "SELECT DISTINCT biblionumber
85 AND reservedate <= CURRENT_DATE()";
86 my $sth = $dbh->prepare($bib_query);
89 my $biblionumbers = $sth->fetchall_arrayref();
91 return [ map { $_->[0] } @$biblionumbers ];
94 =head2 GetPendingHoldRequestsForBib
96 my $requests = GetPendingHoldRequestsForBib($biblionumber);
98 Returns an arrayref of hashrefs to pending, unfilled hold requests
99 on the bib identified by $biblionumber. The following keys
100 are present in each hashref:
111 The arrayref is sorted in order of increasing priority.
115 sub GetPendingHoldRequestsForBib {
116 my $biblionumber = shift;
118 my $dbh = C4::Context->dbh;
120 my $request_query = "SELECT biblionumber, borrowernumber, itemnumber, priority, reserves.branchcode,
121 reservedate, reservenotes, borrowers.branchcode AS borrowerbranch
123 JOIN borrowers USING (borrowernumber)
124 WHERE biblionumber = ?
127 AND reservedate <= CURRENT_DATE()
129 my $sth = $dbh->prepare($request_query);
130 $sth->execute($biblionumber);
132 my $requests = $sth->fetchall_arrayref({});
137 =head2 GetItemsAvailableToFillHoldRequestsForBib
139 my $available_items = GetItemsAvailableToFillHoldRequestsForBib($biblionumber);
141 Returns an arrayref of items available to fill hold requests
142 for the bib identified by C<$biblionumber>. An item is available
143 to fill a hold request if and only if:
146 * it is not withdrawn
147 * it is not marked notforloan
148 * it is not currently in transit
150 * it is not sitting on the hold shelf
154 sub GetItemsAvailableToFillHoldRequestsForBib {
155 my $biblionumber = shift;
156 my @branches_to_use = @_;
158 my $dbh = C4::Context->dbh;
159 my $items_query = "SELECT itemnumber, homebranch, holdingbranch, itemtypes.itemtype AS itype
162 if (C4::Context->preference('item-level_itypes')) {
163 $items_query .= "LEFT JOIN itemtypes ON (itemtypes.itemtype = items.itype) ";
165 $items_query .= "JOIN biblioitems USING (biblioitemnumber)
166 LEFT JOIN itemtypes USING (itemtype) ";
168 $items_query .= "WHERE items.notforloan = 0
169 AND holdingbranch IS NOT NULL
172 AND items.onloan IS NULL
173 AND (itemtypes.notforloan IS NULL OR itemtypes.notforloan = 0)
174 AND itemnumber NOT IN (
177 WHERE biblionumber = ?
178 AND itemnumber IS NOT NULL
179 AND (found IS NOT NULL OR priority = 0)
181 AND items.biblionumber = ?";
182 my @params = ($biblionumber, $biblionumber);
183 if ($#branches_to_use > -1) {
184 $items_query .= " AND holdingbranch IN (" . join (",", map { "?" } @branches_to_use) . ")";
185 push @params, @branches_to_use;
187 my $sth = $dbh->prepare($items_query);
188 $sth->execute(@params);
190 my $items = $sth->fetchall_arrayref({});
191 $items = [ grep { my @transfers = GetTransfers($_->{itemnumber}); $#transfers == -1; } @$items ];
192 map { my $rule = GetBranchItemRule($_->{homebranch}, $_->{itype}); $_->{holdallowed} = $rule->{holdallowed}; $rule->{holdallowed} != 0 } @$items;
193 return [ grep { $_->{holdallowed} != 0 } @$items ];
196 =head2 MapItemsToHoldRequests
198 MapItemsToHoldRequests($hold_requests, $available_items);
202 sub MapItemsToHoldRequests {
203 my $hold_requests = shift;
204 my $available_items = shift;
205 my @branches_to_use = @_;
207 # handle trival cases
208 return unless scalar(@$hold_requests) > 0;
209 return unless scalar(@$available_items) > 0;
211 # identify item-level requests
212 my %specific_items_requested = map { $_->{itemnumber} => 1 }
213 grep { defined($_->{itemnumber}) }
216 # group available items by itemnumber
217 my %items_by_itemnumber = map { $_->{itemnumber} => $_ } @$available_items;
219 # items already allocated
220 my %allocated_items = ();
222 # map of items to hold requests
225 # figure out which item-level requests can be filled
226 my $num_items_remaining = scalar(@$available_items);
227 foreach my $request (@$hold_requests) {
228 last if $num_items_remaining == 0;
230 # is this an item-level request?
231 if (defined($request->{itemnumber})) {
232 # fill it if possible; if not skip it
233 if (exists $items_by_itemnumber{$request->{itemnumber}} and
234 not exists $allocated_items{$request->{itemnumber}}) {
235 $item_map{$request->{itemnumber}} = {
236 borrowernumber => $request->{borrowernumber},
237 biblionumber => $request->{biblionumber},
238 holdingbranch => $items_by_itemnumber{$request->{itemnumber}}->{holdingbranch},
239 pickup_branch => $request->{branchcode},
241 reservedate => $request->{reservedate},
242 reservenotes => $request->{reservenotes},
244 $allocated_items{$request->{itemnumber}}++;
245 $num_items_remaining--;
248 # it's title-level request that will take up one item
249 $num_items_remaining--;
253 # group available items by branch
254 my %items_by_branch = ();
255 foreach my $item (@$available_items) {
256 push @{ $items_by_branch{ $item->{holdingbranch} } }, $item unless exists $allocated_items{ $item->{itemnumber} };
259 # now handle the title-level requests
260 $num_items_remaining = scalar(@$available_items) - scalar(keys %allocated_items);
261 foreach my $request (@$hold_requests) {
262 last if $num_items_remaining <= 0;
263 next if defined($request->{itemnumber}); # already handled these
265 # look for local match first
266 my $pickup_branch = $request->{branchcode};
267 if (exists $items_by_branch{$pickup_branch} and
268 not ($items_by_branch{$pickup_branch}->[0]->{holdallowed} == 1 and
269 $request->{borrowerbranch} ne $items_by_branch{$pickup_branch}->[0]->{homebranch})
271 my $item = pop @{ $items_by_branch{$pickup_branch} };
272 delete $items_by_branch{$pickup_branch} if scalar(@{ $items_by_branch{$pickup_branch} }) == 0;
273 $item_map{$item->{itemnumber}} = {
274 borrowernumber => $request->{borrowernumber},
275 biblionumber => $request->{biblionumber},
276 holdingbranch => $pickup_branch,
277 pickup_branch => $pickup_branch,
279 reservedate => $request->{reservedate},
280 reservenotes => $request->{reservenotes},
282 $num_items_remaining--;
284 my @pull_branches = ();
285 if ($#branches_to_use > -1) {
286 @pull_branches = @branches_to_use;
288 @pull_branches = sort keys %items_by_branch;
290 foreach my $branch (@pull_branches) {
291 next unless exists $items_by_branch{$branch} and
292 not ($items_by_branch{$branch}->[0]->{holdallowed} == 1 and
293 $request->{borrowerbranch} ne $items_by_branch{$branch}->[0]->{homebranch});
294 my $item = pop @{ $items_by_branch{$branch} };
295 delete $items_by_branch{$branch} if scalar(@{ $items_by_branch{$branch} }) == 0;
296 $item_map{$item->{itemnumber}} = {
297 borrowernumber => $request->{borrowernumber},
298 biblionumber => $request->{biblionumber},
299 holdingbranch => $branch,
300 pickup_branch => $pickup_branch,
302 reservedate => $request->{reservedate},
303 reservenotes => $request->{reservenotes},
305 $num_items_remaining--;
313 =head2 CreatePickListFromItemMap
317 sub CreatePicklistFromItemMap {
318 my $item_map = shift;
320 my $dbh = C4::Context->dbh;
322 my $sth_load=$dbh->prepare("
323 INSERT INTO tmp_holdsqueue (biblionumber,itemnumber,barcode,surname,firstname,phone,borrowernumber,
324 cardnumber,reservedate,title, itemcallnumber,
325 holdingbranch,pickbranch,notes, item_level_request)
326 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
329 foreach my $itemnumber (sort keys %$item_map) {
330 my $mapped_item = $item_map->{$itemnumber};
331 my $biblionumber = $mapped_item->{biblionumber};
332 my $borrowernumber = $mapped_item->{borrowernumber};
333 my $pickbranch = $mapped_item->{pickup_branch};
334 my $holdingbranch = $mapped_item->{holdingbranch};
335 my $reservedate = $mapped_item->{reservedate};
336 my $reservenotes = $mapped_item->{reservenotes};
337 my $item_level = $mapped_item->{item_level};
339 my $item = GetItem($itemnumber);
340 my $barcode = $item->{barcode};
341 my $itemcallnumber = $item->{itemcallnumber};
343 my $borrower = GetMember('borrowernumber'=>$borrowernumber);
344 my $cardnumber = $borrower->{'cardnumber'};
345 my $surname = $borrower->{'surname'};
346 my $firstname = $borrower->{'firstname'};
347 my $phone = $borrower->{'phone'};
349 my $bib = GetBiblioData($biblionumber);
350 my $title = $bib->{title};
352 $sth_load->execute($biblionumber, $itemnumber, $barcode, $surname, $firstname, $phone, $borrowernumber,
353 $cardnumber, $reservedate, $title, $itemcallnumber,
354 $holdingbranch, $pickbranch, $reservenotes, $item_level);
358 =head2 AddToHoldTargetMap
362 sub AddToHoldTargetMap {
363 my $item_map = shift;
365 my $dbh = C4::Context->dbh;
368 INSERT INTO hold_fill_targets (borrowernumber, biblionumber, itemnumber, source_branchcode, item_level_request)
369 VALUES (?, ?, ?, ?, ?)
371 my $sth_insert = $dbh->prepare($insert_sql);
373 foreach my $itemnumber (keys %$item_map) {
374 my $mapped_item = $item_map->{$itemnumber};
375 $sth_insert->execute($mapped_item->{borrowernumber}, $mapped_item->{biblionumber}, $itemnumber,
376 $mapped_item->{holdingbranch}, $mapped_item->{item_level});
380 =head2 _get_branches_to_pull_from
382 Query system preferences to get ordered list of
383 branches to use to fill hold requests.
387 sub _get_branches_to_pull_from {
388 my @branches_to_use = ();
390 my $static_branch_list = C4::Context->preference("StaticHoldsQueueWeight");
391 if ($static_branch_list) {
392 @branches_to_use = map { s/^\s+//; s/\s+$//; $_; } split /,/, $static_branch_list;
395 @branches_to_use = shuffle(@branches_to_use) if C4::Context->preference("RandomizeHoldsQueueWeight");
397 return @branches_to_use;