Bug 42997: Sync booking end_date with the due date at renewal

When a checkout satisfying a booking is renewed, the booking previously
kept its original end_date, blocking the booked period from other
patrons for the wrong window and leaving the two records out of sync.

This patch updates C4::Circulation::AddRenewal to pull the linked
booking's end_date along with the renewed due date, mirroring the
behaviour introduced for AddIssue earlier in this patchset. As at
issue time, if the new due date would clash with a subsequent booking
(possible when the renewal was explicitly allowed past a clash), the
booking keeps its original end_date.

Together with the previous patch this makes a renewal the natural way
to extend an issued booking: CanBookBeRenewed refuses renewals that
would run into the next booking, and AddRenewal keeps the booking in
step with the loan.

Test plan:
1) Run t/db_dependent/Circulation.t and confirm the new
   'AddRenewal | booking end_date sync' subtest passes
2) Check out a booked item, renew it from the checkouts screen and
   confirm the booking end date follows the new due date

Sponsored-by: OpenFifth <https://openfifth.co.uk/\>
Sponsored-by: Westminster City Council <https://www.westminster.gov.uk/\>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
This commit is contained in:
Martin Renvoize 2026-07-06 10:16:45 +01:00 committed by Pedro Amorim
parent 2da3248b41
commit 50115d8322
No known key found for this signature in database
GPG Key ID: 06B75BA7F3894D55
2 changed files with 125 additions and 1 deletions

View File

@ -3743,6 +3743,29 @@ sub AddRenewal {
error => 'Update of issue# ' . $issue->issue_id . ' failed with error: ' . $sth->errstr );
}
# Keep any linked booking's end_date in sync with the renewed due date
if ( $issue->booking_id ) {
my $booking = Koha::Bookings->find( $issue->booking_id );
if ( $booking && $booking->status eq 'issued' ) {
my $booking_end = dt_from_string( $booking->end_date );
if ( $datedue->compare($booking_end) != 0 ) {
try {
$booking->end_date($datedue)->store;
} catch {
my $error = $_;
if ( blessed $error && $error->isa('Koha::Exceptions::Booking::Clash') ) {
# The renewal was explicitly allowed past a clash;
# keep the booking's original end_date
$booking->discard_changes;
} else {
die $error;
}
};
}
}
}
# Update the renewal count on the item, and tell zebra to reindex
$renews = ( $item_object->renewals || 0 ) + 1;
$item_object->renewals($renews);

View File

@ -19,7 +19,7 @@ use Modern::Perl;
use utf8;
use Test::NoWarnings;
use Test::More tests => 88;
use Test::More tests => 89;
use Test::Exception;
use Test::MockModule;
use Test::Deep qw( cmp_deeply );
@ -7840,6 +7840,107 @@ subtest 'AddRenewal | booking_id preservation' => sub {
ok( defined $issue->booking_id, "booking_id field is not null after renewal" );
};
subtest 'AddRenewal | booking end_date sync' => sub {
plan tests => 4;
my $schema = Koha::Database->schema;
$schema->storage->txn_begin;
my $library = $builder->build_object( { class => 'Koha::Libraries' } );
my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
my $patron2 = $builder->build_object( { class => 'Koha::Patrons' } );
my $item = $builder->build_sample_item( { library => $library->branchcode, bookable => 1 } );
Koha::CirculationRules->set_rules(
{
branchcode => $library->branchcode,
categorycode => $patron->categorycode,
itemtype => $item->effective_itemtype,
rules => {
renewalsallowed => 10,
renewalperiod => 7,
issuelength => 7,
}
}
);
my $start_date = dt_from_string()->truncate( to => 'minute' );
my $end_date = $start_date->clone->add( days => 7 );
my $booking = $builder->build_object(
{
class => 'Koha::Bookings',
value => {
patron_id => $patron->borrowernumber,
item_id => $item->itemnumber,
biblio_id => $item->biblio->biblionumber,
pickup_library_id => $library->branchcode,
start_date => $start_date,
end_date => $end_date,
status => 'issued',
}
}
);
my $issue = AddIssue( $patron, $item->barcode, $end_date->clone );
$issue->booking_id( $booking->booking_id )->store;
# Renewal to a specific due date pulls the booking end_date along
my $new_due_date = $end_date->clone->add( days => 3 );
AddRenewal(
{
borrowernumber => $patron->borrowernumber,
itemnumber => $item->itemnumber,
branch => $library->branchcode,
datedue => $new_due_date->clone,
}
);
$booking->discard_changes;
is(
dt_from_string( $booking->end_date )->compare($new_due_date), 0,
"Booking end_date synced to the renewal due date"
);
is( $booking->status, 'issued', "Booking remains issued after renewal" );
# A renewal that would clash with the next booking keeps the original end_date
my $next_booking = $builder->build_object(
{
class => 'Koha::Bookings',
value => {
patron_id => $patron2->borrowernumber,
item_id => $item->itemnumber,
biblio_id => $item->biblio->biblionumber,
pickup_library_id => $library->branchcode,
start_date => $new_due_date->clone->add( days => 5 ),
end_date => $new_due_date->clone->add( days => 9 ),
status => 'new',
}
}
);
my $clashing_due_date = $new_due_date->clone->add( days => 6 );
AddRenewal(
{
borrowernumber => $patron->borrowernumber,
itemnumber => $item->itemnumber,
branch => $library->branchcode,
datedue => $clashing_due_date->clone,
}
);
my $renewed_issue = Koha::Checkouts->find( $issue->issue_id );
$booking->discard_changes;
is(
dt_from_string( $renewed_issue->date_due )->compare($clashing_due_date), 0,
"Checkout renewed to the requested due date"
);
is(
dt_from_string( $booking->end_date )->compare($new_due_date), 0,
"Booking end_date kept when the renewal clashes with the next booking"
);
$schema->storage->txn_rollback;
};
subtest "GetSoonestRenewDate tests" => sub {
plan tests => 6;
Koha::CirculationRules->set_rule(