From 3bf68f5bb1ce6182857ae746039089cfa6e035d2 Mon Sep 17 00:00:00 2001 From: Alex Arnaud Date: Thu, 26 Nov 2015 11:00:22 +0100 Subject: [PATCH] Bug 15261: Check if checkout/reserve request overlaps existing reserves When checking out or placing hold, we should check if an existing reserve whose period overlap exists. - A user places a hold from opac whose requested period overlaps with an existing reserve period => prevent reserve - A librarian places a hold from staff whose requested period overlaps with an existing reserve period => Warn librarian (Ask for confirmation) - A librarian makes a checkout from staff whose requested period overlaps with an existing reserve period => Warn librarian (Ask for confirmation) Test plan: Enable syspref: AllowHoldDateInFuture OPACAllowHoldDateInFuture PreventChechoutOnSameReservePeriod and PreventReservesOnSamePeriod 1 (staff side): - Place a hold on title (which has only one items) level with start date and expiration date. - Place another hold (also title level) with period overlaping this reserve. - Check you are warned about an existing reserve 2 (staff side): - Place a hold on title (which has more than one items) level with start date and expiration date. - Place another hold (also title level) with period overlaping this reserve. - Check you are NOT warned about an existing reserve. Because it remains at least one item not reserved. 3 (staff side): - Place a hold on item level with start date and expiration date. - Place another hold on item level with period overlaping this reserve. - Check you are warned about an existing reserve. 4 (opac side): - Do the same than for staff side. Instead of a warn, reserve is prevented. 5: - Place a hold on title (which has only one items) level with start date and expiration date. - Try to checkout the unique item from this title with period overlaping the reserve period. - Check you are warned about an existing reserve 6: - Place a hold on title (which has more than one items) level with start date and expiration date. - Checkout an item from this title with period overlaping the reserve period. - Check you are NOT warned about an existing reserve. 7: - Place a hold on item level with start date and expiration date. - Checkout this item period overlaping the reserve period. - Check you are warned about an existing reserve 8: - Create a checkout (i.e from ${TODAY} to ${TODAY + 10 days}), - Place a hold on the same item from ${TODAY + 11 days} to ${TODAY + 30 days}), - Check that you cannot renew the checkout. Signed-off-by: Laurence Rault https://bugs.koha-community.org/show_bug.cgi?id=1526 --- C4/Circulation.pm | 68 ++++++++--- C4/Reserves.pm | 51 ++++++++- Koha/DateUtils.pm | 43 +++++++ circ/circulation.pl | 4 + ...ventchechoutonsamereserveperiod_syspref.pl | 14 +++ ...add_preventreservesonsameperiod_syspref.pl | 14 +++ installer/data/mysql/mandatory/sysprefs.sql | 2 + .../admin/preferences/circulation.pref | 12 ++ .../prog/en/modules/circ/circulation.tt | 2 + .../prog/en/modules/reserve/placerequest.tt | 88 ++++++++++++++ opac/opac-reserve.pl | 11 +- reserve/placerequest.pl | 65 ++++++++++- svc/renew | 2 +- t/db_dependent/Circulation.t | 60 ++++++++++ t/db_dependent/Circulation/CanBookBeIssued.t | 106 +++++++++++++++++ t/db_dependent/Reserves/ReserveDate.t | 108 ++++++++++++++++++ 16 files changed, 631 insertions(+), 19 deletions(-) create mode 100644 installer/data/mysql/atomicupdate/bug_15261-add_preventchechoutonsamereserveperiod_syspref.pl create mode 100644 installer/data/mysql/atomicupdate/bug_15261-add_preventreservesonsameperiod_syspref.pl create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/reserve/placerequest.tt create mode 100644 t/db_dependent/Circulation/CanBookBeIssued.t create mode 100644 t/db_dependent/Reserves/ReserveDate.t diff --git a/C4/Circulation.pm b/C4/Circulation.pm index bbddb00fb9..42b4a2234d 100644 --- a/C4/Circulation.pm +++ b/C4/Circulation.pm @@ -1150,6 +1150,27 @@ sub CanBookBeIssued { } } } + + my $now = dt_from_string(); + my $preventCheckoutOnSameReservePeriod = + C4::Context->preference("PreventCheckoutOnSameReservePeriod"); + my $reserves_on_same_period = + ReservesOnSamePeriod($item_object->biblionumber, $item_object->itemnumber, $now->ymd, $duedate->ymd); + if ($preventCheckoutOnSameReservePeriod && $reserves_on_same_period) { + my $reserve = $reserves_on_same_period->[0]; + my $patron = Koha::Patrons->find( $reserve->{borrowernumber} ); + my $branchname = Koha::Libraries->find($reserve->{branchcode})->branchname; + + $needsconfirmation{RESERVED} = 1; + $needsconfirmation{resfirstname} = $patron->firstname; + $needsconfirmation{ressurname} = $patron->surname; + $needsconfirmation{rescardnumber} = $patron->cardnumber; + $needsconfirmation{resborrowernumber} = $patron->borrowernumber; + $needsconfirmation{resbranchname} = $branchname; + $needsconfirmation{resreservedate} = $reserve->{reservedate}; + $needsconfirmation{reserve_id} = $reserve->{reserve_id}; + } + } ## CHECK AGE RESTRICTION @@ -1403,15 +1424,11 @@ AddIssue does the following things : - step 01: check that there is a borrowernumber & a barcode provided - check for RENEWAL (book issued & being issued to the same patron) - - renewal YES = Calculate Charge & renew - - renewal NO = - * BOOK ACTUALLY ISSUED ? do a return if book is actually issued (but to someone else) - * RESERVE PLACED ? - - fill reserve if reserve to this patron - - cancel reserve or not, otherwise - * TRANSFERT PENDING ? - - complete the transfert - * ISSUE THE BOOK + - renewal YES = Calculate Charge & renew renewal NO = * BOOK + - ACTUALLY ISSUED ? do a return if book is actually issued (but to + - someone else) * RESERVE PLACED ? fill reserve if reserve to this + - patron cancel reserve or not, otherwise * TRANSFERT PENDING ? + - complete the transfert * ISSUE THE BOOK =back @@ -2712,7 +2729,7 @@ already renewed the loan. $error will contain the reason the renewal can not pro =cut sub CanBookBeRenewed { - my ( $borrowernumber, $itemnumber, $override_limit, $cron ) = @_; + my ( $borrowernumber, $itemnumber, $override_limit, $cron, $date_due ) = @_; my $auto_renew = "no"; @@ -2832,10 +2849,35 @@ sub CanBookBeRenewed { } } - return ( 0, "on_reserve" ) if $resfound; # '' when no hold was found - return ( 0, $auto_renew ) if $auto_renew =~ 'too_soon';#$auto_renew ne "no" && $auto_renew ne "ok"; + unless ($date_due) { + my $itemtype = $item->effective_itemtype; + my $patron_unblessed = $patron->unblessed; + $date_due = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ? + dt_from_string( $issue->date_due, 'sql' ) : + DateTime->now( time_zone => C4::Context->tz()); + $date_due = CalcDateDue($date_due, $itemtype, _GetCircControlBranch($item->unblessed(), $patron_unblessed), $patron_unblessed, 'is a renewal'); + } + + my $now = dt_from_string(); + my $biblionumber = $item->biblionumber; + my $preventCheckoutOnSameReservePeriod = + C4::Context->preference("PreventCheckoutOnSameReservePeriod"); + my $reserves_on_same_period = + ReservesOnSamePeriod($biblionumber, $itemnumber, $now->ymd, $date_due->ymd); + if ($preventCheckoutOnSameReservePeriod && $reserves_on_same_period) { + $resfound = 1; + } + + if( $cron ) { #The cron wants to return 'too_soon' over 'on_reserve' + return ( 0, $auto_renew ) if $auto_renew =~ 'too_soon';#$auto_renew ne "no" && $auto_renew ne "ok"; + return ( 0, "on_reserve" ) if $resfound; # '' when no hold was found + } else { # For other purposes we want 'on_reserve' before 'too_soon' + return ( 0, "on_reserve" ) if $resfound; # '' when no hold was found + return ( 0, $auto_renew ) if $auto_renew =~ 'too_soon';#$auto_renew ne "no" && $auto_renew ne "ok"; + } + if ( GetSoonestRenewDate($borrowernumber, $itemnumber) > dt_from_string() ){ - return (0, "too_soon") unless $override_limit; + return (0, "too_soon") unless $override_limit; } return ( 0, "auto_renew" ) if $auto_renew eq "ok" && !$override_limit; # 0 if auto-renewal should not succeed diff --git a/C4/Reserves.pm b/C4/Reserves.pm index 6e86857db8..8dfdc6a881 100644 --- a/C4/Reserves.pm +++ b/C4/Reserves.pm @@ -36,7 +36,7 @@ use Koha::Biblios; use Koha::Calendar; use Koha::CirculationRules; use Koha::Database; -use Koha::DateUtils qw( dt_from_string output_pref ); +use Koha::DateUtils qw( dt_from_string output_pref date_ranges_overlap ); use Koha::Hold; use Koha::Holds; use Koha::ItemTypes; @@ -143,6 +143,8 @@ BEGIN { MergeHolds RevertWaitingStatus + + ReservesOnSamePeriod ); } @@ -2262,6 +2264,53 @@ sub GetMaxPatronHoldsForRecord { return $max; } +=head2 ReservesOnSamePeriod + + my $reserve = ReservesOnSamePeriod( $biblionumber, $itemnumber, $resdate, $expdate); + + Return the reserve that match the period ($resdate => $expdate), + undef if no reserve match. + +=cut + +sub ReservesOnSamePeriod { + my ($biblionumber, $itemnumber, $resdate, $expdate) = @_; + + unless ($resdate && $expdate) { + return; + } + + my @reserves = Koha::Holds->search({ biblionumber => $biblionumber }); + + $resdate = output_pref({ str => $resdate, dateonly => 1, dateformat => 'iso' }); + $expdate = output_pref({ str => $expdate, dateonly => 1, dateformat => 'iso' }); + + my @reserves_overlaps; + foreach my $reserve ( @reserves ) { + + unless ($reserve->reservedate && $reserve->expirationdate) { + next; + } + + if (date_ranges_overlap($resdate, $expdate, + $reserve->reservedate, + $reserve->expirationdate)) { + + # If reserve is item level and the requested periods overlap. + if ($itemnumber && $reserve->itemnumber == $itemnumber ) { + return [$reserve->unblessed]; + } + push @reserves_overlaps, $reserve->unblessed; + } + } + + if ( @reserves_overlaps >= Koha::Items->search({ biblionumber => $biblionumber })->count() ) { + return \@reserves_overlaps; + } + + return; +} + =head1 AUTHOR Koha Development Team diff --git a/Koha/DateUtils.pm b/Koha/DateUtils.pm index 58fec0bef9..12ec20e817 100644 --- a/Koha/DateUtils.pm +++ b/Koha/DateUtils.pm @@ -30,6 +30,7 @@ BEGIN { dt_from_string output_pref format_sqldatetime + date_ranges_overlap ); } @@ -376,4 +377,46 @@ sub format_sqldatetime { return q{}; } +=head2 date_ranges_overlap + + $bool = date_ranges_overlap($start1, $end1, $start2, $end2); + + Tells if first range ($start1 => $end1) overlaps + the second one ($start2 => $end2) + +=cut + +sub date_ranges_overlap { + my ($start1, $end1, $start2, $end2) = @_; + + $start1 = dt_from_string( $start1, 'iso' ); + $end1 = dt_from_string( $end1, 'iso' ); + $start2 = dt_from_string( $start2, 'iso' ); + $end2 = dt_from_string( $end2, 'iso' ); + + if ( + # Start of range 2 is in the range 1. + ( + DateTime->compare($start2, $start1) >= 0 + && DateTime->compare($start2, $end1) <= 0 + ) + || + # End of range 2 is in the range 1. + ( + DateTime->compare($end2, $start1) >= 0 + && DateTime->compare($end2, $end1) <= 0 + ) + || + # Range 2 start before and end after range 1. + ( + DateTime->compare($start2, $start1) < 0 + && DateTime->compare($end2, $end1) > 0 + ) + ) { + return 1; + } + + return; +} + 1; diff --git a/circ/circulation.pl b/circ/circulation.pl index cb49995163..a4d3497482 100755 --- a/circ/circulation.pl +++ b/circ/circulation.pl @@ -392,6 +392,10 @@ if (@$barcodes) { } unless($confirm_required) { my $switch_onsite_checkout = exists $messages->{ONSITE_CHECKOUT_WILL_BE_SWITCHED}; + if ( $cancelreserve eq 'cancel' ) { + CancelReserve({ reserve_id => $query->param('reserveid') }); + } + $cancelreserve = $cancelreserve eq 'revert' ? 'revert' : undef; my $issue = AddIssue( $patron->unblessed, $barcode, $datedue, $cancelreserve, undef, undef, { onsite_checkout => $onsite_checkout, auto_renew => $session->param('auto_renew'), switch_onsite_checkout => $switch_onsite_checkout, } ); $template_params->{issue} = $issue; $session->clear('auto_renew'); diff --git a/installer/data/mysql/atomicupdate/bug_15261-add_preventchechoutonsamereserveperiod_syspref.pl b/installer/data/mysql/atomicupdate/bug_15261-add_preventchechoutonsamereserveperiod_syspref.pl new file mode 100644 index 0000000000..3d9ece5494 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_15261-add_preventchechoutonsamereserveperiod_syspref.pl @@ -0,0 +1,14 @@ +use Modern::Perl; + +return { + bug_number => "15261", + description => "Add new system preference PreventCheckoutOnSameReservePeriod", + up => sub { + my ($args) = @_; + my ($dbh, $out) = @$args{qw(dbh out)}; + # Do you stuffs here + $dbh->do(q{INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES ('PreventCheckoutOnSameReservePeriod', '0', 'Prevent to checkout a document if a reserve on same period exists', NULL, 'YesNo')}); + # Print useful stuff here + say $out "Update is going well so far"; + }, +} diff --git a/installer/data/mysql/atomicupdate/bug_15261-add_preventreservesonsameperiod_syspref.pl b/installer/data/mysql/atomicupdate/bug_15261-add_preventreservesonsameperiod_syspref.pl new file mode 100644 index 0000000000..1e7771fa83 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_15261-add_preventreservesonsameperiod_syspref.pl @@ -0,0 +1,14 @@ +use Modern::Perl; + +return { + bug_number => "15261", + description => "Add new system preference PreventReservesOnSamePeriod", + up => sub { + my ($args) = @_; + my ($dbh, $out) = @$args{qw(dbh out)}; + # Do you stuffs here + $dbh->do(q{INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES ('PreventReservesOnSamePeriod', '0', 'Prevent to hold a document if a reserve on same period exists', NULL, 'YesNo')}); + # Print useful stuff here + say $out "Update is going well so far"; + }, +} diff --git a/installer/data/mysql/mandatory/sysprefs.sql b/installer/data/mysql/mandatory/sysprefs.sql index dd83a00fd8..071fa26c3a 100644 --- a/installer/data/mysql/mandatory/sysprefs.sql +++ b/installer/data/mysql/mandatory/sysprefs.sql @@ -533,6 +533,8 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('PrefillGuaranteeField', 'phone,email,streetnumber,address,city,state,zipcode,country', NULL, 'Prefill these fields in guarantee member entry form from guarantor patron record', 'Multiple'), ('PrefillItem','0','','When a new item is added, should it be prefilled with last created item values?','YesNo'), ('PreserveSerialNotes','1','','When a new "Expected" issue is generated, should it be prefilled with last created issue notes?','YesNo'), +('PreventCheckoutOnSameReservePeriod','0','','Prevent to checkout a document if a reserve on same period exists','YesNo'), +('PreventReservesOnSamePeriod','0','','Prevent to hold a document if a reserve on same period exists','YesNo'), ('previousIssuesDefaultSortOrder','asc','asc|desc','Specify the sort order of Previous Issues on the circulation page','Choice'), ('PrintNoticesMaxLines','0','','If greater than 0, sets the maximum number of lines an overdue notice will print. If the number of items is greater than this number, the notice will end with a warning asking the borrower to check their online account for a full list of overdue items.','Integer'), ('PrivacyPolicyURL','',NULL,'This URL is used in messages about GDPR consents.', 'Free'), diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref index ecc49a3a90..b18cbadcfa 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref @@ -564,6 +564,12 @@ Circulation: class: integer - to fill holds. - "(list of not for loan values separated with a pipe '|')" + - + - pref: PreventCheckoutOnSameReservePeriod + choices: + yes: Do + no: "Don't" + - If yes, checkouts periods can't overlap with a reserve period. - - pref: HoldsAutoFill choices: @@ -915,6 +921,12 @@ Circulation: months: months years: years - from reserve date. + - + - pref: PreventReservesOnSamePeriod + choices: + yes: Do + no: "Don't" + - If yes, Reserves periods for the same document can't overlap. Interlibrary loans: - - pref: ILLModule diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt index 7e62d7ae3f..b634927a55 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt @@ -251,6 +251,7 @@ [% IF ( RESERVED ) %]

