4 #written 2/1/00 by chris@katipo.oc.nz
5 # Copyright 2000-2002 Katipo Communications
6 # Parts Copyright 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>.
25 script to place reserves/requests
32 use List
::MoreUtils qw
/uniq/;
33 use Date
::Calc qw
/Date_to_Days/;
42 use C4
::Utils
::DataTables
::Members
;
44 use C4
::Search
; # enabled_staff_search_views
50 my $dbh = C4
::Context
->dbh;
52 my ( $template, $borrowernumber, $cookie, $flags ) = get_template_and_user
(
54 template_name
=> "reserve/request.tt",
58 flagsrequired
=> { reserveforothers
=> 'place_holds' },
62 my $multihold = $input->param('multi_hold');
63 $template->param(multi_hold
=> $multihold);
64 my $showallitems = $input->param('showallitems');
66 my $itemtypes = GetItemTypes
();
68 # Select borrowers infos
69 my $findborrower = $input->param('findborrower');
70 $findborrower = '' unless defined $findborrower;
71 $findborrower =~ s
|,| |g
;
72 my $borrowernumber_hold = $input->param('borrowernumber') || '';
76 my $exceeded_maxreserves;
77 my $exceeded_holds_per_record;
79 my $date = output_pref
({ dt
=> dt_from_string
, dateformat
=> 'iso', dateonly
=> 1 });
80 my $action = $input->param('action');
83 if ( $action eq 'move' ) {
84 my $where = $input->param('where');
85 my $reserve_id = $input->param('reserve_id');
86 AlterPriority
( $where, $reserve_id );
87 } elsif ( $action eq 'cancel' ) {
88 my $reserve_id = $input->param('reserve_id');
89 CancelReserve
({ reserve_id
=> $reserve_id });
90 } elsif ( $action eq 'setLowestPriority' ) {
91 my $reserve_id = $input->param('reserve_id');
92 ToggleLowestPriority
( $reserve_id );
93 } elsif ( $action eq 'toggleSuspend' ) {
94 my $reserve_id = $input->param('reserve_id');
95 my $suspend_until = $input->param('suspend_until');
96 ToggleSuspend
( $reserve_id, $suspend_until );
100 my $borrower = C4
::Members
::GetMember
( cardnumber
=> $findborrower );
102 $borrowernumber_hold = $borrower->{borrowernumber
};
104 my $dt_params = { iDisplayLength
=> -1 };
105 my $results = C4
::Utils
::DataTables
::Members
::search
(
107 searchmember
=> $findborrower,
108 dt_params
=> $dt_params,
111 my $borrowers = $results->{patrons
};
112 if ( scalar @
$borrowers == 1 ) {
113 $borrowernumber_hold = $borrowers->[0]->{borrowernumber
};
114 } elsif ( @
$borrowers ) {
115 $template->param( borrowers
=> $borrowers );
117 $messageborrower = "'$findborrower'";
122 my @biblionumbers = ();
123 my $biblionumbers = $input->param('biblionumbers');
125 @biblionumbers = split '/', $biblionumbers;
127 push @biblionumbers, $input->multi_param('biblionumber');
131 # If we have the borrowernumber because we've performed an action, then we
132 # don't want to try to place another reserve.
133 if ($borrowernumber_hold && !$action) {
134 my $borrowerinfo = GetMember
( borrowernumber
=> $borrowernumber_hold );
137 # we check the reserves of the borrower, and if he can reserv a document
138 # FIXME At this time we have a simple count of reservs, but, later, we could improve the infos "title" ...
141 GetReserveCount
( $borrowerinfo->{'borrowernumber'} );
143 my $new_reserves_count = scalar( @biblionumbers );
145 my $maxreserves = C4
::Context
->preference('maxreserves');
147 && ( $reserves_count + $new_reserves_count > $maxreserves ) )
149 my $new_reserves_allowed =
150 $maxreserves - $reserves_count > 0
151 ?
$maxreserves - $reserves_count
154 $exceeded_maxreserves = 1;
156 new_reserves_allowed
=> $new_reserves_allowed,
157 new_reserves_count
=> $new_reserves_count,
158 reserves_count
=> $reserves_count,
159 maxreserves
=> $maxreserves,
163 # we check the date expiry of the borrower (only if there is an expiry date, otherwise, set to 1 (warn)
164 my $expiry_date = $borrowerinfo->{dateexpiry
};
165 my $expiry = 0; # flag set if patron account has expired
166 if ($expiry_date and $expiry_date ne '0000-00-00' and
167 Date_to_Days
(split /-/,$date) > Date_to_Days
(split /-/,$expiry_date)) {
171 # check if the borrower make the reserv in a different branch
172 if ( $borrowerinfo->{'branchcode'} ne C4
::Context
->userenv->{'branch'} ) {
176 my $is_debarred = Koha
::Patrons
->find( $borrowerinfo->{borrowernumber
} )->is_debarred;
178 borrowernumber
=> $borrowerinfo->{'borrowernumber'},
179 borrowersurname
=> $borrowerinfo->{'surname'},
180 borrowerfirstname
=> $borrowerinfo->{'firstname'},
181 borrowerstreetaddress
=> $borrowerinfo->{'address'},
182 borrowercity
=> $borrowerinfo->{'city'},
183 borrowerphone
=> $borrowerinfo->{'phone'},
184 borrowermobile
=> $borrowerinfo->{'mobile'},
185 borrowerfax
=> $borrowerinfo->{'fax'},
186 borrowerphonepro
=> $borrowerinfo->{'phonepro'},
187 borroweremail
=> $borrowerinfo->{'email'},
188 borroweremailpro
=> $borrowerinfo->{'emailpro'},
189 borrowercategory
=> $borrowerinfo->{'category'},
190 cardnumber
=> $borrowerinfo->{'cardnumber'},
192 diffbranch
=> $diffbranch,
193 messages
=> $messages,
194 warnings
=> $warnings,
195 restricted
=> $is_debarred,
196 amount_outstanding
=> GetMemberAccountRecords
($borrowerinfo->{borrowernumber
}),
200 $template->param( messageborrower
=> $messageborrower );
202 # FIXME launch another time GetMember perhaps until
203 my $borrowerinfo = GetMember
( borrowernumber
=> $borrowernumber_hold );
205 my $logged_in_patron = Koha
::Patrons
->find( $borrowernumber );
207 my $itemdata_enumchron = 0;
209 foreach my $biblionumber (@biblionumbers) {
210 next unless $biblionumber =~ m
|^\d
+$|;
212 my %biblioloopiter = ();
214 my $dat = GetBiblioData
($biblionumber);
216 my $canReserve = CanBookBeReserved
( $borrowerinfo->{borrowernumber
}, $biblionumber );
218 if ( $canReserve eq 'OK' ) {
220 #All is OK and we can continue
222 elsif ( $canReserve eq 'tooManyReserves' ) {
223 $exceeded_maxreserves = 1;
225 elsif ( $canReserve eq 'tooManyHoldsForThisRecord' ) {
226 $exceeded_holds_per_record = 1;
227 $biblioloopiter{$canReserve} = 1;
229 elsif ( $canReserve eq 'ageRestricted' ) {
230 $template->param( $canReserve => 1 );
231 $biblioloopiter{$canReserve} = 1;
234 $biblioloopiter{$canReserve} = 1;
237 my $force_hold_level;
238 if ( $borrowerinfo->{borrowernumber
} ) {
239 # For multiple holds per record, if a patron has previously placed a hold,
240 # the patron can only place more holds of the same type. That is, if the
241 # patron placed a record level hold, all the holds the patron places must
242 # be record level. If the patron placed an item level hold, all holds
243 # the patron places must be item level
244 my $holds = Koha
::Holds
->search(
246 borrowernumber
=> $borrowerinfo->{borrowernumber
},
247 biblionumber
=> $biblionumber,
251 $force_hold_level = $holds->forced_hold_level();
252 $biblioloopiter{force_hold_level
} = $force_hold_level;
253 $template->param( force_hold_level
=> $force_hold_level );
255 # For a librarian to be able to place multiple record holds for a patron for a record,
256 # we must find out what the maximum number of holds they can place for the patron is
257 my $max_holds_for_record = GetMaxPatronHoldsForRecord
( $borrowerinfo->{borrowernumber
}, $biblionumber );
258 my $remaining_holds_for_record = $max_holds_for_record - $holds->count();
259 $biblioloopiter{remaining_holds_for_record
} = $max_holds_for_record;
260 $template->param( max_holds_for_record
=> $max_holds_for_record );
261 $template->param( remaining_holds_for_record
=> $remaining_holds_for_record );
264 # Check to see if patron is allowed to place holds on records where the
265 # patron already has an item from that record checked out
266 my $alreadypossession;
267 if ( !C4
::Context
->preference('AllowHoldsOnPatronsPossessions')
268 && CheckIfIssuedToPatron
( $borrowerinfo->{borrowernumber
}, $biblionumber ) )
270 $template->param( alreadypossession
=> $alreadypossession, );
274 my $count = Koha
::Holds
->search( { biblionumber
=> $biblionumber } )->count();
275 my $totalcount = $count;
277 # FIXME think @optionloop, is maybe obsolete, or must be switchable by a systeme preference fixed rank or not
278 # make priorities options
281 for ( 1 .. $count + 1 ) {
286 selected
=> ( $_ == $count + 1 ),
290 # adding a fixed value for priority options
291 my $fixedRank = $count+1;
293 my %itemnumbers_of_biblioitem;
296 ## $items is array of 'item' table numbers
297 if (my $items = get_itemnumbers_of
($biblionumber)->{$biblionumber}){
298 @itemnumbers = @
$items;
300 my @hostitems = get_hostitemnumbers_of
($biblionumber);
302 $template->param('hostitemsflag' => 1);
303 push(@itemnumbers, @hostitems);
307 $template->param('noitems' => 1);
308 $biblioloopiter{noitems
} = 1;
311 ## Hash of item number to 'item' table fields
312 my $iteminfos_of = GetItemInfosOf
(@itemnumbers);
314 ## Here we go backwards again to create hash of biblioitemnumber to itemnumbers,
315 ## when by definition all of the itemnumber have the same biblioitemnumber
316 foreach my $itemnumber (@itemnumbers) {
317 my $biblioitemnumber = $iteminfos_of->{$itemnumber}->{biblioitemnumber
};
318 push( @
{ $itemnumbers_of_biblioitem{$biblioitemnumber} }, $itemnumber );
321 ## Should be same as biblionumber
322 my @biblioitemnumbers = keys %itemnumbers_of_biblioitem;
324 my $notforloan_label_of = get_notforloan_label_of
();
326 ## Hash of biblioitemnumber to 'biblioitem' table records
327 my $biblioiteminfos_of = GetBiblioItemInfosOf
(@biblioitemnumbers);
331 my @available_itemtypes;
332 foreach my $biblioitemnumber (@biblioitemnumbers) {
333 my $biblioitem = $biblioiteminfos_of->{$biblioitemnumber};
334 my $num_available = 0;
335 my $num_override = 0;
338 $biblioitem->{force_hold_level
} = $force_hold_level;
340 if ( $biblioitem->{biblioitemnumber
} ne $biblionumber ) {
341 $biblioitem->{hostitemsflag
} = 1;
344 $biblioloopiter{description
} = $biblioitem->{description
};
345 $biblioloopiter{itypename
} = $biblioitem->{description
};
346 if ( $biblioitem->{itemtype
} ) {
348 $biblioitem->{description
} =
349 $itemtypes->{ $biblioitem->{itemtype
} }{description
};
351 $biblioloopiter{imageurl
} =
352 getitemtypeimagelocation
( 'intranet',
353 $itemtypes->{ $biblioitem->{itemtype
} }{imageurl
} );
356 foreach my $itemnumber ( @
{ $itemnumbers_of_biblioitem{$biblioitemnumber} } ) {
357 my $item = $iteminfos_of->{$itemnumber};
359 $item->{force_hold_level
} = $force_hold_level;
361 unless (C4
::Context
->preference('item-level_itypes')) {
362 $item->{itype
} = $biblioitem->{itemtype
};
365 $item->{itypename
} = $itemtypes->{ $item->{itype
} }{description
};
366 $item->{imageurl
} = getitemtypeimagelocation
( 'intranet', $itemtypes->{ $item->{itype
} }{imageurl
} );
367 $item->{homebranch
} = $item->{homebranch
};
369 # if the holdingbranch is different than the homebranch, we show the
370 # holdingbranch of the document too
371 if ( $item->{homebranch
} ne $item->{holdingbranch
} ) {
372 $item->{holdingbranch
} = $item->{holdingbranch
};
375 if($item->{biblionumber
} ne $biblionumber){
376 $item->{hostitemsflag
}=1;
377 $item->{hosttitle
} = GetBiblioData
($item->{biblionumber
})->{title
};
380 # if the item is currently on loan, we display its return date and
381 # change the background color
382 my $issues= GetItemIssue
($itemnumber);
383 if ( $issues->{'date_due'} ) {
384 $item->{date_due
} = $issues->{date_due_sql
};
385 $item->{backgroundcolor
} = 'onloan';
389 my ($reservedate,$reservedfor,$expectedAt,$reserve_id,$wait) = GetReservesFromItemnumber
($itemnumber);
390 if ( defined $reservedate ) {
391 my $ItemBorrowerReserveInfo = GetMember
( borrowernumber
=> $reservedfor );
393 $item->{backgroundcolor
} = 'reserved';
394 $item->{reservedate
} = output_pref
({ dt
=> dt_from_string
( $reservedate ), dateonly
=> 1 });
395 $item->{ReservedForBorrowernumber
} = $reservedfor;
396 $item->{ReservedForSurname
} = $ItemBorrowerReserveInfo->{'surname'};
397 $item->{ReservedForFirstname
} = $ItemBorrowerReserveInfo->{'firstname'};
398 $item->{ExpectedAtLibrary
} = $expectedAt;
399 $item->{waitingdate
} = $wait;
402 # Management of the notforloan document
403 if ( $item->{notforloan
} ) {
404 $item->{backgroundcolor
} = 'other';
405 $item->{notforloanvalue
} =
406 $notforloan_label_of->{ $item->{notforloan
} };
409 # Management of lost or long overdue items
410 if ( $item->{itemlost
} ) {
412 # FIXME localized strings should never be in Perl code
414 $item->{itemlost
} == 1 ?
"(lost)"
415 : $item->{itemlost
} == 2 ?
"(long overdue)"
417 $item->{backgroundcolor
} = 'other';
418 if ($logged_in_patron->category->hidelostitems && !$showallitems) {
424 # Check the transit status
425 my ( $transfertwhen, $transfertfrom, $transfertto ) =
426 GetTransfers
($itemnumber);
428 if ( defined $transfertwhen && $transfertwhen ne '' ) {
429 $item->{transfertwhen
} = output_pref
({ dt
=> dt_from_string
( $transfertwhen ), dateonly
=> 1 });
430 $item->{transfertfrom
} = $transfertfrom;
431 $item->{transfertto
} = $transfertto;
432 $item->{nocancel
} = 1;
435 # If there is no loan, return and transfer, we show a checkbox.
436 $item->{notforloan
} ||= 0;
438 # if independent branches is on we need to check if the person can reserve
439 # for branches they arent logged in to
440 if ( C4
::Context
->preference("IndependentBranches") ) {
441 if (! C4
::Context
->preference("canreservefromotherbranches")){
442 # cant reserve items so need to check if item homebranch and userenv branch match if not we cant reserve
443 my $userenv = C4
::Context
->userenv;
444 unless ( C4
::Context
->IsSuperLibrarian ) {
445 $item->{cantreserve
} = 1 if ( $item->{homebranch
} ne $userenv->{branch
} );
450 my $branch = C4
::Circulation
::_GetCircControlBranch
($item, $borrowerinfo);
452 my $branchitemrule = GetBranchItemRule
( $branch, $item->{'itype'} );
454 $item->{'holdallowed'} = $branchitemrule->{'holdallowed'};
456 my $can_item_be_reserved = CanItemBeReserved
( $borrowerinfo->{borrowernumber
}, $itemnumber );
457 $item->{not_holdable
} = $can_item_be_reserved unless ( $can_item_be_reserved eq 'OK' );
459 $item->{item_level_holds
} = OPACItemHoldsAllowed
( $item, $borrowerinfo );
462 !$item->{cantreserve
}
463 && !$exceeded_maxreserves
464 && IsAvailableForItemLevelRequest
($item, $borrowerinfo)
465 && $can_item_be_reserved eq 'OK'
468 $item->{available
} = 1;
471 push( @available_itemtypes, $item->{itype
} );
473 elsif ( C4
::Context
->preference('AllowHoldPolicyOverride') ) {
474 # If AllowHoldPolicyOverride is set, it should override EVERY restriction, not just branch item rules
475 $item->{override
} = 1;
479 # If none of the conditions hold true, then neither override nor available is set and the item cannot be checked
481 # Show serial enumeration when needed
482 if ($item->{enumchron
}) {
483 $itemdata_enumchron = 1;
486 push @
{ $biblioitem->{itemloop
} }, $item;
489 if ( $num_override == scalar( @
{ $biblioitem->{itemloop
} } ) ) { # That is, if all items require an override
490 $template->param( override_required
=> 1 );
491 } elsif ( $num_available == 0 ) {
492 $template->param( none_available
=> 1 );
493 $biblioloopiter{warn} = 1;
494 $biblioloopiter{none_avail
} = 1;
496 $template->param( hiddencount
=> $hiddencount);
498 push @bibitemloop, $biblioitem;
501 @available_itemtypes = uniq
( @available_itemtypes );
502 $template->param( available_itemtypes
=> \
@available_itemtypes );
504 # existingreserves building
506 my @reserves = Koha
::Holds
->search( { biblionumber
=> $biblionumber }, { order_by
=> 'priority' } );
509 my $a_found = $a->found() || '';
510 my $b_found = $a->found() || '';
511 $a_found cmp $b_found;
517 for ( my $i = 1 ; $i <= $totalcount ; $i++ ) {
522 selected
=> ( $i == $res->priority() ),
527 if ( $res->is_found() ) {
528 $reserve{'holdingbranch'} = $res->item()->holdingbranch();
529 $reserve{'biblionumber'} = $res->item()->biblionumber();
530 $reserve{'barcodenumber'} = $res->item()->barcode();
531 $reserve{'wbrcode'} = $res->branchcode();
532 $reserve{'itemnumber'} = $res->itemnumber();
533 $reserve{'wbrname'} = $res->branch()->branchname();
535 if ( $reserve{'holdingbranch'} eq $reserve{'wbrcode'} ) {
537 # Just because the holdingbranch matches the reserve branch doesn't mean the item
538 # has arrived at the destination, check for an open transfer for the item as well
539 my ( $transfertwhen, $transfertfrom, $transferto ) =
540 C4
::Circulation
::GetTransfers
( $res->itemnumber() );
541 if ( not $transferto or $transferto ne $res->branchcode() ) {
542 $reserve{'atdestination'} = 1;
546 # set found to 1 if reserve is waiting for patron pickup
547 $reserve{'found'} = $res->is_found();
548 $reserve{'intransit'} = $res->is_in_transit();
550 elsif ( $res->priority() > 0 ) {
551 if ( my $item = $res->item() ) {
552 $reserve{'itemnumber'} = $item->id();
553 $reserve{'barcodenumber'} = $item->barcode();
554 $reserve{'item_level_hold'} = 1;
558 # get borrowers reserve info
559 if ( C4
::Context
->preference('HidePatronName') ) {
560 $reserve{'hidename'} = 1;
561 $reserve{'cardnumber'} = $res->borrower()->cardnumber();
563 $reserve{'expirationdate'} = output_pref
( { dt
=> dt_from_string
( $res->expirationdate ), dateonly
=> 1 } )
564 unless ( !defined( $res->expirationdate ) || $res->expirationdate eq '0000-00-00' );
565 $reserve{'date'} = output_pref
( { dt
=> dt_from_string
( $res->reservedate ), dateonly
=> 1 } );
566 $reserve{'borrowernumber'} = $res->borrowernumber();
567 $reserve{'biblionumber'} = $res->biblionumber();
568 $reserve{'borrowernumber'} = $res->borrowernumber();
569 $reserve{'firstname'} = $res->borrower()->firstname();
570 $reserve{'surname'} = $res->borrower()->surname();
571 $reserve{'notes'} = $res->reservenotes();
572 $reserve{'waiting_date'} = $res->waitingdate();
573 $reserve{'waiting_until'} = $res->is_waiting() ?
$res->waiting_expires_on() : undef;
574 $reserve{'ccode'} = $res->item() ?
$res->item()->ccode() : undef;
575 $reserve{'barcode'} = $res->item() ?
$res->item()->barcode() : undef;
576 $reserve{'priority'} = $res->priority();
577 $reserve{'lowestPriority'} = $res->lowestPriority();
578 $reserve{'optionloop'} = \
@optionloop;
579 $reserve{'suspend'} = $res->suspend();
580 $reserve{'suspend_until'} = $res->suspend_until();
581 $reserve{'reserve_id'} = $res->reserve_id();
582 $reserve{itemtype
} = $res->itemtype();
583 $reserve{branchcode
} = $res->branchcode();
585 push( @reserveloop, \
%reserve );
588 # get the time for the form name...
593 fixedRank
=> $fixedRank,
598 optionloop
=> \
@optionloop,
599 bibitemloop
=> \
@bibitemloop,
600 itemdata_enumchron
=> $itemdata_enumchron,
602 biblionumber
=> $biblionumber,
603 findborrower
=> $findborrower,
604 title
=> $dat->{title
},
605 author
=> $dat->{author
},
607 C4
::Search
::enabled_staff_search_views
,
609 if (defined $borrowerinfo && exists $borrowerinfo->{'branchcode'}) {
610 $template->param( borrower_branchcode
=> $borrowerinfo->{'branchcode'},);
613 $biblioloopiter{biblionumber
} = $biblionumber;
614 $biblioloopiter{title
} = $dat->{title
};
615 $biblioloopiter{rank
} = $fixedRank;
616 $biblioloopiter{reserveloop
} = \
@reserveloop;
619 $template->param( reserveloop
=> \
@reserveloop );
622 push @biblioloop, \
%biblioloopiter;
625 $template->param( biblioloop
=> \
@biblioloop );
626 $template->param( biblionumbers
=> $biblionumbers );
627 $template->param( exceeded_maxreserves
=> $exceeded_maxreserves );
628 $template->param( exceeded_holds_per_record
=> $exceeded_holds_per_record );
631 $template->param( multi_hold
=> 1 );
634 if ( C4
::Context
->preference( 'AllowHoldDateInFuture' ) ) {
635 $template->param( reserve_in_future
=> 1 );
639 SuspendHoldsIntranet
=> C4
::Context
->preference('SuspendHoldsIntranet'),
640 AutoResumeSuspendedHolds
=> C4
::Context
->preference('AutoResumeSuspendedHolds'),
644 output_html_with_http_headers
$input, $cookie, $template->output;
646 sub sort_borrowerlist
{
647 my $borrowerslist = shift;
650 uc( $a->{surname
} . $a->{firstname
} ) cmp
651 uc( $b->{surname
} . $b->{firstname
} )