Bug 19179: Email option for SMSSendDriver is not documented
[koha.git] / C4 / Reserves.pm
blob9bac1e9190ab6d483af668a6b5cc958b4589c1d8
1 package C4::Reserves;
3 # Copyright 2000-2002 Katipo Communications
4 # 2006 SAN Ouest Provence
5 # 2007-2010 BibLibre Paul POULAIN
6 # 2011 Catalyst IT
8 # This file is part of Koha.
10 # Koha is free software; you can redistribute it and/or modify it
11 # under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
15 # Koha is distributed in the hope that it will be useful, but
16 # WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with Koha; if not, see <http://www.gnu.org/licenses>.
24 use strict;
25 #use warnings; FIXME - Bug 2505
26 use C4::Context;
27 use C4::Biblio;
28 use C4::Members;
29 use C4::Items;
30 use C4::Circulation;
31 use C4::Accounts;
33 # for _koha_notify_reserve
34 use C4::Members::Messaging;
35 use C4::Members qw();
36 use C4::Letters;
37 use C4::Log;
39 use Koha::Biblios;
40 use Koha::DateUtils;
41 use Koha::Calendar;
42 use Koha::Database;
43 use Koha::Hold;
44 use Koha::Old::Hold;
45 use Koha::Holds;
46 use Koha::Libraries;
47 use Koha::IssuingRules;
48 use Koha::Items;
49 use Koha::ItemTypes;
50 use Koha::Patrons;
52 use List::MoreUtils qw( firstidx any );
53 use Carp;
54 use Data::Dumper;
56 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
58 =head1 NAME
60 C4::Reserves - Koha functions for dealing with reservation.
62 =head1 SYNOPSIS
64 use C4::Reserves;
66 =head1 DESCRIPTION
68 This modules provides somes functions to deal with reservations.
70 Reserves are stored in reserves table.
71 The following columns contains important values :
72 - priority >0 : then the reserve is at 1st stage, and not yet affected to any item.
73 =0 : then the reserve is being dealed
74 - found : NULL : means the patron requested the 1st available, and we haven't chosen the item
75 T(ransit) : the reserve is linked to an item but is in transit to the pickup branch
76 W(aiting) : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
77 F(inished) : the reserve has been completed, and is done
78 - itemnumber : empty : the reserve is still unaffected to an item
79 filled: the reserve is attached to an item
80 The complete workflow is :
81 ==== 1st use case ====
82 patron request a document, 1st available : P >0, F=NULL, I=NULL
83 a library having it run "transfertodo", and clic on the list
84 if there is no transfer to do, the reserve waiting
85 patron can pick it up P =0, F=W, I=filled
86 if there is a transfer to do, write in branchtransfer P =0, F=T, I=filled
87 The pickup library receive the book, it check in P =0, F=W, I=filled
88 The patron borrow the book P =0, F=F, I=filled
90 ==== 2nd use case ====
91 patron requests a document, a given item,
92 If pickup is holding branch P =0, F=W, I=filled
93 If transfer needed, write in branchtransfer P =0, F=T, I=filled
94 The pickup library receive the book, it checks it in P =0, F=W, I=filled
95 The patron borrow the book P =0, F=F, I=filled
97 =head1 FUNCTIONS
99 =cut
101 BEGIN {
102 require Exporter;
103 @ISA = qw(Exporter);
104 @EXPORT = qw(
105 &AddReserve
107 &GetReserveStatus
109 &GetOtherReserves
111 &ModReserveFill
112 &ModReserveAffect
113 &ModReserve
114 &ModReserveStatus
115 &ModReserveCancelAll
116 &ModReserveMinusPriority
117 &MoveReserve
119 &CheckReserves
120 &CanBookBeReserved
121 &CanItemBeReserved
122 &CanReserveBeCanceledFromOpac
123 &CancelExpiredReserves
125 &AutoUnsuspendReserves
127 &IsAvailableForItemLevelRequest
129 &AlterPriority
130 &ToggleLowestPriority
132 &ReserveSlip
133 &ToggleSuspend
134 &SuspendAll
136 &GetReservesControlBranch
138 IsItemOnHoldAndFound
140 GetMaxPatronHoldsForRecord
142 @EXPORT_OK = qw( MergeHolds );
145 =head2 AddReserve
147 AddReserve($branch,$borrowernumber,$biblionumber,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
149 Adds reserve and generates HOLDPLACED message.
151 The following tables are available witin the HOLDPLACED message:
153 branches
154 borrowers
155 biblio
156 biblioitems
157 items
158 reserves
160 =cut
162 sub AddReserve {
163 my (
164 $branch, $borrowernumber, $biblionumber, $bibitems,
165 $priority, $resdate, $expdate, $notes,
166 $title, $checkitem, $found, $itemtype
167 ) = @_;
169 $resdate = output_pref( { str => dt_from_string( $resdate ), dateonly => 1, dateformat => 'iso' })
170 or output_pref({ dt => dt_from_string, dateonly => 1, dateformat => 'iso' });
172 $expdate = output_pref({ str => $expdate, dateonly => 1, dateformat => 'iso' });
174 # if we have an item selectionned, and the pickup branch is the same as the holdingbranch
175 # of the document, we force the value $priority and $found .
176 if ( $checkitem and not C4::Context->preference('ReservesNeedReturns') ) {
177 $priority = 0;
178 my $item = Koha::Items->find( $checkitem ); # FIXME Prevent bad calls
179 if ( $item->holdingbranch eq $branch ) {
180 $found = 'W';
184 if ( C4::Context->preference('AllowHoldDateInFuture') ) {
186 # Make room in reserves for this before those of a later reserve date
187 $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
190 my $waitingdate;
192 # If the reserv had the waiting status, we had the value of the resdate
193 if ( $found eq 'W' ) {
194 $waitingdate = $resdate;
197 # Don't add itemtype limit if specific item is selected
198 $itemtype = undef if $checkitem;
200 # updates take place here
201 my $hold = Koha::Hold->new(
203 borrowernumber => $borrowernumber,
204 biblionumber => $biblionumber,
205 reservedate => $resdate,
206 branchcode => $branch,
207 priority => $priority,
208 reservenotes => $notes,
209 itemnumber => $checkitem,
210 found => $found,
211 waitingdate => $waitingdate,
212 expirationdate => $expdate,
213 itemtype => $itemtype,
215 )->store();
216 $hold->set_waiting() if $found eq 'W';
218 logaction( 'HOLDS', 'CREATE', $hold->id, Dumper($hold->unblessed) )
219 if C4::Context->preference('HoldsLog');
221 my $reserve_id = $hold->id();
223 # add a reserve fee if needed
224 if ( C4::Context->preference('HoldFeeMode') ne 'any_time_is_collected' ) {
225 my $reserve_fee = GetReserveFee( $borrowernumber, $biblionumber );
226 ChargeReserveFee( $borrowernumber, $reserve_fee, $title );
229 _FixPriority({ biblionumber => $biblionumber});
231 # Send e-mail to librarian if syspref is active
232 if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
233 my $patron = Koha::Patrons->find( $borrowernumber );
234 my $library = $patron->library;
235 if ( my $letter = C4::Letters::GetPreparedLetter (
236 module => 'reserves',
237 letter_code => 'HOLDPLACED',
238 branchcode => $branch,
239 lang => $patron->lang,
240 tables => {
241 'branches' => $library->unblessed,
242 'borrowers' => $patron->unblessed,
243 'biblio' => $biblionumber,
244 'biblioitems' => $biblionumber,
245 'items' => $checkitem,
246 'reserves' => $hold->unblessed,
248 ) ) {
250 my $admin_email_address = $library->branchemail || C4::Context->preference('KohaAdminEmailAddress');
252 C4::Letters::EnqueueLetter(
253 { letter => $letter,
254 borrowernumber => $borrowernumber,
255 message_transport_type => 'email',
256 from_address => $admin_email_address,
257 to_address => $admin_email_address,
263 return $reserve_id;
266 =head2 CanBookBeReserved
268 $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber, $branchcode)
269 if ($canReserve eq 'OK') { #We can reserve this Item! }
271 See CanItemBeReserved() for possible return values.
273 =cut
275 sub CanBookBeReserved{
276 my ($borrowernumber, $biblionumber, $pickup_branchcode) = @_;
278 my @itemnumbers = Koha::Items->search({ biblionumber => $biblionumber})->get_column("itemnumber");
279 #get items linked via host records
280 my @hostitems = get_hostitemnumbers_of($biblionumber);
281 if (@hostitems){
282 push (@itemnumbers, @hostitems);
285 my $canReserve;
286 foreach my $itemnumber (@itemnumbers) {
287 $canReserve = CanItemBeReserved( $borrowernumber, $itemnumber, $pickup_branchcode );
288 return { status => 'OK' } if $canReserve->{status} eq 'OK';
290 return $canReserve;
293 =head2 CanItemBeReserved
295 $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber, $branchcode)
296 if ($canReserve->{status} eq 'OK') { #We can reserve this Item! }
298 @RETURNS { status => OK }, if the Item can be reserved.
299 { status => ageRestricted }, if the Item is age restricted for this borrower.
300 { status => damaged }, if the Item is damaged.
301 { status => cannotReserveFromOtherBranches }, if syspref 'canreservefromotherbranches' is OK.
302 { status => tooManyReserves, limit => $limit }, if the borrower has exceeded their maximum reserve amount.
303 { status => notReservable }, if holds on this item are not allowed
304 { status => libraryNotFound }, if given branchcode is not an existing library
305 { status => libraryNotPickupLocation }, if given branchcode is not configured to be a pickup location
307 =cut
309 sub CanItemBeReserved {
310 my ( $borrowernumber, $itemnumber, $pickup_branchcode ) = @_;
312 my $dbh = C4::Context->dbh;
313 my $ruleitemtype; # itemtype of the matching issuing rule
314 my $allowedreserves = 0; # Total number of holds allowed across all records
315 my $holds_per_record = 1; # Total number of holds allowed for this one given record
317 # we retrieve borrowers and items informations #
318 # item->{itype} will come for biblioitems if necessery
319 my $item = GetItem($itemnumber);
320 my $biblio = Koha::Biblios->find( $item->{biblionumber} );
321 my $patron = Koha::Patrons->find( $borrowernumber );
322 my $borrower = $patron->unblessed;
324 # If an item is damaged and we don't allow holds on damaged items, we can stop right here
325 return { status =>'damaged' }
326 if ( $item->{damaged}
327 && !C4::Context->preference('AllowHoldsOnDamagedItems') );
329 # Check for the age restriction
330 my ( $ageRestriction, $daysToAgeRestriction ) =
331 C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction, $borrower );
332 return { status => 'ageRestricted' } if $daysToAgeRestriction && $daysToAgeRestriction > 0;
334 # Check that the patron doesn't have an item level hold on this item already
335 return { status =>'itemAlreadyOnHold' }
336 if Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->count();
338 my $controlbranch = C4::Context->preference('ReservesControlBranch');
340 my $querycount = q{
341 SELECT count(*) AS count
342 FROM reserves
343 LEFT JOIN items USING (itemnumber)
344 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
345 LEFT JOIN borrowers USING (borrowernumber)
346 WHERE borrowernumber = ?
349 my $branchcode = "";
350 my $branchfield = "reserves.branchcode";
352 if ( $controlbranch eq "ItemHomeLibrary" ) {
353 $branchfield = "items.homebranch";
354 $branchcode = $item->{homebranch};
356 elsif ( $controlbranch eq "PatronLibrary" ) {
357 $branchfield = "borrowers.branchcode";
358 $branchcode = $borrower->{branchcode};
361 # we retrieve rights
362 if ( my $rights = GetHoldRule( $borrower->{'categorycode'}, $item->{'itype'}, $branchcode ) ) {
363 $ruleitemtype = $rights->{itemtype};
364 $allowedreserves = $rights->{reservesallowed};
365 $holds_per_record = $rights->{holds_per_record};
367 else {
368 $ruleitemtype = '*';
371 $item = Koha::Items->find( $itemnumber );
372 my $holds = Koha::Holds->search(
374 borrowernumber => $borrowernumber,
375 biblionumber => $item->biblionumber,
376 found => undef, # Found holds don't count against a patron's holds limit
379 if ( $holds->count() >= $holds_per_record ) {
380 return { status => "tooManyHoldsForThisRecord", limit => $holds_per_record };
383 # we retrieve count
385 $querycount .= "AND $branchfield = ?";
387 # If using item-level itypes, fall back to the record
388 # level itemtype if the hold has no associated item
389 $querycount .=
390 C4::Context->preference('item-level_itypes')
391 ? " AND COALESCE( items.itype, biblioitems.itemtype ) = ?"
392 : " AND biblioitems.itemtype = ?"
393 if ( $ruleitemtype ne "*" );
395 my $sthcount = $dbh->prepare($querycount);
397 if ( $ruleitemtype eq "*" ) {
398 $sthcount->execute( $borrowernumber, $branchcode );
400 else {
401 $sthcount->execute( $borrowernumber, $branchcode, $ruleitemtype );
404 my $reservecount = "0";
405 if ( my $rowcount = $sthcount->fetchrow_hashref() ) {
406 $reservecount = $rowcount->{count};
409 # we check if it's ok or not
410 if ( $reservecount >= $allowedreserves ) {
411 return { status => 'tooManyReserves', limit => $allowedreserves };
414 # Now we need to check hold limits by patron category
415 my $schema = Koha::Database->new()->schema();
416 my $rule = $schema->resultset('BranchBorrowerCircRule')->find(
418 branchcode => $branchcode,
419 categorycode => $borrower->{categorycode},
422 $rule ||= $schema->resultset('DefaultBorrowerCircRule')->find(
424 categorycode => $borrower->{categorycode}
427 if ( $rule && defined $rule->max_holds ) {
428 my $total_holds_count = Koha::Holds->search(
430 borrowernumber => $borrower->{borrowernumber}
432 )->count();
434 return { status => 'tooManyReserves', limit => $rule->max_holds } if $total_holds_count >= $rule->max_holds;
437 my $circ_control_branch =
438 C4::Circulation::_GetCircControlBranch( $item->unblessed(), $borrower );
439 my $branchitemrule =
440 C4::Circulation::GetBranchItemRule( $circ_control_branch, $item->itype );
442 if ( $branchitemrule->{holdallowed} == 0 ) {
443 return { status => 'notReservable' };
446 if ( $branchitemrule->{holdallowed} == 1
447 && $borrower->{branchcode} ne $item->homebranch )
449 return { status => 'cannotReserveFromOtherBranches' };
452 # If reservecount is ok, we check item branch if IndependentBranches is ON
453 # and canreservefromotherbranches is OFF
454 if ( C4::Context->preference('IndependentBranches')
455 and !C4::Context->preference('canreservefromotherbranches') )
457 my $itembranch = $item->homebranch;
458 if ( $itembranch ne $borrower->{branchcode} ) {
459 return { status => 'cannotReserveFromOtherBranches' };
463 if ($pickup_branchcode) {
464 my $destination = Koha::Libraries->find({
465 branchcode => $pickup_branchcode,
467 unless ($destination) {
468 return { status => 'libraryNotFound' };
470 unless ($destination->pickup_location) {
471 return { status => 'libraryNotPickupLocation' };
475 return { status => 'OK' };
478 =head2 CanReserveBeCanceledFromOpac
480 $number = CanReserveBeCanceledFromOpac($reserve_id, $borrowernumber);
482 returns 1 if reserve can be cancelled by user from OPAC.
483 First check if reserve belongs to user, next checks if reserve is not in
484 transfer or waiting status
486 =cut
488 sub CanReserveBeCanceledFromOpac {
489 my ($reserve_id, $borrowernumber) = @_;
491 return unless $reserve_id and $borrowernumber;
492 my $reserve = Koha::Holds->find($reserve_id);
494 return 0 unless $reserve->borrowernumber == $borrowernumber;
495 return 0 if ( $reserve->found eq 'W' ) or ( $reserve->found eq 'T' );
497 return 1;
501 =head2 GetOtherReserves
503 ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
505 Check queued list of this document and check if this document must be transferred
507 =cut
509 sub GetOtherReserves {
510 my ($itemnumber) = @_;
511 my $messages;
512 my $nextreservinfo;
513 my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
514 if ($checkreserves) {
515 my $iteminfo = GetItem($itemnumber);
516 if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
517 $messages->{'transfert'} = $checkreserves->{'branchcode'};
518 #minus priorities of others reservs
519 ModReserveMinusPriority(
520 $itemnumber,
521 $checkreserves->{'reserve_id'},
524 #launch the subroutine dotransfer
525 C4::Items::ModItemTransfer(
526 $itemnumber,
527 $iteminfo->{'holdingbranch'},
528 $checkreserves->{'branchcode'}
533 #step 2b : case of a reservation on the same branch, set the waiting status
534 else {
535 $messages->{'waiting'} = 1;
536 ModReserveMinusPriority(
537 $itemnumber,
538 $checkreserves->{'reserve_id'},
540 ModReserveStatus($itemnumber,'W');
543 $nextreservinfo = $checkreserves->{'borrowernumber'};
546 return ( $messages, $nextreservinfo );
549 =head2 ChargeReserveFee
551 $fee = ChargeReserveFee( $borrowernumber, $fee, $title );
553 Charge the fee for a reserve (if $fee > 0)
555 =cut
557 sub ChargeReserveFee {
558 my ( $borrowernumber, $fee, $title ) = @_;
559 return if !$fee || $fee==0; # the last test is needed to include 0.00
560 my $accquery = qq{
561 INSERT INTO accountlines ( borrowernumber, accountno, date, amount, description, accounttype, amountoutstanding ) VALUES (?, ?, NOW(), ?, ?, 'Res', ?)
563 my $dbh = C4::Context->dbh;
564 my $nextacctno = &getnextacctno( $borrowernumber );
565 $dbh->do( $accquery, undef, ( $borrowernumber, $nextacctno, $fee, "Reserve Charge - $title", $fee ) );
568 =head2 GetReserveFee
570 $fee = GetReserveFee( $borrowernumber, $biblionumber );
572 Calculate the fee for a reserve (if applicable).
574 =cut
576 sub GetReserveFee {
577 my ( $borrowernumber, $biblionumber ) = @_;
578 my $borquery = qq{
579 SELECT reservefee FROM borrowers LEFT JOIN categories ON borrowers.categorycode = categories.categorycode WHERE borrowernumber = ?
581 my $issue_qry = qq{
582 SELECT COUNT(*) FROM items
583 LEFT JOIN issues USING (itemnumber)
584 WHERE items.biblionumber=? AND issues.issue_id IS NULL
586 my $holds_qry = qq{
587 SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>?
590 my $dbh = C4::Context->dbh;
591 my ( $fee ) = $dbh->selectrow_array( $borquery, undef, ($borrowernumber) );
592 my $hold_fee_mode = C4::Context->preference('HoldFeeMode') || 'not_always';
593 if( $fee and $fee > 0 and $hold_fee_mode eq 'not_always' ) {
594 # This is a reconstruction of the old code:
595 # Compare number of items with items issued, and optionally check holds
596 # If not all items are issued and there are no holds: charge no fee
597 # NOTE: Lost, damaged, not-for-loan, etc. are just ignored here
598 my ( $notissued, $reserved );
599 ( $notissued ) = $dbh->selectrow_array( $issue_qry, undef,
600 ( $biblionumber ) );
601 if( $notissued ) {
602 ( $reserved ) = $dbh->selectrow_array( $holds_qry, undef,
603 ( $biblionumber, $borrowernumber ) );
604 $fee = 0 if $reserved == 0;
607 return $fee;
610 =head2 GetReserveStatus
612 $reservestatus = GetReserveStatus($itemnumber);
614 Takes an itemnumber and returns the status of the reserve placed on it.
615 If several reserves exist, the reserve with the lower priority is given.
617 =cut
619 ## FIXME: I don't think this does what it thinks it does.
620 ## It only ever checks the first reserve result, even though
621 ## multiple reserves for that bib can have the itemnumber set
622 ## the sub is only used once in the codebase.
623 sub GetReserveStatus {
624 my ($itemnumber) = @_;
626 my $dbh = C4::Context->dbh;
628 my ($sth, $found, $priority);
629 if ( $itemnumber ) {
630 $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
631 $sth->execute($itemnumber);
632 ($found, $priority) = $sth->fetchrow_array;
635 if(defined $found) {
636 return 'Waiting' if $found eq 'W' and $priority == 0;
637 return 'Finished' if $found eq 'F';
640 return 'Reserved' if $priority > 0;
642 return ''; # empty string here will remove need for checking undef, or less log lines
645 =head2 CheckReserves
647 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber);
648 ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode);
649 ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
651 Find a book in the reserves.
653 C<$itemnumber> is the book's item number.
654 C<$lookahead> is the number of days to look in advance for future reserves.
656 As I understand it, C<&CheckReserves> looks for the given item in the
657 reserves. If it is found, that's a match, and C<$status> is set to
658 C<Waiting>.
660 Otherwise, it finds the most important item in the reserves with the
661 same biblio number as this book (I'm not clear on this) and returns it
662 with C<$status> set to C<Reserved>.
664 C<&CheckReserves> returns a two-element list:
666 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
668 C<$reserve> is the reserve item that matched. It is a
669 reference-to-hash whose keys are mostly the fields of the reserves
670 table in the Koha database.
672 =cut
674 sub CheckReserves {
675 my ( $item, $barcode, $lookahead_days, $ignore_borrowers) = @_;
676 my $dbh = C4::Context->dbh;
677 my $sth;
678 my $select;
679 if (C4::Context->preference('item-level_itypes')){
680 $select = "
681 SELECT items.biblionumber,
682 items.biblioitemnumber,
683 itemtypes.notforloan,
684 items.notforloan AS itemnotforloan,
685 items.itemnumber,
686 items.damaged,
687 items.homebranch,
688 items.holdingbranch
689 FROM items
690 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
691 LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype
694 else {
695 $select = "
696 SELECT items.biblionumber,
697 items.biblioitemnumber,
698 itemtypes.notforloan,
699 items.notforloan AS itemnotforloan,
700 items.itemnumber,
701 items.damaged,
702 items.homebranch,
703 items.holdingbranch
704 FROM items
705 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
706 LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype
710 if ($item) {
711 $sth = $dbh->prepare("$select WHERE itemnumber = ?");
712 $sth->execute($item);
714 else {
715 $sth = $dbh->prepare("$select WHERE barcode = ?");
716 $sth->execute($barcode);
718 # note: we get the itemnumber because we might have started w/ just the barcode. Now we know for sure we have it.
719 my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged, $item_homebranch, $item_holdingbranch ) = $sth->fetchrow_array;
721 return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') );
723 return unless $itemnumber; # bail if we got nothing.
725 # if item is not for loan it cannot be reserved either.....
726 # except where items.notforloan < 0 : This indicates the item is holdable.
727 return if ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
729 # Find this item in the reserves
730 my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers);
732 # $priority and $highest are used to find the most important item
733 # in the list returned by &_Findgroupreserve. (The lower $priority,
734 # the more important the item.)
735 # $highest is the most important item we've seen so far.
736 my $highest;
737 if (scalar @reserves) {
738 my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
739 my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
740 my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl');
742 my $priority = 10000000;
743 foreach my $res (@reserves) {
744 if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
745 if ($res->{'found'} eq 'W') {
746 return ( "Waiting", $res, \@reserves ); # Found it, it is waiting
747 } else {
748 return ( "Reserved", $res, \@reserves ); # Found determinated hold, e. g. the tranferred one
750 } else {
751 my $patron;
752 my $iteminfo;
753 my $local_hold_match;
755 if ($LocalHoldsPriority) {
756 $patron = Koha::Patrons->find( $res->{borrowernumber} );
757 $iteminfo = C4::Items::GetItem($itemnumber);
759 my $local_holds_priority_item_branchcode =
760 $iteminfo->{$LocalHoldsPriorityItemControl};
761 my $local_holds_priority_patron_branchcode =
762 ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
763 ? $res->{branchcode}
764 : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
765 ? $patron->branchcode
766 : undef;
767 $local_hold_match =
768 $local_holds_priority_item_branchcode eq
769 $local_holds_priority_patron_branchcode;
772 # See if this item is more important than what we've got so far
773 if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) {
774 $iteminfo ||= C4::Items::GetItem($itemnumber);
775 next if $res->{itemtype} && $res->{itemtype} ne _get_itype( $iteminfo );
776 $patron ||= Koha::Patrons->find( $res->{borrowernumber} );
777 my $branch = GetReservesControlBranch( $iteminfo, $patron->unblessed );
778 my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'});
779 next if ($branchitemrule->{'holdallowed'} == 0);
780 next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $patron->branchcode));
781 next if ( ($branchitemrule->{hold_fulfillment_policy} ne 'any') && ($res->{branchcode} ne $iteminfo->{ $branchitemrule->{hold_fulfillment_policy} }) );
782 $priority = $res->{'priority'};
783 $highest = $res;
784 last if $local_hold_match;
790 # If we get this far, then no exact match was found.
791 # We return the most important (i.e. next) reservation.
792 if ($highest) {
793 $highest->{'itemnumber'} = $item;
794 return ( "Reserved", $highest, \@reserves );
797 return ( '' );
800 =head2 CancelExpiredReserves
802 CancelExpiredReserves();
804 Cancels all reserves with an expiration date from before today.
806 =cut
808 sub CancelExpiredReserves {
809 my $today = dt_from_string();
810 my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
811 my $expireWaiting = C4::Context->preference('ExpireReservesMaxPickUpDelay');
813 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
814 my $params = { expirationdate => { '<', $dtf->format_date($today) } };
815 $params->{found} = undef unless $expireWaiting;
817 # FIXME To move to Koha::Holds->search_expired (?)
818 my $holds = Koha::Holds->search( $params );
820 while ( my $hold = $holds->next ) {
821 my $calendar = Koha::Calendar->new( branchcode => $hold->branchcode );
823 next if !$cancel_on_holidays && $calendar->is_holiday( $today );
825 my $cancel_params = {};
826 if ( $hold->found eq 'W' ) {
827 $cancel_params->{charge_cancel_fee} = 1;
829 $hold->cancel( $cancel_params );
833 =head2 AutoUnsuspendReserves
835 AutoUnsuspendReserves();
837 Unsuspends all suspended reserves with a suspend_until date from before today.
839 =cut
841 sub AutoUnsuspendReserves {
842 my $today = dt_from_string();
844 my @holds = Koha::Holds->search( { suspend_until => { '<=' => $today->ymd() } } );
846 map { $_->suspend(0)->suspend_until(undef)->store() } @holds;
849 =head2 ModReserve
851 ModReserve({ rank => $rank,
852 reserve_id => $reserve_id,
853 branchcode => $branchcode
854 [, itemnumber => $itemnumber ]
855 [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
858 Change a hold request's priority or cancel it.
860 C<$rank> specifies the effect of the change. If C<$rank>
861 is 'W' or 'n', nothing happens. This corresponds to leaving a
862 request alone when changing its priority in the holds queue
863 for a bib.
865 If C<$rank> is 'del', the hold request is cancelled.
867 If C<$rank> is an integer greater than zero, the priority of
868 the request is set to that value. Since priority != 0 means
869 that the item is not waiting on the hold shelf, setting the
870 priority to a non-zero value also sets the request's found
871 status and waiting date to NULL.
873 The optional C<$itemnumber> parameter is used only when
874 C<$rank> is a non-zero integer; if supplied, the itemnumber
875 of the hold request is set accordingly; if omitted, the itemnumber
876 is cleared.
878 B<FIXME:> Note that the forgoing can have the effect of causing
879 item-level hold requests to turn into title-level requests. This
880 will be fixed once reserves has separate columns for requested
881 itemnumber and supplying itemnumber.
883 =cut
885 sub ModReserve {
886 my ( $params ) = @_;
888 my $rank = $params->{'rank'};
889 my $reserve_id = $params->{'reserve_id'};
890 my $branchcode = $params->{'branchcode'};
891 my $itemnumber = $params->{'itemnumber'};
892 my $suspend_until = $params->{'suspend_until'};
893 my $borrowernumber = $params->{'borrowernumber'};
894 my $biblionumber = $params->{'biblionumber'};
896 return if $rank eq "W";
897 return if $rank eq "n";
899 return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
901 my $hold;
902 unless ( $reserve_id ) {
903 my $holds = Koha::Holds->search({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber });
904 return unless $holds->count; # FIXME Should raise an exception
905 $hold = $holds->next;
906 $reserve_id = $hold->reserve_id;
909 $hold ||= Koha::Holds->find($reserve_id);
911 if ( $rank eq "del" ) {
912 $hold->cancel;
914 elsif ($rank =~ /^\d+/ and $rank > 0) {
915 logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
916 if C4::Context->preference('HoldsLog');
918 $hold->set(
920 priority => $rank,
921 branchcode => $branchcode,
922 itemnumber => $itemnumber,
923 found => undef,
924 waitingdate => undef
926 )->store();
928 if ( defined( $suspend_until ) ) {
929 if ( $suspend_until ) {
930 $suspend_until = eval { dt_from_string( $suspend_until ) };
931 $hold->suspend_hold( $suspend_until );
932 } else {
933 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
934 # If the hold is not suspended, this does nothing.
935 $hold->set( { suspend_until => undef } )->store();
939 _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
943 =head2 ModReserveFill
945 &ModReserveFill($reserve);
947 Fill a reserve. If I understand this correctly, this means that the
948 reserved book has been found and given to the patron who reserved it.
950 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
951 whose keys are fields from the reserves table in the Koha database.
953 =cut
955 sub ModReserveFill {
956 my ($res) = @_;
957 my $reserve_id = $res->{'reserve_id'};
959 my $hold = Koha::Holds->find($reserve_id);
961 # get the priority on this record....
962 my $priority = $hold->priority;
964 # update the hold statuses, no need to store it though, we will be deleting it anyway
965 $hold->set(
967 found => 'F',
968 priority => 0,
972 # FIXME Must call Koha::Hold->cancel ? => No, should call ->filled and add the correct log
973 Koha::Old::Hold->new( $hold->unblessed() )->store();
975 $hold->delete();
977 if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
978 my $reserve_fee = GetReserveFee( $hold->borrowernumber, $hold->biblionumber );
979 ChargeReserveFee( $hold->borrowernumber, $reserve_fee, $hold->biblio->title );
982 # now fix the priority on the others (if the priority wasn't
983 # already sorted!)....
984 unless ( $priority == 0 ) {
985 _FixPriority( { reserve_id => $reserve_id, biblionumber => $hold->biblionumber } );
989 =head2 ModReserveStatus
991 &ModReserveStatus($itemnumber, $newstatus);
993 Update the reserve status for the active (priority=0) reserve.
995 $itemnumber is the itemnumber the reserve is on
997 $newstatus is the new status.
999 =cut
1001 sub ModReserveStatus {
1003 #first : check if we have a reservation for this item .
1004 my ($itemnumber, $newstatus) = @_;
1005 my $dbh = C4::Context->dbh;
1007 my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1008 my $sth_set = $dbh->prepare($query);
1009 $sth_set->execute( $newstatus, $itemnumber );
1011 if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
1012 CartToShelf( $itemnumber );
1016 =head2 ModReserveAffect
1018 &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id);
1020 This function affect an item and a status for a given reserve, either fetched directly
1021 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
1022 is given, only first reserve returned is affected, which is ok for anything but
1023 multi-item holds.
1025 if $transferToDo is not set, then the status is set to "Waiting" as well.
1026 otherwise, a transfer is on the way, and the end of the transfer will
1027 take care of the waiting status
1029 =cut
1031 sub ModReserveAffect {
1032 my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id ) = @_;
1033 my $dbh = C4::Context->dbh;
1035 # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1036 # attached to $itemnumber
1037 my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1038 $sth->execute($itemnumber);
1039 my ($biblionumber) = $sth->fetchrow;
1041 # get request - need to find out if item is already
1042 # waiting in order to not send duplicate hold filled notifications
1044 my $hold;
1045 # Find hold by id if we have it
1046 $hold = Koha::Holds->find( $reserve_id ) if $reserve_id;
1047 # Find item level hold for this item if there is one
1048 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->next();
1049 # Find record level hold if there is no item level hold
1050 $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->next();
1052 return unless $hold;
1054 my $already_on_shelf = $hold->found && $hold->found eq 'W';
1056 $hold->itemnumber($itemnumber);
1057 $hold->set_waiting($transferToDo);
1059 _koha_notify_reserve( $hold->reserve_id )
1060 if ( !$transferToDo && !$already_on_shelf );
1062 _FixPriority( { biblionumber => $biblionumber } );
1064 if ( C4::Context->preference("ReturnToShelvingCart") ) {
1065 CartToShelf($itemnumber);
1068 return;
1071 =head2 ModReserveCancelAll
1073 ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1075 function to cancel reserv,check other reserves, and transfer document if it's necessary
1077 =cut
1079 sub ModReserveCancelAll {
1080 my $messages;
1081 my $nextreservinfo;
1082 my ( $itemnumber, $borrowernumber ) = @_;
1084 #step 1 : cancel the reservation
1085 my $holds = Koha::Holds->search({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1086 return unless $holds->count;
1087 $holds->next->cancel;
1089 #step 2 launch the subroutine of the others reserves
1090 ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1092 return ( $messages, $nextreservinfo );
1095 =head2 ModReserveMinusPriority
1097 &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1099 Reduce the values of queued list
1101 =cut
1103 sub ModReserveMinusPriority {
1104 my ( $itemnumber, $reserve_id ) = @_;
1106 #first step update the value of the first person on reserv
1107 my $dbh = C4::Context->dbh;
1108 my $query = "
1109 UPDATE reserves
1110 SET priority = 0 , itemnumber = ?
1111 WHERE reserve_id = ?
1113 my $sth_upd = $dbh->prepare($query);
1114 $sth_upd->execute( $itemnumber, $reserve_id );
1115 # second step update all others reserves
1116 _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1119 =head2 IsAvailableForItemLevelRequest
1121 my $is_available = IsAvailableForItemLevelRequest($item_record,$borrower_record);
1123 Checks whether a given item record is available for an
1124 item-level hold request. An item is available if
1126 * it is not lost AND
1127 * it is not damaged AND
1128 * it is not withdrawn AND
1129 * a waiting or in transit reserve is placed on
1130 * does not have a not for loan value > 0
1132 Need to check the issuingrules onshelfholds column,
1133 if this is set items on the shelf can be placed on hold
1135 Note that IsAvailableForItemLevelRequest() does not
1136 check if the staff operator is authorized to place
1137 a request on the item - in particular,
1138 this routine does not check IndependentBranches
1139 and canreservefromotherbranches.
1141 =cut
1143 sub IsAvailableForItemLevelRequest {
1144 my $item = shift;
1145 my $borrower = shift;
1147 my $dbh = C4::Context->dbh;
1148 # must check the notforloan setting of the itemtype
1149 # FIXME - a lot of places in the code do this
1150 # or something similar - need to be
1151 # consolidated
1152 my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
1153 my $item_object = Koha::Items->find( $item->{itemnumber } );
1154 my $itemtype = $item_object->effective_itemtype;
1155 my $notforloan_per_itemtype
1156 = $dbh->selectrow_array("SELECT notforloan FROM itemtypes WHERE itemtype = ?",
1157 undef, $itemtype);
1159 return 0 if
1160 $notforloan_per_itemtype ||
1161 $item->{itemlost} ||
1162 $item->{notforloan} > 0 ||
1163 $item->{withdrawn} ||
1164 ($item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1166 my $on_shelf_holds = Koha::IssuingRules->get_onshelfholds_policy( { item => $item_object, patron => $patron } );
1168 if ( $on_shelf_holds == 1 ) {
1169 return 1;
1170 } elsif ( $on_shelf_holds == 2 ) {
1171 my @items =
1172 Koha::Items->search( { biblionumber => $item->{biblionumber} } );
1174 my $any_available = 0;
1176 foreach my $i (@items) {
1178 my $circ_control_branch = C4::Circulation::_GetCircControlBranch( $i->unblessed(), $borrower );
1179 my $branchitemrule = C4::Circulation::GetBranchItemRule( $circ_control_branch, $i->itype );
1181 $any_available = 1
1182 unless $i->itemlost
1183 || $i->notforloan > 0
1184 || $i->withdrawn
1185 || $i->onloan
1186 || IsItemOnHoldAndFound( $i->id )
1187 || ( $i->damaged
1188 && !C4::Context->preference('AllowHoldsOnDamagedItems') )
1189 || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan
1190 || $branchitemrule->{holdallowed} == 1 && $borrower->{branchcode} ne $i->homebranch;
1193 return $any_available ? 0 : 1;
1194 } else { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved)
1195 return $item->{onloan} || IsItemOnHoldAndFound( $item->{itemnumber} );
1199 sub _get_itype {
1200 my $item = shift;
1202 my $itype;
1203 if (C4::Context->preference('item-level_itypes')) {
1204 # We can't trust GetItem to honour the syspref, so safest to do it ourselves
1205 # When GetItem is fixed, we can remove this
1206 $itype = $item->{itype};
1208 else {
1209 # XXX This is a bit dodgy. It relies on biblio itemtype column having different name.
1210 # So if we already have a biblioitems join when calling this function,
1211 # we don't need to access the database again
1212 $itype = $item->{itemtype};
1214 unless ($itype) {
1215 my $dbh = C4::Context->dbh;
1216 my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
1217 my $sth = $dbh->prepare($query);
1218 $sth->execute($item->{biblioitemnumber});
1219 if (my $data = $sth->fetchrow_hashref()){
1220 $itype = $data->{itemtype};
1223 return $itype;
1226 =head2 AlterPriority
1228 AlterPriority( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority );
1230 This function changes a reserve's priority up, down, to the top, or to the bottom.
1231 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1233 =cut
1235 sub AlterPriority {
1236 my ( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority ) = @_;
1238 my $hold = Koha::Holds->find( $reserve_id );
1239 return unless $hold;
1241 if ( $hold->cancellationdate ) {
1242 warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (" . $hold->cancellationdate . ')';
1243 return;
1246 if ( $where eq 'up' ) {
1247 return unless $prev_priority;
1248 _FixPriority({ reserve_id => $reserve_id, rank => $prev_priority })
1249 } elsif ( $where eq 'down' ) {
1250 return unless $next_priority;
1251 _FixPriority({ reserve_id => $reserve_id, rank => $next_priority })
1252 } elsif ( $where eq 'top' ) {
1253 _FixPriority({ reserve_id => $reserve_id, rank => $first_priority })
1254 } elsif ( $where eq 'bottom' ) {
1255 _FixPriority({ reserve_id => $reserve_id, rank => $last_priority });
1258 # FIXME Should return the new priority
1261 =head2 ToggleLowestPriority
1263 ToggleLowestPriority( $borrowernumber, $biblionumber );
1265 This function sets the lowestPriority field to true if is false, and false if it is true.
1267 =cut
1269 sub ToggleLowestPriority {
1270 my ( $reserve_id ) = @_;
1272 my $dbh = C4::Context->dbh;
1274 my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1275 $sth->execute( $reserve_id );
1277 _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1280 =head2 ToggleSuspend
1282 ToggleSuspend( $reserve_id );
1284 This function sets the suspend field to true if is false, and false if it is true.
1285 If the reserve is currently suspended with a suspend_until date, that date will
1286 be cleared when it is unsuspended.
1288 =cut
1290 sub ToggleSuspend {
1291 my ( $reserve_id, $suspend_until ) = @_;
1293 $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
1295 my $hold = Koha::Holds->find( $reserve_id );
1297 if ( $hold->is_suspended ) {
1298 $hold->resume()
1299 } else {
1300 $hold->suspend_hold( $suspend_until );
1304 =head2 SuspendAll
1306 SuspendAll(
1307 borrowernumber => $borrowernumber,
1308 [ biblionumber => $biblionumber, ]
1309 [ suspend_until => $suspend_until, ]
1310 [ suspend => $suspend ]
1313 This function accepts a set of hash keys as its parameters.
1314 It requires either borrowernumber or biblionumber, or both.
1316 suspend_until is wholly optional.
1318 =cut
1320 sub SuspendAll {
1321 my %params = @_;
1323 my $borrowernumber = $params{'borrowernumber'} || undef;
1324 my $biblionumber = $params{'biblionumber'} || undef;
1325 my $suspend_until = $params{'suspend_until'} || undef;
1326 my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1328 $suspend_until = eval { dt_from_string($suspend_until) }
1329 if ( defined($suspend_until) );
1331 return unless ( $borrowernumber || $biblionumber );
1333 my $params;
1334 $params->{found} = undef;
1335 $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1336 $params->{biblionumber} = $biblionumber if $biblionumber;
1338 my @holds = Koha::Holds->search($params);
1340 if ($suspend) {
1341 map { $_->suspend_hold($suspend_until) } @holds;
1343 else {
1344 map { $_->resume() } @holds;
1349 =head2 _FixPriority
1351 _FixPriority({
1352 reserve_id => $reserve_id,
1353 [rank => $rank,]
1354 [ignoreSetLowestRank => $ignoreSetLowestRank]
1359 _FixPriority({ biblionumber => $biblionumber});
1361 This routine adjusts the priority of a hold request and holds
1362 on the same bib.
1364 In the first form, where a reserve_id is passed, the priority of the
1365 hold is set to supplied rank, and other holds for that bib are adjusted
1366 accordingly. If the rank is "del", the hold is cancelled. If no rank
1367 is supplied, all of the holds on that bib have their priority adjusted
1368 as if the second form had been used.
1370 In the second form, where a biblionumber is passed, the holds on that
1371 bib (that are not captured) are sorted in order of increasing priority,
1372 then have reserves.priority set so that the first non-captured hold
1373 has its priority set to 1, the second non-captured hold has its priority
1374 set to 2, and so forth.
1376 In both cases, holds that have the lowestPriority flag on are have their
1377 priority adjusted to ensure that they remain at the end of the line.
1379 Note that the ignoreSetLowestRank parameter is meant to be used only
1380 when _FixPriority calls itself.
1382 =cut
1384 sub _FixPriority {
1385 my ( $params ) = @_;
1386 my $reserve_id = $params->{reserve_id};
1387 my $rank = $params->{rank} // '';
1388 my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1389 my $biblionumber = $params->{biblionumber};
1391 my $dbh = C4::Context->dbh;
1393 my $hold;
1394 if ( $reserve_id ) {
1395 $hold = Koha::Holds->find( $reserve_id );
1396 return unless $hold;
1399 unless ( $biblionumber ) { # FIXME This is a very weird API
1400 $biblionumber = $hold->biblionumber;
1403 if ( $rank eq "del" ) { # FIXME will crash if called without $hold
1404 $hold->cancel;
1406 elsif ( $rank eq "W" || $rank eq "0" ) {
1408 # make sure priority for waiting or in-transit items is 0
1409 my $query = "
1410 UPDATE reserves
1411 SET priority = 0
1412 WHERE reserve_id = ?
1413 AND found IN ('W', 'T')
1415 my $sth = $dbh->prepare($query);
1416 $sth->execute( $reserve_id );
1418 my @priority;
1420 # get whats left
1421 my $query = "
1422 SELECT reserve_id, borrowernumber, reservedate
1423 FROM reserves
1424 WHERE biblionumber = ?
1425 AND ((found <> 'W' AND found <> 'T') OR found IS NULL)
1426 ORDER BY priority ASC
1428 my $sth = $dbh->prepare($query);
1429 $sth->execute( $biblionumber );
1430 while ( my $line = $sth->fetchrow_hashref ) {
1431 push( @priority, $line );
1434 # To find the matching index
1435 my $i;
1436 my $key = -1; # to allow for 0 to be a valid result
1437 for ( $i = 0 ; $i < @priority ; $i++ ) {
1438 if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1439 $key = $i; # save the index
1440 last;
1444 # if index exists in array then move it to new position
1445 if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1446 my $new_rank = $rank -
1447 1; # $new_rank is what you want the new index to be in the array
1448 my $moving_item = splice( @priority, $key, 1 );
1449 splice( @priority, $new_rank, 0, $moving_item );
1452 # now fix the priority on those that are left....
1453 $query = "
1454 UPDATE reserves
1455 SET priority = ?
1456 WHERE reserve_id = ?
1458 $sth = $dbh->prepare($query);
1459 for ( my $j = 0 ; $j < @priority ; $j++ ) {
1460 $sth->execute(
1461 $j + 1,
1462 $priority[$j]->{'reserve_id'}
1466 $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1467 $sth->execute();
1469 unless ( $ignoreSetLowestRank ) {
1470 while ( my $res = $sth->fetchrow_hashref() ) {
1471 _FixPriority({
1472 reserve_id => $res->{'reserve_id'},
1473 rank => '999999',
1474 ignoreSetLowestRank => 1
1480 =head2 _Findgroupreserve
1482 @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1484 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1485 first match found. If neither, then we look for non-holds-queue based holds.
1486 Lookahead is the number of days to look in advance.
1488 C<&_Findgroupreserve> returns :
1489 C<@results> is an array of references-to-hash whose keys are mostly
1490 fields from the reserves table of the Koha database, plus
1491 C<biblioitemnumber>.
1493 =cut
1495 sub _Findgroupreserve {
1496 my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1497 my $dbh = C4::Context->dbh;
1499 # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1500 # check for exact targeted match
1501 my $item_level_target_query = qq{
1502 SELECT reserves.biblionumber AS biblionumber,
1503 reserves.borrowernumber AS borrowernumber,
1504 reserves.reservedate AS reservedate,
1505 reserves.branchcode AS branchcode,
1506 reserves.cancellationdate AS cancellationdate,
1507 reserves.found AS found,
1508 reserves.reservenotes AS reservenotes,
1509 reserves.priority AS priority,
1510 reserves.timestamp AS timestamp,
1511 biblioitems.biblioitemnumber AS biblioitemnumber,
1512 reserves.itemnumber AS itemnumber,
1513 reserves.reserve_id AS reserve_id,
1514 reserves.itemtype AS itemtype
1515 FROM reserves
1516 JOIN biblioitems USING (biblionumber)
1517 JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1518 WHERE found IS NULL
1519 AND priority > 0
1520 AND item_level_request = 1
1521 AND itemnumber = ?
1522 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1523 AND suspend = 0
1524 ORDER BY priority
1526 my $sth = $dbh->prepare($item_level_target_query);
1527 $sth->execute($itemnumber, $lookahead||0);
1528 my @results;
1529 if ( my $data = $sth->fetchrow_hashref ) {
1530 push( @results, $data )
1531 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1533 return @results if @results;
1535 # check for title-level targeted match
1536 my $title_level_target_query = qq{
1537 SELECT reserves.biblionumber AS biblionumber,
1538 reserves.borrowernumber AS borrowernumber,
1539 reserves.reservedate AS reservedate,
1540 reserves.branchcode AS branchcode,
1541 reserves.cancellationdate AS cancellationdate,
1542 reserves.found AS found,
1543 reserves.reservenotes AS reservenotes,
1544 reserves.priority AS priority,
1545 reserves.timestamp AS timestamp,
1546 biblioitems.biblioitemnumber AS biblioitemnumber,
1547 reserves.itemnumber AS itemnumber,
1548 reserves.reserve_id AS reserve_id,
1549 reserves.itemtype AS itemtype
1550 FROM reserves
1551 JOIN biblioitems USING (biblionumber)
1552 JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1553 WHERE found IS NULL
1554 AND priority > 0
1555 AND item_level_request = 0
1556 AND hold_fill_targets.itemnumber = ?
1557 AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1558 AND suspend = 0
1559 ORDER BY priority
1561 $sth = $dbh->prepare($title_level_target_query);
1562 $sth->execute($itemnumber, $lookahead||0);
1563 @results = ();
1564 if ( my $data = $sth->fetchrow_hashref ) {
1565 push( @results, $data )
1566 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1568 return @results if @results;
1570 my $query = qq{
1571 SELECT reserves.biblionumber AS biblionumber,
1572 reserves.borrowernumber AS borrowernumber,
1573 reserves.reservedate AS reservedate,
1574 reserves.waitingdate AS waitingdate,
1575 reserves.branchcode AS branchcode,
1576 reserves.cancellationdate AS cancellationdate,
1577 reserves.found AS found,
1578 reserves.reservenotes AS reservenotes,
1579 reserves.priority AS priority,
1580 reserves.timestamp AS timestamp,
1581 reserves.itemnumber AS itemnumber,
1582 reserves.reserve_id AS reserve_id,
1583 reserves.itemtype AS itemtype
1584 FROM reserves
1585 WHERE reserves.biblionumber = ?
1586 AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1587 AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1588 AND suspend = 0
1589 ORDER BY priority
1591 $sth = $dbh->prepare($query);
1592 $sth->execute( $biblio, $itemnumber, $lookahead||0);
1593 @results = ();
1594 while ( my $data = $sth->fetchrow_hashref ) {
1595 push( @results, $data )
1596 unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1598 return @results;
1601 =head2 _koha_notify_reserve
1603 _koha_notify_reserve( $hold->reserve_id );
1605 Sends a notification to the patron that their hold has been filled (through
1606 ModReserveAffect, _not_ ModReserveFill)
1608 The letter code for this notice may be found using the following query:
1610 select distinct letter_code
1611 from message_transports
1612 inner join message_attributes using (message_attribute_id)
1613 where message_name = 'Hold_Filled'
1615 This will probably sipmly be 'HOLD', but because it is defined in the database,
1616 it is subject to addition or change.
1618 The following tables are availalbe witin the notice:
1620 branches
1621 borrowers
1622 biblio
1623 biblioitems
1624 reserves
1625 items
1627 =cut
1629 sub _koha_notify_reserve {
1630 my $reserve_id = shift;
1631 my $hold = Koha::Holds->find($reserve_id);
1632 my $borrowernumber = $hold->borrowernumber;
1634 my $patron = Koha::Patrons->find( $borrowernumber );
1636 # Try to get the borrower's email address
1637 my $to_address = $patron->notice_email_address;
1639 my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1640 borrowernumber => $borrowernumber,
1641 message_name => 'Hold_Filled'
1642 } );
1644 my $library = Koha::Libraries->find( $hold->branchcode )->unblessed;
1646 my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
1648 my %letter_params = (
1649 module => 'reserves',
1650 branchcode => $hold->branchcode,
1651 lang => $patron->lang,
1652 tables => {
1653 'branches' => $library,
1654 'borrowers' => $patron->unblessed,
1655 'biblio' => $hold->biblionumber,
1656 'biblioitems' => $hold->biblionumber,
1657 'reserves' => $hold->unblessed,
1658 'items' => $hold->itemnumber,
1662 my $notification_sent = 0; #Keeping track if a Hold_filled message is sent. If no message can be sent, then default to a print message.
1663 my $send_notification = sub {
1664 my ( $mtt, $letter_code ) = (@_);
1665 return unless defined $letter_code;
1666 $letter_params{letter_code} = $letter_code;
1667 $letter_params{message_transport_type} = $mtt;
1668 my $letter = C4::Letters::GetPreparedLetter ( %letter_params );
1669 unless ($letter) {
1670 warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1671 return;
1674 C4::Letters::EnqueueLetter( {
1675 letter => $letter,
1676 borrowernumber => $borrowernumber,
1677 from_address => $admin_email_address,
1678 message_transport_type => $mtt,
1679 } );
1682 while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1683 next if (
1684 ( $mtt eq 'email' and not $to_address ) # No email address
1685 or ( $mtt eq 'sms' and not $patron->smsalertnumber ) # No SMS number
1686 or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
1689 &$send_notification($mtt, $letter_code);
1690 $notification_sent++;
1692 #Making sure that a print notification is sent if no other transport types can be utilized.
1693 if (! $notification_sent) {
1694 &$send_notification('print', 'HOLD');
1699 =head2 _ShiftPriorityByDateAndPriority
1701 $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1703 This increments the priority of all reserves after the one
1704 with either the lowest date after C<$reservedate>
1705 or the lowest priority after C<$priority>.
1707 It effectively makes room for a new reserve to be inserted with a certain
1708 priority, which is returned.
1710 This is most useful when the reservedate can be set by the user. It allows
1711 the new reserve to be placed before other reserves that have a later
1712 reservedate. Since priority also is set by the form in reserves/request.pl
1713 the sub accounts for that too.
1715 =cut
1717 sub _ShiftPriorityByDateAndPriority {
1718 my ( $biblio, $resdate, $new_priority ) = @_;
1720 my $dbh = C4::Context->dbh;
1721 my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1722 my $sth = $dbh->prepare( $query );
1723 $sth->execute( $biblio, $resdate, $new_priority );
1724 my $min_priority = $sth->fetchrow;
1725 # if no such matches are found, $new_priority remains as original value
1726 $new_priority = $min_priority if ( $min_priority );
1728 # Shift the priority up by one; works in conjunction with the next SQL statement
1729 $query = "UPDATE reserves
1730 SET priority = priority+1
1731 WHERE biblionumber = ?
1732 AND borrowernumber = ?
1733 AND reservedate = ?
1734 AND found IS NULL";
1735 my $sth_update = $dbh->prepare( $query );
1737 # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1738 $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1739 $sth = $dbh->prepare( $query );
1740 $sth->execute( $new_priority, $biblio );
1741 while ( my $row = $sth->fetchrow_hashref ) {
1742 $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1745 return $new_priority; # so the caller knows what priority they wind up receiving
1748 =head2 MoveReserve
1750 MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1752 Use when checking out an item to handle reserves
1753 If $cancelreserve boolean is set to true, it will remove existing reserve
1755 =cut
1757 sub MoveReserve {
1758 my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1760 my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
1761 my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber, undef, $lookahead );
1762 return unless $res;
1764 my $biblionumber = $res->{biblionumber};
1766 if ($res->{borrowernumber} == $borrowernumber) {
1767 ModReserveFill($res);
1769 else {
1770 # warn "Reserved";
1771 # The item is reserved by someone else.
1772 # Find this item in the reserves
1774 my $borr_res;
1775 foreach (@$all_reserves) {
1776 $_->{'borrowernumber'} == $borrowernumber or next;
1777 $_->{'biblionumber'} == $biblionumber or next;
1779 $borr_res = $_;
1780 last;
1783 if ( $borr_res ) {
1784 # The item is reserved by the current patron
1785 ModReserveFill($borr_res);
1788 if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
1789 RevertWaitingStatus({ itemnumber => $itemnumber });
1791 elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
1792 my $hold = Koha::Holds->find( $res->{reserve_id} );
1793 $hold->cancel;
1798 =head2 MergeHolds
1800 MergeHolds($dbh,$to_biblio, $from_biblio);
1802 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
1804 =cut
1806 sub MergeHolds {
1807 my ( $dbh, $to_biblio, $from_biblio ) = @_;
1808 my $sth = $dbh->prepare(
1809 "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
1811 $sth->execute($from_biblio);
1812 if ( my $data = $sth->fetchrow_hashref() ) {
1814 # holds exist on old record, if not we don't need to do anything
1815 $sth = $dbh->prepare(
1816 "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
1817 $sth->execute( $to_biblio, $from_biblio );
1819 # Reorder by date
1820 # don't reorder those already waiting
1822 $sth = $dbh->prepare(
1823 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
1825 my $upd_sth = $dbh->prepare(
1826 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
1827 AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
1829 $sth->execute( $to_biblio, 'W', 'T' );
1830 my $priority = 1;
1831 while ( my $reserve = $sth->fetchrow_hashref() ) {
1832 $upd_sth->execute(
1833 $priority, $to_biblio,
1834 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
1835 $reserve->{'itemnumber'}
1837 $priority++;
1842 =head2 RevertWaitingStatus
1844 RevertWaitingStatus({ itemnumber => $itemnumber });
1846 Reverts a 'waiting' hold back to a regular hold with a priority of 1.
1848 Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
1849 item level hold, even if it was only a bibliolevel hold to
1850 begin with. This is because we can no longer know if a hold
1851 was item-level or bib-level after a hold has been set to
1852 waiting status.
1854 =cut
1856 sub RevertWaitingStatus {
1857 my ( $params ) = @_;
1858 my $itemnumber = $params->{'itemnumber'};
1860 return unless ( $itemnumber );
1862 my $dbh = C4::Context->dbh;
1864 ## Get the waiting reserve we want to revert
1865 my $query = "
1866 SELECT * FROM reserves
1867 WHERE itemnumber = ?
1868 AND found IS NOT NULL
1870 my $sth = $dbh->prepare( $query );
1871 $sth->execute( $itemnumber );
1872 my $reserve = $sth->fetchrow_hashref();
1874 ## Increment the priority of all other non-waiting
1875 ## reserves for this bib record
1876 $query = "
1877 UPDATE reserves
1879 priority = priority + 1
1880 WHERE
1881 biblionumber = ?
1883 priority > 0
1885 $sth = $dbh->prepare( $query );
1886 $sth->execute( $reserve->{'biblionumber'} );
1888 ## Fix up the currently waiting reserve
1889 $query = "
1890 UPDATE reserves
1892 priority = 1,
1893 found = NULL,
1894 waitingdate = NULL
1895 WHERE
1896 reserve_id = ?
1898 $sth = $dbh->prepare( $query );
1899 $sth->execute( $reserve->{'reserve_id'} );
1900 _FixPriority( { biblionumber => $reserve->{biblionumber} } );
1903 =head2 ReserveSlip
1905 ReserveSlip(
1907 branchcode => $branchcode,
1908 borrowernumber => $borrowernumber,
1909 biblionumber => $biblionumber,
1910 [ itemnumber => $itemnumber, ]
1911 [ barcode => $barcode, ]
1915 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
1917 The letter code will be HOLD_SLIP, and the following tables are
1918 available within the slip:
1920 reserves
1921 branches
1922 borrowers
1923 biblio
1924 biblioitems
1925 items
1927 =cut
1929 sub ReserveSlip {
1930 my ($args) = @_;
1931 my $branchcode = $args->{branchcode};
1932 my $borrowernumber = $args->{borrowernumber};
1933 my $biblionumber = $args->{biblionumber};
1934 my $itemnumber = $args->{itemnumber};
1935 my $barcode = $args->{barcode};
1938 my $patron = Koha::Patrons->find($borrowernumber);
1940 my $hold;
1941 if ($itemnumber || $barcode ) {
1942 $itemnumber ||= Koha::Items->find( { barcode => $barcode } )->itemnumber;
1944 $hold = Koha::Holds->search(
1946 biblionumber => $biblionumber,
1947 borrowernumber => $borrowernumber,
1948 itemnumber => $itemnumber
1950 )->next;
1952 else {
1953 $hold = Koha::Holds->search(
1955 biblionumber => $biblionumber,
1956 borrowernumber => $borrowernumber
1958 )->next;
1961 return unless $hold;
1962 my $reserve = $hold->unblessed;
1964 return C4::Letters::GetPreparedLetter (
1965 module => 'circulation',
1966 letter_code => 'HOLD_SLIP',
1967 branchcode => $branchcode,
1968 lang => $patron->lang,
1969 tables => {
1970 'reserves' => $reserve,
1971 'branches' => $reserve->{branchcode},
1972 'borrowers' => $reserve->{borrowernumber},
1973 'biblio' => $reserve->{biblionumber},
1974 'biblioitems' => $reserve->{biblionumber},
1975 'items' => $reserve->{itemnumber},
1980 =head2 GetReservesControlBranch
1982 my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
1984 Return the branchcode to be used to determine which reserves
1985 policy applies to a transaction.
1987 C<$item> is a hashref for an item. Only 'homebranch' is used.
1989 C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
1991 =cut
1993 sub GetReservesControlBranch {
1994 my ( $item, $borrower ) = @_;
1996 my $reserves_control = C4::Context->preference('ReservesControlBranch');
1998 my $branchcode =
1999 ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2000 : ( $reserves_control eq 'PatronLibrary' ) ? $borrower->{'branchcode'}
2001 : undef;
2003 return $branchcode;
2006 =head2 CalculatePriority
2008 my $p = CalculatePriority($biblionumber, $resdate);
2010 Calculate priority for a new reserve on biblionumber, placing it at
2011 the end of the line of all holds whose start date falls before
2012 the current system time and that are neither on the hold shelf
2013 or in transit.
2015 The reserve date parameter is optional; if it is supplied, the
2016 priority is based on the set of holds whose start date falls before
2017 the parameter value.
2019 After calculation of this priority, it is recommended to call
2020 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2021 AddReserves.
2023 =cut
2025 sub CalculatePriority {
2026 my ( $biblionumber, $resdate ) = @_;
2028 my $sql = q{
2029 SELECT COUNT(*) FROM reserves
2030 WHERE biblionumber = ?
2031 AND priority > 0
2032 AND (found IS NULL OR found = '')
2034 #skip found==W or found==T (waiting or transit holds)
2035 if( $resdate ) {
2036 $sql.= ' AND ( reservedate <= ? )';
2038 else {
2039 $sql.= ' AND ( reservedate < NOW() )';
2041 my $dbh = C4::Context->dbh();
2042 my @row = $dbh->selectrow_array(
2043 $sql,
2044 undef,
2045 $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2048 return @row ? $row[0]+1 : 1;
2051 =head2 IsItemOnHoldAndFound
2053 my $bool = IsItemFoundHold( $itemnumber );
2055 Returns true if the item is currently on hold
2056 and that hold has a non-null found status ( W, T, etc. )
2058 =cut
2060 sub IsItemOnHoldAndFound {
2061 my ($itemnumber) = @_;
2063 my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2065 my $found = $rs->count(
2067 itemnumber => $itemnumber,
2068 found => { '!=' => undef }
2072 return $found;
2075 =head2 GetMaxPatronHoldsForRecord
2077 my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
2079 For multiple holds on a given record for a given patron, the max
2080 number of record level holds that a patron can be placed is the highest
2081 value of the holds_per_record rule for each item if the record for that
2082 patron. This subroutine finds and returns the highest holds_per_record
2083 rule value for a given patron id and record id.
2085 =cut
2087 sub GetMaxPatronHoldsForRecord {
2088 my ( $borrowernumber, $biblionumber ) = @_;
2090 my $patron = Koha::Patrons->find($borrowernumber);
2091 my @items = Koha::Items->search( { biblionumber => $biblionumber } );
2093 my $controlbranch = C4::Context->preference('ReservesControlBranch');
2095 my $categorycode = $patron->categorycode;
2096 my $branchcode;
2097 $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
2099 my $max = 0;
2100 foreach my $item (@items) {
2101 my $itemtype = $item->effective_itemtype();
2103 $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
2105 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2106 my $holds_per_record = $rule ? $rule->{holds_per_record} : 0;
2107 $max = $holds_per_record if $holds_per_record > $max;
2110 return $max;
2113 =head2 GetHoldRule
2115 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2117 Returns the matching hold related issuingrule fields for a given
2118 patron category, itemtype, and library.
2120 =cut
2122 sub GetHoldRule {
2123 my ( $categorycode, $itemtype, $branchcode ) = @_;
2125 my $dbh = C4::Context->dbh;
2127 my $sth = $dbh->prepare(
2129 SELECT categorycode, itemtype, branchcode, reservesallowed, holds_per_record
2130 FROM issuingrules
2131 WHERE (categorycode in (?,'*') )
2132 AND (itemtype IN (?,'*'))
2133 AND (branchcode IN (?,'*'))
2134 ORDER BY categorycode DESC,
2135 itemtype DESC,
2136 branchcode DESC
2140 $sth->execute( $categorycode, $itemtype, $branchcode );
2142 return $sth->fetchrow_hashref();
2145 =head1 AUTHOR
2147 Koha Development Team <http://koha-community.org/>
2149 =cut