+

[% END %] @@ -259,6 +260,7 @@


+

diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/placerequest.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/placerequest.tt new file mode 100644 index 0000000000..8342d99e15 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/placerequest.tt @@ -0,0 +1,88 @@ +[% USE JSON.Escape %] +[% USE raw %] +[% INCLUDE 'doc-head-open.inc' %] + Koha › Circulation › Holds › Confirm holds +[% INCLUDE 'doc-head-close.inc' %] + + +[% INCLUDE 'header.inc' %] +[% INCLUDE 'circ-search.inc' %] + + + +
+ +
+
+
+ +
+
+ +
+ +[% INCLUDE 'intranet-bottom.inc' %] diff --git a/opac/opac-reserve.pl b/opac/opac-reserve.pl index a339200561..93b66b57d0 100755 --- a/opac/opac-reserve.pl +++ b/opac/opac-reserve.pl @@ -24,7 +24,7 @@ use CGI qw ( -utf8 ); use C4::Auth qw( get_template_and_user ); use C4::Koha qw( getitemtypeimagelocation getitemtypeimagesrc ); use C4::Circulation qw( GetBranchItemRule GetTransfers ); -use C4::Reserves qw( CanItemBeReserved CanBookBeReserved AddReserve GetReservesControlBranch ItemsAnyAvailableAndNotRestricted IsAvailableForItemLevelRequest ); +use C4::Reserves qw( CanItemBeReserved CanBookBeReserved AddReserve GetReservesControlBranch ItemsAnyAvailableAndNotRestricted IsAvailableForItemLevelRequest ReservesOnSamePeriod ); use C4::Biblio qw( GetBiblioData GetFrameworkCode GetMarcBiblio ); use C4::Items qw( GetHostItemsInfo GetItemsInfo ); use C4::Output qw( output_html_with_http_headers ); @@ -285,6 +285,15 @@ if ( $query->param('place_reserve') ) { # Inserts a null into the 'itemnumber' field of 'reserves' table. $itemNum = undef; } + + if ($canreserve) { + if (C4::Context->preference("PreventReservesOnSamePeriod") && + ReservesOnSamePeriod($biblioNum, $itemNum, $startdate, $expiration_date)) { + $canreserve = 0; + $failed_holds++; + } + } + my $notes = $query->param('notes_'.$biblioNum)||''; if ( $maxreserves diff --git a/reserve/placerequest.pl b/reserve/placerequest.pl index 0a8d6dd64e..8d9f434831 100755 --- a/reserve/placerequest.pl +++ b/reserve/placerequest.pl @@ -23,20 +23,31 @@ use Modern::Perl; +use List::MoreUtils qw( none ); + use CGI qw ( -utf8 ); use URI; -use C4::Reserves qw( CanItemBeReserved AddReserve CanBookBeReserved ); -use C4::Auth qw( checkauth ); +use C4::Reserves qw( CanItemBeReserved AddReserve CanBookBeReserved ReservesOnSamePeriod); +use C4::Auth qw( checkauth get_template_and_user); +use C4::Output qw( output_html_with_http_headers ); use Koha::Items; use Koha::Patrons; my $input = CGI->new(); -checkauth($input, 0, { reserveforothers => 'place_holds' }, 'intranet'); +my ($template, $loggedinuser, $cookie) = get_template_and_user({ + template_name => 'reserve/placerequest.tt', + query => $input, + type => 'intranet', + authnotrequired => 0, + flagsrequired => { reserveforothers => 'place_holds' }, +}); + my @reqbib = $input->multi_param('reqbib'); my @biblionumbers = $input->multi_param('biblionumber'); +my @bibitems = $input->multi_param('biblioitem'); my $borrowernumber = $input->param('borrowernumber'); my $notes = $input->param('notes'); my $branch = $input->param('pickup'); @@ -48,6 +59,8 @@ my $checkitem = $input->param('checkitem'); my $expirationdate = $input->param('expiration_date'); my $itemtype = $input->param('itemtype') || undef; my $non_priority = $input->param('non_priority'); +my $confirm = $input->param('confirm'); +my @confirm_biblionumbers = $input->multi_param('confirm_biblionumbers'); my $patron = Koha::Patrons->find( $borrowernumber ); @@ -68,7 +81,53 @@ my $found; if ( $type eq 'str8' && $patron ) { + if (C4::Context->preference('PreventReservesOnSamePeriod') && !$confirm) { + my %overlap_reserves; + foreach my $biblionumber ( keys %bibinfos ) { + my $overlapping_reserves = ReservesOnSamePeriod($biblionumber, $checkitem, $startdate, $expirationdate); + if ($overlapping_reserves && 0 < scalar @$overlapping_reserves) { + $overlap_reserves{$biblionumber} = { + title => $multi_hold ? $bibinfos{$biblionumber}->{title} : $title, + checkitem => $checkitem, + rank => $multi_hold ? $bibinfos{$biblionumber}->{rank} : $rank[0], + }; + } + } + + if (scalar keys %overlap_reserves) { + $template->param( + borrowernumber => $borrowernumber, + biblionumbers => $biblionumbers, + biblionumber => $biblionumber, + overlap_reserves => \%overlap_reserves, + reserve_date => $startdate, + expiration_date => $expirationdate, + notes => $notes, + rank_request => \@rank, + pickup => $branch, + multi_hold => $multi_hold, + ); + + output_html_with_http_headers $input, $cookie, $template->output; + exit; + } + } + foreach my $biblionumber ( keys %bibinfos ) { + next if ($confirm && none { $_ eq $biblionumber } @confirm_biblionumbers); + + my $count = @bibitems; + @bibitems = sort @bibitems; + my $i2 = 1; + my @realbi; + $realbi[0] = $bibitems[0]; + for ( my $i = 1 ; $i < $count ; $i++ ) { + my $i3 = $i2 - 1; + if ( $realbi[$i3] ne $bibitems[$i] ) { + $realbi[$i2] = $bibitems[$i]; + $i2++; + } + } my $can_override = C4::Context->preference('AllowHoldPolicyOverride'); if ( defined $checkitem && $checkitem ne '' ) { diff --git a/svc/renew b/svc/renew index 91f7958031..d4f1c910dc 100755 --- a/svc/renew +++ b/svc/renew @@ -59,7 +59,7 @@ $data->{borrowernumber} = $borrowernumber; $data->{branchcode} = $branchcode; ( $data->{renew_okay}, $data->{error} ) = - CanBookBeRenewed( $borrowernumber, $itemnumber, $override_limit ); + CanBookBeRenewed( $borrowernumber, $itemnumber, $override_limit, $date_due ); # If we're allowing reserved items to be renewed... if ( $data->{error} && $data->{error} eq 'on_reserve' && C4::Context->preference('AllowRenewalOnHoldOverride')) { diff --git a/t/db_dependent/Circulation.t b/t/db_dependent/Circulation.t index 249e8aee1c..113140a5e1 100755 --- a/t/db_dependent/Circulation.t +++ b/t/db_dependent/Circulation.t @@ -524,6 +524,7 @@ subtest "CanBookBeRenewed tests" => sub { my $borrowing_borrowernumber = Koha::Checkouts->find( { itemnumber => $item_1->itemnumber } )->borrowernumber; is ($borrowing_borrowernumber, $renewing_borrowernumber, "Item checked out to $renewing_borrower->{firstname} $renewing_borrower->{surname}"); + t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary'); my ( $renewokay, $error ) = CanBookBeRenewed($renewing_borrowernumber, $item_1->itemnumber, 1); is( $renewokay, 1, 'Can renew, no holds for this title or item'); @@ -1719,6 +1720,65 @@ subtest "AllowRenewalIfOtherItemsAvailable tests" => sub { is( $error, 'onsite_checkout', 'A correct error code should be returned by CanBookBeRenewed for on-site checkout' ); } +{ + # Don't allow renewing on reserve period with PreventCheckoutOnSameReservePeriod enabled + t::lib::Mocks::mock_preference( 'PreventCheckoutOnSameReservePeriod', 1 ); + + my $start_issue_dt = DateTime->now(); + $start_issue_dt->subtract( days => 15); + my $due_date = $start_issue_dt->clone; + $due_date->add( days => 17 ); + + my $biblio = $builder->build({ + source => 'Biblio', + }); + my $biblionumber = $biblio->{biblionumber}; + + my $item = $builder->build({ + source => 'Item', + value => { + biblionumber => $biblionumber + } + }); + my $itemnumber = $item->{itemnumber}; + + my $issue = $builder->build({ + source => 'Issue', + value => { + itemnumber => $itemnumber, + biblionumber => $biblionumber, + issuedate => $start_issue_dt->ymd, + date_due => $due_date->ymd, + onsite_checkout => 0 + } + }); + + my $reservedate = $due_date->clone; + $reservedate->add( days => 5 ); + my $expirationdate = $reservedate->clone; + $expirationdate->add( days => 15 ); + $builder->build({ + source => 'Reserve', + value => { + biblionumber => $biblionumber, + itemnumber => $itemnumber, + reservedate => $reservedate->ymd, + expirationdate => $expirationdate->ymd + } + }); + + my $requested_date_due = $due_date->clone; + $requested_date_due->add( days => 4 ); + my ( $renewed, $error ) = CanBookBeRenewed( $issue->{borrowernumber}, $itemnumber, 1, $requested_date_due ); + is( $renewed, 1, 'CanBookBeRenewed should allow to renew if no reserve date overlap' ); + + $requested_date_due->add( days => 2 ); + ( $renewed, $error ) = CanBookBeRenewed( $issue->{borrowernumber}, $itemnumber, 1, $requested_date_due ); + is( $renewed, 0, 'CanBookBeRenewed should not allow to renew if reserve date overlap' ); + + t::lib::Mocks::mock_preference( 'PreventCheckoutOnSameReservePeriod', 0 ); +} + { my $library = $builder->build({ source => 'Branch' }); diff --git a/t/db_dependent/Circulation/CanBookBeIssued.t b/t/db_dependent/Circulation/CanBookBeIssued.t new file mode 100644 index 0000000000..fad19eea13 --- /dev/null +++ b/t/db_dependent/Circulation/CanBookBeIssued.t @@ -0,0 +1,106 @@ +#!/usr/bin/env perl + +# 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 . + +use Modern::Perl; + +use Test::More tests => 1; +use C4::Members; +use C4::Reserves; +use C4::Circulation; +use Koha::DateUtils; + +use t::lib::TestBuilder; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $builder = t::lib::TestBuilder->new(); + +subtest 'Tests for CanBookBeIssued with overlap reserves' => sub { + plan tests => 6; + + my $categorycode = $builder->build({ source => 'Category' })->{ categorycode }; + my $branch = $builder->build({ source => 'Branch' }); + my $branchcode = $branch->{branchcode}; + + my $borrower = $builder->build_object({ + class => 'Koha::Patrons', + value => { + branchcode => $branchcode, + categorycode => $categorycode, + } + }); + my $borrowernumber = $borrower->borrowernumber; + + my $biblio = $builder->build({source => 'Biblio'}); + my $biblioitem = $builder->build({ + source => 'Biblioitem', + value => { + biblionumber => $biblio->{biblionumber}, + }, + }); + my $item = $builder->build({ + source => 'Item', + value => { + biblionumber => $biblio->{biblionumber}, + biblioitemnumber => $biblioitem->{biblioitemnumber}, + withdrawn => 0, + itemlost => 0, + notforloan => 0, + }, + }); + + + my $startdate = dt_from_string(); + $startdate->add_duration(DateTime::Duration->new(days => 4)); + my $expdate = $startdate->clone(); + $expdate->add_duration(DateTime::Duration->new(days => 10)); + + my $reserveid = AddReserve($branchcode, $borrowernumber, + $item->{biblionumber}, undef, 1, $startdate->ymd(), $expdate->ymd, + undef, undef, undef, undef); + + my $non_overlap_duedate = dt_from_string(); + $non_overlap_duedate->add_duration(DateTime::Duration->new(days => 2)); + my ($error, $question, $alerts ) = + CanBookBeIssued($borrower, $item->{barcode}, $non_overlap_duedate, 1, 0); + + is_deeply($error, {}, ""); + is_deeply($question, {}, ""); + is_deeply($alerts, {}, ""); + + my $overlap_duedate = dt_from_string(); + $overlap_duedate->add_duration(DateTime::Duration->new(days => 5)); + ($error, $question, $alerts ) = + CanBookBeIssued($borrower, $item->{barcode}, $overlap_duedate, 1, 0); + + is_deeply($error, {}, ""); + my $expected = { + RESERVED => 1, + resfirstname => $borrower->firstname, + ressurname => $borrower->surname, + rescardnumber => $borrower->cardnumber, + resborrowernumber => $borrower->borrowernumber, + resbranchname => $branch->{branchname}, + resreservedate => $startdate->ymd, + resreserveid => $reserveid, + }; + is_deeply($question, $expected, ""); + is_deeply($alerts, {}, ""); +}; + +$schema->storage->txn_rollback; diff --git a/t/db_dependent/Reserves/ReserveDate.t b/t/db_dependent/Reserves/ReserveDate.t new file mode 100644 index 0000000000..4d7f8afc4a --- /dev/null +++ b/t/db_dependent/Reserves/ReserveDate.t @@ -0,0 +1,108 @@ +#!/usr/bin/perl + +# 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 . + +use Modern::Perl; + +use Test::More tests => 6; +use Test::Warn; + +use MARC::Record; +use DateTime::Duration; + +use C4::Biblio; +use C4::Items; +use C4::Members; +use C4::Circulation; +use Koha::Holds; +use t::lib::TestBuilder; + +use Koha::DateUtils; + + +use_ok('C4::Reserves'); + +my $dbh = C4::Context->dbh; + +# Start transaction +$dbh->{AutoCommit} = 0; +$dbh->{RaiseError} = 1; + +my $builder = t::lib::TestBuilder->new(); +my $categorycode = $builder->build({ source => 'Category' })->{ categorycode }; +my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode }; + +my $borrower = $builder->build({ + source => 'Borrower', + value => { + branchcode => $branchcode, + categorycode => $categorycode, + } +}); + +my $borrower2 = $builder->build({ + source => 'Borrower', + value => { + branchcode => $branchcode, + categorycode => $categorycode, + } +}); + +my $borrowernumber = $borrower->{borrowernumber}; +my $borrowernumber2 = $borrower2->{borrowernumber}; + +# Create a helper biblio +my $biblio = MARC::Record->new(); +my $title = 'Alone in the Dark'; +my $author = 'Karen Rose'; +if( C4::Context->preference('marcflavour') eq 'UNIMARC' ) { + $biblio->append_fields( + MARC::Field->new('600', '', '1', a => $author), + MARC::Field->new('200', '', '', a => $title), + ); +} +else { + $biblio->append_fields( + MARC::Field->new('100', '', '', a => $author), + MARC::Field->new('245', '', '', a => $title), + ); +} +my ($bibnum, $bibitemnum); +($bibnum, $title, $bibitemnum) = AddBiblio($biblio, ''); + +my ($item_bibnum, $item_bibitemnum, $itemnumber) = AddItem({ homebranch => $branchcode, holdingbranch => $branchcode, barcode => '333' } , $bibnum); + +C4::Context->set_preference('AllowHoldDateInFuture', 1); + +AddReserve($branchcode, $borrowernumber, $bibnum, + $bibitemnum, 1, '2015-11-01', '2015-11-20', undef, + undef, undef, undef); + +is(ReservesOnSamePeriod($bibnum, undef, '2015-11-25', '2015-11-30'), undef, "Period doesn't overlaps"); + +ok(ReservesOnSamePeriod($bibnum, undef, '2015-11-02', '2015-11-10'), "Period overlaps"); + +my ($item_bibnum2, $item_bibitemnum2, $itemnumber2) = AddItem({ homebranch => $branchcode, holdingbranch => $branchcode, barcode => '444' } , $bibnum); +is(ReservesOnSamePeriod($bibnum, undef, '2015-11-02', '2015-11-10'), undef, "Period overlaps but there is 2 items"); + +AddReserve($branchcode, $borrowernumber2, $bibnum, + $bibitemnum, 1, '2016-02-01', '2016-02-10', undef, + undef, $itemnumber, undef); +is(ReservesOnSamePeriod($bibnum, $itemnumber, '02/12/2015', '10/12/2015'), undef, "Period on item does not overlap (with metric date format)"); + +my $reserve = ReservesOnSamePeriod($bibnum, $itemnumber, '2016-01-31', '2016-02-05'); +is($reserve->[0]->{itemnumber}, $itemnumber, 'Period on item overlaps'); +$dbh->rollback; -- 2.30.2