Koha/opac/opac-reserve.pl
Martin Renvoize 1ff3d65c70
Bug 35977: (follow-up) Cleaner working approach
This patch removes the Date_from_syspref recently added and replaces it
with the correct parseDate call as per bug 35559. We also clean up the
code around setting the input value in the first place and use iso which
is what the rest of flatpickr expects and now is handled correctly in
futuredate pickers.

Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
2024-05-02 13:19:12 +02:00

630 lines
22 KiB
Perl
Executable file

#!/usr/bin/perl
# Copyright Katipo Communications 2002
# Copyright Koha Development team 2012
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use Modern::Perl;
use CGI qw ( -utf8 );
use C4::Auth qw( get_template_and_user );
use C4::Koha qw( getitemtypeimagelocation getitemtypeimagesrc );
use C4::Circulation qw( GetBranchItemRule );
use C4::Reserves qw( CanItemBeReserved CanBookBeReserved AddReserve IsAvailableForItemLevelRequest GetReserveFee );
use C4::Biblio qw( GetBiblioData GetFrameworkCode );
use C4::Output qw( output_html_with_http_headers );
use C4::Context;
use C4::Members;
use C4::Overdues;
use Koha::AuthorisedValues;
use Koha::Biblios;
use Koha::CirculationRules;
use Koha::DateUtils qw( dt_from_string );
use Koha::Items;
use Koha::ItemTypes;
use Koha::Checkouts;
use Koha::Libraries;
use Koha::Patrons;
use List::MoreUtils qw( uniq );
my $maxreserves = C4::Context->preference("maxreserves");
my $query = CGI->new;
my $op = $query->param('op') // q{};
# if OPACHoldRequests (for placing holds) is disabled, leave immediately
if ( ! C4::Context->preference('OPACHoldRequests') ) {
print $query->redirect("/cgi-bin/koha/errors/404.pl");
exit;
}
my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
{
template_name => "opac-reserve.tt",
query => $query,
type => "opac",
}
);
my $patron = Koha::Patrons->find( $borrowernumber, { prefetch => ['categorycode'] } );
my $category = $patron->category;
my $can_place_hold_if_available_at_pickup = C4::Context->preference('OPACHoldsIfAvailableAtPickup');
unless ( $can_place_hold_if_available_at_pickup ) {
my @patron_categories = split ',', C4::Context->preference('OPACHoldsIfAvailableAtPickupExceptions');
if ( @patron_categories ) {
my $categorycode = $patron->categorycode;
$can_place_hold_if_available_at_pickup = grep { $_ eq $categorycode } @patron_categories;
}
}
my $itemtypes = { map { $_->{itemtype} => $_ } @{ Koha::ItemTypes->search_with_localization->unblessed } };
# There are two ways of calling this script, with a single biblio num
# or multiple biblio nums.
my $biblionumbers = $query->param('biblionumbers');
my $reserveMode = $query->param('reserve_mode');
if ($reserveMode && ($reserveMode eq 'single')) {
my $bib = $query->param('single_bib');
$biblionumbers = "$bib/";
}
if (! $biblionumbers) {
$biblionumbers = $query->param('biblionumber');
}
if ( !$biblionumbers && $op ne 'cud-place_reserve' ) {
$template->param( message => 1, no_biblionumber => 1 );
output_html_with_http_headers $query, $cookie, $template->output, undef, { force_no_caching => 1 };
exit;
}
# Pass the numbers to the page so they can be fed back
# when the hold is confirmed. TODO: Not necessary?
$template->param( biblionumbers => $biblionumbers );
# Each biblio number is suffixed with '/', e.g. "1/2/3/"
my @biblionumbers = split /\//, $biblionumbers;
if ( $#biblionumbers < 0 && $op ne 'cud-place_reserve' ) {
# TODO: New message?
$template->param(message=>1, no_biblionumber=>1);
output_html_with_http_headers $query, $cookie, $template->output, undef, { force_no_caching => 1 };
exit;
}
#
#
# Here we check that the borrower can actually make reserves Stage 1.
#
#
my $noreserves = 0;
if ( $category->effective_BlockExpiredPatronOpacActions ) {
if ( $patron->is_expired ) {
# cannot reserve, their card has expired and the rules set mean this is not allowed
$noreserves = 1;
$template->param( message => 1, expired_patron => 1 );
}
}
my $maxoutstanding = C4::Context->preference("maxoutstanding");
my $amountoutstanding = $patron->account->balance;
if ( $amountoutstanding && ($amountoutstanding > $maxoutstanding) ) {
my $amount = sprintf "%.02f", $amountoutstanding;
$template->param( message => 1 );
$noreserves = 1;
$template->param( too_much_oweing => $amount );
}
if ( $patron->gonenoaddress && ($patron->gonenoaddress == 1) ) {
$noreserves = 1;
$template->param(
message => 1,
GNA => 1
);
}
if ( $patron->lost && ($patron->lost == 1) ) {
$noreserves = 1;
$template->param(
message => 1,
lost => 1
);
}
if ( $patron->is_debarred ) {
$noreserves = 1;
$template->param(
message => 1,
debarred => 1,
debarred_comment => $patron->debarredcomment,
debarred_date => $patron->debarred,
);
}
my $holds = $patron->holds;
my $reserves_count = $holds->count;
$template->param( RESERVES => $holds->unblessed );
if ( $maxreserves && ( $reserves_count >= $maxreserves ) ) {
$template->param( message => 1 );
$noreserves = 1;
$template->param( too_many_reserves => $holds->count );
}
if( $noreserves ){
output_html_with_http_headers $query, $cookie, $template->output, undef, { force_no_caching => 1 };
exit;
}
# pass the pickup branch along....
my $branch = $query->param('branch') || $patron->branchcode || C4::Context->userenv->{branch} || '' ;
$template->param( branch => $branch );
#
#
# Here we are carrying out the hold request, possibly
# with a specific item for each biblionumber.
#
#
if ( $op eq 'cud-place_reserve' ) {
my $reserve_cnt = 0;
if ($maxreserves) {
$reserve_cnt = $patron->holds->count;
}
# List is composed of alternating biblio/item/branch
my $selectedItems = $query->param('selecteditems');
if ($query->param('reserve_mode') eq 'single') {
# This indicates non-JavaScript mode, so there was
# only a single biblio number selected.
my $bib = $query->param('single_bib');
my $item = $query->param("checkitem_$bib");
if ($item eq 'any') {
$item = '';
}
my $branch = $query->param('branch');
$selectedItems = "$bib/$item/$branch/";
}
$selectedItems =~ s!/$!!;
my @selectedItems = split /\//, $selectedItems, -1;
# Make sure there is a biblionum/itemnum/branch triplet for each item.
# The itemnum can be 'any', meaning next available.
my $selectionCount = @selectedItems;
if (($selectionCount == 0) || (($selectionCount % 3) != 0)) {
$template->param(message=>1, bad_data=>1);
output_html_with_http_headers $query, $cookie, $template->output, undef, { force_no_caching => 1 };
exit;
}
my @failed_holds;
while (@selectedItems) {
my $biblioNum = shift(@selectedItems);
my $itemNum = shift(@selectedItems);
my $branch = shift(@selectedItems); # i.e., branch code, not name
my $canreserve = 0;
my $singleBranchMode = Koha::Libraries->search->count == 1;
if ( $singleBranchMode || ! C4::Context->preference("OPACAllowUserToChooseBranch") )
{ # single branch mode or disabled user choosing
$branch = $patron->branchcode;
}
# FIXME We shouldn't need to fetch the item here
my $item = $itemNum ? Koha::Items->find( $itemNum ) : undef;
# When choosing a specific item, the default pickup library should be dictated by the default hold policy
if ( ! C4::Context->preference("OPACAllowUserToChooseBranch") && $item ) {
my $type = $item->effective_itemtype;
my $reserves_control_branch = Koha::Policy::Holds->holds_control_library( $item, $patron );
my $rule = GetBranchItemRule( $reserves_control_branch, $type );
if ( $rule->{hold_fulfillment_policy} eq 'any' || $rule->{hold_fulfillment_policy} eq 'patrongroup' ) {
$branch = $patron->branchcode;
} elsif ( $rule->{hold_fulfillment_policy} eq 'holdgroup' ){
$branch = $item->homebranch;
} else {
my $policy = $rule->{hold_fulfillment_policy};
$branch = $item->$policy;
}
}
# if we have an item, we are placing the hold on the item's bib, in case of analytics
if ( $item ) {
$biblioNum = $item->biblionumber;
}
# Check for user supplied reserve date
my $startdate;
if ( C4::Context->preference('AllowHoldDateInFuture')
&& C4::Context->preference('OPACAllowHoldDateInFuture') )
{
$startdate = $query->param("reserve_date_$biblioNum");
}
my $patron_expiration_date = $query->param("expiration_date_$biblioNum");
my $itemtype = $query->param('itemtype') || undef;
$itemtype = undef if $itemNum;
my $biblio = Koha::Biblios->find($biblioNum);
my $rank = $biblio->holds->search( { found => [ { "!=" => "W" }, undef ] } )->count + 1;
if ( $item ) {
my $status = CanItemBeReserved( $patron, $item, $branch, { get_from_cache => 1 } )->{status};
if( $status eq 'OK' ){
$canreserve = 1;
} else {
push @failed_holds, $status;
}
}
else {
my $status = CanBookBeReserved( $borrowernumber, $biblioNum, $branch, { itemtype => $itemtype } )->{status};
if( $status eq 'OK'){
$canreserve = 1;
} else {
push @failed_holds, $status;
}
# Inserts a null into the 'itemnumber' field of 'reserves' table.
$itemNum = undef;
}
my $notes = $query->param('notes_'.$biblioNum)||'';
my $item_group_id = $query->param("item_group_id_$biblioNum") || undef;
if ( $maxreserves
&& $reserve_cnt >= $maxreserves )
{
push @failed_holds, 'tooManyReserves';
$canreserve = 0;
}
unless ( $can_place_hold_if_available_at_pickup ) {
my $items_in_this_library = Koha::Items->search({ biblionumber => $biblioNum, holdingbranch => $branch });
my $nb_of_items_issued = $items_in_this_library->search({ 'issue.itemnumber' => { not => undef }}, { join => 'issue' })->count;
my $nb_of_items_unavailable = $items_in_this_library->search({ -or => { lost => { '!=' => 0 }, damaged => { '!=' => 0 }, } });
if ( $items_in_this_library->count > $nb_of_items_issued + $nb_of_items_unavailable ) {
$canreserve = 0;
push @failed_holds, 'items_available';
}
}
# Here we actually do the reserveration. Stage 3.
if ($canreserve) {
my $reserve_id = AddReserve(
{
branchcode => $branch,
borrowernumber => $borrowernumber,
biblionumber => $biblioNum,
priority => $rank,
reservation_date => $startdate,
expiration_date => $patron_expiration_date,
notes => $notes,
title => $biblio->title,
itemnumber => $itemNum,
found => undef,
itemtype => $itemtype,
item_group_id => $item_group_id,
}
);
if( $reserve_id ){
++$reserve_cnt;
} else {
push @failed_holds, 'not_placed';
}
}
}
print $query->redirect("/cgi-bin/koha/opac-user.pl?" . ( @failed_holds ? "failed_holds=" . join('|',@failed_holds) : q|| ) . "&opac-user-holds=1");
exit;
}
#
#
# Build hashes of the requested biblio(item)s and items.
#
#
my %biblioDataHash; # Hash of biblionumber to biblio/biblioitems record.
foreach my $biblioNumber (@biblionumbers) {
my $biblioData = GetBiblioData($biblioNumber);
$biblioDataHash{$biblioNumber} = $biblioData;
my $biblio = Koha::Biblios->find( $biblioNumber );
next unless $biblio;
my $items = Koha::Items->search_ordered(
[
'me.biblionumber' => $biblioNumber,
'me.itemnumber' => {
-in => [
$biblio->host_items->get_column('itemnumber')
]
}
],
{ prefetch => [ 'issue', 'homebranch', 'holdingbranch' ] }
)->filter_by_visible_in_opac({ patron => $patron });
$biblioData->{items} = [$items->as_list]; # FIXME Potentially a lot in memory here!
# Compute the priority rank.
$biblioData->{object} = $biblio;
my $reservecount = $biblio->holds->search({ found => [ {"!=" => "W"},undef] })->count;
$biblioData->{reservecount} = $reservecount;
$biblioData->{rank} = $reservecount + 1;
}
my $requested_reserves_count = scalar( @biblionumbers );
if ( $maxreserves && ( $reserves_count + $requested_reserves_count > $maxreserves ) ) {
$template->param( new_reserves_allowed => $maxreserves - $reserves_count );
}
$template->param( select_item_types => 1 );
#
#
# Build the template parameters that will show the info
# and items for each biblionumber.
#
#
my $biblioLoop = [];
my $numBibsAvailable = 0;
my $itemdata_enumchron = 0;
my $itemdata_ccode = 0;
my $anyholdable = 0;
my $itemLevelTypes = C4::Context->preference('item-level_itypes');
my $pickup_locations = Koha::Libraries->search({ pickup_location => 1 });
$template->param('item_level_itypes' => $itemLevelTypes);
my $patron_unblessed = $patron->unblessed;
foreach my $biblioNum (@biblionumbers) {
# Init the bib item with the choices for branch pickup
my %biblioLoopIter;
# Get relevant biblio data.
my $biblioData = $biblioDataHash{$biblioNum};
if (! $biblioData) {
$template->param(message=>1, bad_biblionumber=>$biblioNum);
output_html_with_http_headers $query, $cookie, $template->output, undef, { force_no_caching => 1 };
exit;
}
my @not_available_at = ();
my $biblio = $biblioData->{object};
foreach my $library ( $pickup_locations->as_list ) {
push( @not_available_at, $library->branchcode ) unless $biblio->can_be_transferred({ to => $library });
}
my $frameworkcode = GetFrameworkCode( $biblioData->{biblionumber} );
$biblioLoopIter{biblionumber} = $biblioData->{biblionumber};
$biblioLoopIter{title} = $biblioData->{title};
$biblioLoopIter{subtitle} = $biblioData->{'subtitle'};
$biblioLoopIter{medium} = $biblioData->{medium};
$biblioLoopIter{part_number} = $biblioData->{part_number};
$biblioLoopIter{part_name} = $biblioData->{part_name};
$biblioLoopIter{author} = $biblioData->{author};
$biblioLoopIter{rank} = $biblioData->{rank};
$biblioLoopIter{reservecount} = $biblioData->{reservecount};
$biblioLoopIter{already_reserved} = $biblioData->{already_reserved};
$biblioLoopIter{object} = $biblio;
if (!$itemLevelTypes && $biblioData->{itemtype}) {
$biblioLoopIter{translated_description} = $itemtypes->{$biblioData->{itemtype}}{translated_description};
$biblioLoopIter{imageurl} = getitemtypeimagesrc() . "/". $itemtypes->{$biblioData->{itemtype}}{imageurl};
}
$biblioLoopIter{itemLoop} = [];
my $numCopiesAvailable = 0;
my $numCopiesOPACAvailable = 0;
# iterating through all items first to check if any of them available
# to pass this value further inside down to IsAvailableForItemLevelRequest to
# it's complicated logic to analyse.
# (before this loop was inside that sub loop so it was O(n^2) )
foreach my $item (@{$biblioData->{items}}) {
my $item_info = $item->unblessed;
$item_info->{holding_branch} = $item->holding_branch;
$item_info->{home_branch} = $item->home_branch;
if ($itemLevelTypes) {
my $itemtype = $item->itemtype;
$item_info->{'imageurl'} = getitemtypeimagelocation( 'opac',
$itemtypes->{ $itemtype->itemtype }->{'imageurl'} );
$item_info->{'translated_description'} =
$itemtypes->{ $itemtype->itemtype }->{translated_description};
}
# checking for holds
my $holds = $item->current_holds;
if ( my $first_hold = $holds->next ) {
$item_info->{first_hold} = $first_hold;
}
$item_info->{checkout} = $item->checkout;
# Check of the transferred documents
my $transfer = $item->get_transfer;
if ( $transfer && $transfer->in_transit ) {
$item_info->{transfertwhen} = $transfer->datesent;
$item_info->{transfertfrom} = $transfer->frombranch;
$item_info->{transfertto} = $transfer->tobranch;
$item_info->{nocancel} = 1;
}
# if the items belongs to a host record, show link to host record
if ( $item_info->{biblionumber} ne $biblioNum ) {
$item_info->{hostbiblionumber} = $item->biblionumber;
$item_info->{hosttitle} = Koha::Biblios->find( $item_info->{biblionumber} )->title;
}
my $branch = Koha::Policy::Holds->holds_control_library( $item, $patron );
my $policy_holdallowed =
CanItemBeReserved( $patron, $item, undef, { get_from_cache => 1 } )->{status} eq 'OK' &&
IsAvailableForItemLevelRequest($item, $patron, undef);
if ($policy_holdallowed) {
my $opac_hold_policy = Koha::CirculationRules->get_opacitemholds_policy( { item => $item, patron => $patron } );
if ( $opac_hold_policy ne 'N' ) { # If Y or F
$item_info->{available} = 1;
$numCopiesOPACAvailable++;
$biblioLoopIter{force_hold} = 1 if $opac_hold_policy eq 'F';
}
$numCopiesAvailable++;
unless ( $can_place_hold_if_available_at_pickup ) {
my $items_in_this_library = Koha::Items->search({ biblionumber => $item->biblionumber, holdingbranch => $item->holdingbranch, notforloan => 0 });
my $nb_of_items_issued = $items_in_this_library->search({ 'issue.itemnumber' => { not => undef }}, { join => 'issue' })->count;
if ( $items_in_this_library->count > $nb_of_items_issued ) {
push @not_available_at, $item->holdingbranch;
}
}
}
# Show serial enumeration when needed
if ($item_info->{enumchron}) {
$itemdata_enumchron = 1;
}
# Show collection when needed
if ($item_info->{ccode}) {
$itemdata_ccode = 1;
}
push @{$biblioLoopIter{itemLoop}}, $item_info;
}
$template->param(
itemdata_enumchron => $itemdata_enumchron,
itemdata_ccode => $itemdata_ccode,
);
if ($numCopiesAvailable > 0) {
$numBibsAvailable++;
$biblioLoopIter{bib_available} = 1;
$biblioLoopIter{holdable} = 1;
$biblioLoopIter{itemholdable} = 1 if $numCopiesOPACAvailable;
}
if ($biblioLoopIter{already_reserved}) {
$biblioLoopIter{holdable} = undef;
$biblioLoopIter{itemholdable} = undef;
}
if ( $biblioLoopIter{holdable} ) {
@not_available_at = uniq @not_available_at;
$biblioLoopIter{not_available_at} = \@not_available_at ;
}
unless ( $can_place_hold_if_available_at_pickup ) {
@not_available_at = uniq @not_available_at;
$biblioLoopIter{not_available_at} = \@not_available_at ;
# The record is not holdable is not available at any of the libraries
if ( Koha::Libraries->search->count == @not_available_at ) {
$biblioLoopIter{holdable} = 0;
}
}
my $status = CanBookBeReserved( $borrowernumber, $biblioNum )->{status};
$biblioLoopIter{holdable} &&= $status eq 'OK';
$biblioLoopIter{$status} = 1;
if ( $biblioLoopIter{holdable} and C4::Context->preference('AllowHoldItemTypeSelection') ) {
# build the allowed item types loop
my $rs = $biblio->items->search_ordered(
undef,
{ select => [ { distinct => 'itype' } ],
as => 'item_type'
}
);
my @item_types =
grep { CanBookBeReserved( $borrowernumber, $biblioNum, $branch, { itemtype => $_ } )->{status} eq 'OK' }
$rs->get_column('item_type');
$biblioLoopIter{allowed_item_types} = \@item_types;
}
if ( $status eq 'recall' ) {
$biblioLoopIter{recall} = 1;
}
# For multiple holds per record, if a patron has previously placed a hold,
# the patron can only place more holds of the same type. That is, if the
# patron placed a record level hold, all the holds the patron places must
# be record level. If the patron placed an item level hold, all holds
# the patron places must be item level
my $forced_hold_level = Koha::Holds->search(
{
borrowernumber => $borrowernumber,
biblionumber => $biblioNum,
found => undef,
}
)->forced_hold_level();
if ($forced_hold_level) {
$biblioLoopIter{force_hold} = 1 if $forced_hold_level eq 'item';
$biblioLoopIter{force_hold} = 0 if $forced_hold_level eq 'item_group';
$biblioLoopIter{itemholdable} = 0 if $forced_hold_level eq 'record';
$biblioLoopIter{itemholdable} = 0 if $forced_hold_level eq 'item_group';
$biblioLoopIter{forced_hold_level} = $forced_hold_level;
}
# Pass through any reserve charge
$biblioLoopIter{reserve_charge} = GetReserveFee( $patron->id, $biblioNum );
push @$biblioLoop, \%biblioLoopIter;
$anyholdable = 1 if $biblioLoopIter{holdable};
}
unless ($pickup_locations->count) {
$numBibsAvailable = 0;
$anyholdable = 0;
$template->param(
message => 1,
no_pickup_locations => 1
);
}
if ( $numBibsAvailable == 0 || $anyholdable == 0) {
$template->param( none_available => 1 );
}
if (scalar @biblionumbers > 1) {
$template->param( multi_hold => 1);
}
my $show_notes=C4::Context->preference('OpacHoldNotes');
$template->param(OpacHoldNotes=>$show_notes);
# display infos
$template->param(bibitemloop => $biblioLoop);
# can set reserve date in future
if ( C4::Context->preference('AllowHoldDateInFuture')
&& C4::Context->preference('OPACAllowHoldDateInFuture') )
{
$template->param(
reserve_in_future => 1,
);
}
output_html_with_http_headers $query, $cookie, $template->output, undef, { force_no_caching => 1 };