diff --git a/C4/Circulation.pm b/C4/Circulation.pm index e4c76fb5f8..211de86d21 100644 --- a/C4/Circulation.pm +++ b/C4/Circulation.pm @@ -864,6 +864,7 @@ sub CanBookBeIssued { my $now = dt_from_string(); + my $requested_duedate = $duedate ? 1 : 0; $duedate ||= CalcDateDue( $now, $effective_itemtype, $circ_library->branchcode, $patron ); if ( DateTime->compare( $duedate, $now ) == -1 ) { # duedate cannot be before now @@ -1327,6 +1328,21 @@ sub CanBookBeIssued { } else { $alerts{'BOOKED'} = $booking; } + + # An explicitly requested due date that runs into the next booking + # for this item, regardless of whose booking that is, needs staff + # confirmation; the booking end date will not be extended past that + # booking (see AddIssue) + if ($requested_duedate) { + my $next_booking = $item_object->find_booking( + { + checkout_date => $now, + due_date => $duedate, + exclude_booking_id => $booking->booking_id, + } + ); + $needsconfirmation{'BOOKED_DUE_DATE_CLASH'} = $next_booking if $next_booking; + } } # Booked to another patron diff --git a/Koha/Item.pm b/Koha/Item.pm index eb19468653..a5599caab6 100644 --- a/Koha/Item.pm +++ b/Koha/Item.pm @@ -629,12 +629,17 @@ sub bookings { =head3 find_booking - my $booking = $item->find_booking( { checkout_date => $now, due_date => $future_date } ); + my $booking = $item->find_booking( + { checkout_date => $now, due_date => $future_date, [ exclude_booking_id => $booking_id ] } ); Find the first booking that would conflict with the passed checkout dates for this item. If a booking lead period is configured for the itemtype we will also take that into account here, counting bookings that fall in that lead period as conflicts too. +Optionally, you may pass an exclude_booking_id to leave a specific booking out of the checks; this is +helpful when looking for bookings that would clash with an extension of the checkout that is already +satisfying that booking. + FIXME: This can be simplified, it was originally intended to iterate all biblio level bookings to catch cases where this item may be the last available to satisfy a biblio level only booking. However, we dropped the biblio level functionality prior to push as bugs were found in it's @@ -645,9 +650,10 @@ implementation. sub find_booking { my ( $self, $params ) = @_; - my $start_date = $params->{checkout_date}; - my $end_date = $params->{due_date}; - my $biblio = $self->biblio; + my $start_date = $params->{checkout_date}; + my $end_date = $params->{due_date}; + my $exclude_booking_id = $params->{exclude_booking_id}; + my $biblio = $self->biblio; my $rule = Koha::CirculationRules->get_effective_rule( { @@ -689,7 +695,8 @@ sub find_booking { } ] }, - { status => { '-not_in' => [ 'cancelled', 'completed' ] } } + { status => { '-not_in' => [ 'cancelled', 'completed' ] } }, + ( $exclude_booking_id ? { booking_id => { '!=' => $exclude_booking_id } } : () ), ] }, { order_by => { '-asc' => 'start_date' } } 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 4a74a2b403..39f1c61ec8 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt @@ -317,6 +317,13 @@ [% IF ( BOOKED_EARLY ) %]
  • Patron has this item booked for checkout on [% BOOKED_EARLY.start_date | $KohaDates %]
  • [% END %] + + [% IF ( BOOKED_DUE_DATE_CLASH ) %] +
  • + The requested due date falls within the next booking for this item (booked from [% BOOKED_DUE_DATE_CLASH.start_date | $KohaDates %]). If you continue, the booking linked to this checkout will keep its original end + date. +
  • + [% END %] [% IF CAN_user_circulate_force_checkout or HIGHHOLDS %] diff --git a/t/db_dependent/Circulation.t b/t/db_dependent/Circulation.t index 1974c2efa8..7564d18d90 100755 --- a/t/db_dependent/Circulation.t +++ b/t/db_dependent/Circulation.t @@ -5711,7 +5711,7 @@ subtest 'CanBookBeIssued | recalls' => sub { }; subtest 'CanBookBeIssued | bookings' => sub { - plan tests => 4; + plan tests => 7; my $schema = Koha::Database->schema; $schema->storage->txn_begin; @@ -5780,6 +5780,50 @@ subtest 'CanBookBeIssued | bookings' => sub { "Booked to this user, but they're collecting early" ); + # Requested due date runs into the next booking for this item + $booking->start_date( dt_from_string()->subtract( days => 1 ) )->store(); + + my $subsequent_booking = Koha::Booking->new( + { + patron_id => $patron2->borrowernumber, + pickup_library_id => $pickup_library->branchcode, + item_id => $item->itemnumber, + biblio_id => $item->biblio->biblionumber, + start_date => dt_from_string()->add( days => 8 ), + end_date => dt_from_string()->add( days => 12 ), + } + )->store(); + + ( $issuingimpossible, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued( + $patron1, $item->barcode, + dt_from_string()->add( days => 9 ), + undef, undef, undef + ); + is( + $needsconfirmation->{BOOKED_DUE_DATE_CLASH}->booking_id, + $subsequent_booking->booking_id, + "Requested due date running into the next booking needs confirmation" + ); + + ( $issuingimpossible, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued( + $patron1, $item->barcode, + dt_from_string()->add( days => 5 ), + undef, undef, undef + ); + is( + $needsconfirmation->{BOOKED_DUE_DATE_CLASH}, undef, + "No confirmation needed when the requested due date stays clear of the next booking" + ); + + ( $issuingimpossible, $needsconfirmation, $alerts, $messages ) = CanBookBeIssued( + $patron1, $item->barcode, + undef, undef, undef, undef + ); + is( + $needsconfirmation->{BOOKED_DUE_DATE_CLASH}, undef, + "No confirmation triggered by a calculated due date; only explicit overrides are checked" + ); + $schema->storage->txn_rollback; }; diff --git a/t/db_dependent/Koha/Item.t b/t/db_dependent/Koha/Item.t index 3f1dc11c97..12f9409e15 100755 --- a/t/db_dependent/Koha/Item.t +++ b/t/db_dependent/Koha/Item.t @@ -3499,7 +3499,7 @@ subtest 'bookings' => sub { }; subtest 'find_booking' => sub { - plan tests => 9; + plan tests => 11; $schema->storage->txn_begin; @@ -3606,6 +3606,31 @@ subtest 'find_booking' => sub { "Koha::Item->find_booking returns the current booking not a future one" ); + # Exclusion of a specific booking + $found_booking = $item->find_booking( + { + checkout_date => dt_from_string(), + due_date => dt_from_string()->add( days => 7 ), + exclude_booking_id => $booking2->booking_id, + } + ); + is( + $found_booking, undef, + "Koha::Item->find_booking skips the excluded booking when nothing else clashes" + ); + + $found_booking = $item->find_booking( + { + checkout_date => dt_from_string(), + due_date => dt_from_string()->add( days => 9 ), + exclude_booking_id => $booking2->booking_id, + } + ); + is( + $found_booking->booking_id, $booking3->booking_id, + "Koha::Item->find_booking returns the next clashing booking when the excluded one is skipped" + ); + # Cancelled booking $booking2->status('cancelled')->store(); $found_booking = $item->find_booking(