Bug 29002: Circulation

This patch adds handling for circulation of booked materials.

We prevent checkouts of booked items during the booking period where the
booking is for another user.

We update the due date to match the end of booking period when checking
out to the user who the booking is for.

If a checkout would overlap a booking to another user, we ask the user if
they will accept a reduced due date so the booked item is returned in
time to fulfill the booking.

Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Janet McGowan <janet.mcgowan@ptfs-europe.com>
Signed-off-by: Caroline Cyr La Rose <caroline.cyr-la-rose@inlibro.com>
Signed-off-by: Laurence Rault <laurence.rault@biblibre.com>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
This commit is contained in:
Martin Renvoize 2021-10-01 09:03:31 +01:00 committed by Tomas Cohen Arazi
parent ea4cb8ff85
commit 3b1612cf27
No known key found for this signature in database
GPG Key ID: 0A272EA1B2F3C15F
4 changed files with 173 additions and 4 deletions

View File

@ -1219,6 +1219,36 @@ sub CanBookBeIssued {
}
}
## CHECK FOR BOOKINGS
if (
my $booking = $item_object->find_booking(
{
checkout_date => $now,
due_date => $duedate,
patron_id => $patron->borrowernumber
}
)
)
{
# Booked to this patron :)
if ( $booking->patron_id == $patron->borrowernumber ) {
if ( $now < dt_from_string($booking->start_date) ) {
$needsconfirmation{'BOOKED_EARLY'} = $booking;
}
else {
$alerts{'BOOKED'} = $booking;
}
}
# Booking starts before due date, reduce loan?
elsif ( $duedate > dt_from_string($booking->start_date) ) {
$needsconfirmation{'BOOKED_TO_ANOTHER'} = $booking;
}
# Loan falls inside booking
else {
$issuingimpossible{'BOOKED_TO_ANOTHER'} = $booking;
}
}
## CHECK AGE RESTRICTION
my $agerestriction = $biblioitem->agerestriction;
my ($restriction_age, $daysToAgeRestriction) = GetAgeRestriction( $agerestriction, $patron->unblessed );

View File

@ -526,6 +526,85 @@ sub bookings {
return Koha::Bookings->_new_from_dbic( $bookings_rs );
}
=head3 find_booking
Find the first booking that would conflict with the passed checkout dates
=cut
sub find_booking {
my ( $self, $params ) = @_;
my $checkout_date = $params->{checkout_date};
my $due_date = $params->{due_date};
my $biblio = $self->biblio;
my $dtf = Koha::Database->new->schema->storage->datetime_parser;
my $bookings = $biblio->bookings(
[
# Checkout starts during booked period
start_date => {
'-between' => [
$dtf->format_datetime($checkout_date),
$dtf->format_datetime($due_date)
]
},
# Checkout is due during booked period
end_date => {
'-between' => [
$dtf->format_datetime($checkout_date),
$dtf->format_datetime($due_date)
]
},
# Checkout contains booked period
{
start_date => { '<' => $dtf->format_datetime($checkout_date) },
end_date => { '>' => $dtf->format_datetime($due_date) }
}
],
{
order_by => { '-asc' => 'start_date' }
}
);
my $checkouts = {};
my $loanable_items = {};
my $bookable_items = $biblio->bookable_items;
while ( my $item = $bookable_items->next ) {
$loanable_items->{ $item->itemnumber } = 1;
if ( my $checkout = $item->checkout ) {
$checkouts->{ $item->itemnumber } =
dt_from_string( $checkout->date_due );
}
}
while ( my $booking = $bookings->next ) {
# Booking for this item
if ( defined( $booking->item_id )
&& $booking->item_id == $self->itemnumber )
{
return $booking;
}
# Booking for another item
elsif ( defined( $booking->item_id ) ) {
# Due for another booking, remove from pool
delete $loanable_items->{ $booking->item_id };
next;
}
# Booking for any item
else {
# Can another item satisfy this booking?
}
}
return;
}
=head3 check_booking
my $bookable =
@ -613,9 +692,9 @@ sub place_booking {
{
start_date => $params->{start_date},
end_date => $params->{end_date},
borrowernumber => $patron->borrowernumber,
biblionumber => $self->biblionumber,
itemnumber => $self->itemnumber,
patron_id => $patron->borrowernumber,
biblio_id => $self->biblionumber,
item_id => $self->itemnumber,
}
)->store();
return $booking;

View File

@ -202,6 +202,10 @@ if( $onsite_checkout && !$duedatespec_allow ) {
}
}
}
my $reduced_datedue = $query->param('reduceddue');
if ( $reduced_datedue ) {
$datedue = dt_from_string( $reduced_datedue );
}
my $inprocess = (@$barcodes == 0) ? '' : $query->param('inprocess');
if ( @$barcodes == 0 && $charges eq 'yes' ) {
@ -370,9 +374,16 @@ if (@$barcodes) {
? qw( UNKNOWN_BARCODE NO_OPEN_DAYS )
: ( keys %$error );
if ( $error->{BOOKED_TO_ANOTHER} ) {
$template_params->{BOOKED_TO_ANOTHER} = $error->{BOOKED_TO_ANOTHER};
$template_params->{IMPOSSIBLE} = 1;
$blocker = 1;
}
foreach my $code ( @blocking_error_codes ) {
if ($error->{$code}) {
$template_params->{$code} = $error->{$code};
$template_params->{IMPOSSIBLE} = 1;
$blocker = 1;
}
@ -397,6 +408,10 @@ if (@$barcodes) {
$template_params->{getBarcodeMessageIteminfo} = $item->barcode;
$template_params->{NEEDSCONFIRMATION} = 1;
$confirm_required = 1;
if ( $needsconfirmation eq 'BOOKED_TO_ANOTHER' ) {
my $reduceddue = dt_from_string($$question{$needsconfirmation}->start_date)->subtract( days => 1 );
$template_params->{reduceddue} = $reduceddue;
}
}
}
unless($confirm_required) {
@ -413,7 +428,20 @@ if (@$barcodes) {
);
$recall_id = ( $recall and $recall->id ) ? $recall->id : undef;
}
my $issue = AddIssue( $patron, $barcode, $datedue, $cancelreserve, undef, undef, { onsite_checkout => $onsite_checkout, auto_renew => $session->param('auto_renew'), switch_onsite_checkout => $switch_onsite_checkout, cancel_recall => $cancel_recall, recall_id => $recall_id, } );
# If booked (alerts or confirmation) update datedue to end of booking
if ( my $booked = $question->{BOOKED_EARLY} // $alerts->{BOOKED} ) {
$datedue = $booked->end_date;
}
my $issue = AddIssue(
$patron, $barcode, $datedue,
$cancelreserve,
undef, undef,
{
onsite_checkout => $onsite_checkout, auto_renew => $session->param('auto_renew'),
switch_onsite_checkout => $switch_onsite_checkout, cancel_recall => $cancel_recall,
recall_id => $recall_id,
}
);
$template_params->{issue} = $issue;
$session->clear('auto_renew');
$inprocess = 1;

View File

@ -92,6 +92,10 @@
<div class="dialog alert">Item was checked out to [% INCLUDE 'patron-title.inc' patron = alert.RETURNED_FROM_ANOTHER.patron %] and was returned automatically.</div>
[% END %]
[% IF alert.BOOKED %]
<div class="dialog alert">Item is booked by this user<div>
[% END %]
[% IF ( nopermission ) %]
<div class="dialog alert">Staff members are not allowed to discharge borrowers, nor borrowers to request a discharge.</div>
[% END %]
@ -244,6 +248,17 @@
[% END %]
[% END %]
[% IF ( BOOKED_TO_ANOTHER ) %]
<li>
Due for another patron booking by: [% BOOKED_TO_ANOTHER.start_date | $KohaDates %]
</li>
[% END %]
[% IF ( BOOKED_EARLY ) %]
<li>
Patron has this item booked for checkout on [% BOOKED_EARLY.start_date | $KohaDates %]
</li>
[% END %]
</ul>
[% IF CAN_user_circulate_force_checkout or HIGHHOLDS %]
@ -324,6 +339,19 @@
<input type="hidden" name="duedatespec" value="[% duedatespec | html %]" />
[% END %]
[% IF ( BOOKED_TO_ANOTHER ) %]
<p>
<label for="reduceddue">Reducing due date to: <label>
<input type="text" size="20" id="reudceddue" name="reduceddue" value="[% reduceddue | $KohaDates with_hours => 1 %]" readonly="readonly">
</p>
[% END %]
[% IF ( BOOKED_EARLY ) %]
<p>
Allow early collection?
</p>
[% END %]
<input type="hidden" name="stickyduedate" value="[% stickyduedate | html %]" />
<input type="hidden" name="branch" value="[% branch | html %]" />
@ -530,6 +558,10 @@
[% IF (forceallow) %]
<li>Restriction overridden temporarily.</li>
[% END %]
[% IF ( BOOKED_TO_ANOTHER ) %]
<li>!!A booking exists!!</li>
[% END %]
</ul>
</div> <!-- /#circ_impossible -->