From a8ea5cdbe007fa983c34d7c0b615d83469ce6585 Mon Sep 17 00:00:00 2001 From: Thibaud Guillot Date: Thu, 17 Oct 2024 11:53:03 +0000 Subject: [PATCH] Bug 36271: Add circulation rules for booking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Welcome to 3 new circulation rules :) - Bookings allowed (total) / bookings_allowed_total - Bookings per item (total) / bookings_per_item - Bookings period length (day) / bookings_period_length Test plan: 1) Apply this patch 2) Set values for these new rules 3) Define a total to 2, 1 per item with 2 days for example for booking period. 4) Book an item and retry on the same item, normally an error message is showing in the pop up. 6) Book another item, and again another one, on the third an error message is showing (always in pop up) 7) You can also try to test with a longer period than bookings period length rule, an another error will be displayed. Sponsored by: Association de Gestion des Ĺ’uvres Sociales d'Inria (AGOS) Signed-off-by: esther --- Koha/Booking.pm | 99 +++++++++++++++++++ Koha/CirculationRules.pm | 9 ++ Koha/Exceptions/Booking.pm | 4 + Koha/REST/V1/Bookings.pm | 18 ++++ admin/smart-rules.pl | 9 ++ .../prog/en/modules/admin/smart-rules.tt | 35 ++++++- .../prog/js/modals/place_booking.js | 4 +- 7 files changed, 176 insertions(+), 2 deletions(-) diff --git a/Koha/Booking.pm b/Koha/Booking.pm index e5297d8d17..11e29344ff 100644 --- a/Koha/Booking.pm +++ b/Koha/Booking.pm @@ -24,10 +24,16 @@ use Koha::DateUtils qw( dt_from_string ); use Koha::Items; use Koha::Patrons; use Koha::Libraries; +use Koha::Bookings; +use Koha::CirculationRules; +use Koha::Cache::Memory::Lite; use C4::Letters; +use C4::Circulation; +use C4::Biblio; use base qw(Koha::Object); +use List::Util qw(min); =head1 NAME @@ -37,6 +43,96 @@ Koha::Booking - Koha Booking object class =head2 Class methods +=head2 can_be_booked_in_advance + + $canBeBooked = &can_be_booked_in_advance($patron, $item, $branchcode) + if ($canBeBooked->{status} eq 'OK') { #We can booked this Item in advance! } + +@RETURNS { status => OK }, if the Item can be booked. + { status => tooManyBookings, limit => $limit, rule => $rule }, if the borrower has exceeded their maximum booking amount. + { status => tooLongBookingPeriod, limit => $limit }, if the borrower has exceeded their maximum booking period. + { status => bookingPeriodNotValid }, if booking period is not valid (undef or equal to 0). +=cut + +sub can_be_booked_in_advance { + my ( $self, $params ) = @_; + my $patron = $self->patron; + my $item = $self->item; + + my $dbh = C4::Context->dbh; + + my $borrower = $patron->unblessed; + + if ( C4::Biblio->GetMarcFromKohaField('biblioitems.agerestriction') ) { + my $biblio = $item->biblio; + + # Check for the age restriction + my ( $ageRestriction, $daysToAgeRestriction ) = + C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction, $borrower ); + return { status => 'ageRestricted' } if $daysToAgeRestriction && $daysToAgeRestriction > 0; + } + + # By default for now, control branch is the item homebranch + my $bookings_control_branch = $item->homebranch; + + # we retrieve rights + my $rights = Koha::CirculationRules->get_effective_rules( + { + categorycode => $borrower->{'categorycode'}, + itemtype => $item->effective_itemtype, + branchcode => $bookings_control_branch, + rules => + [ 'bookings_allowed_total', 'bookings_per_item', 'bookings_period_length' ] + } + ); + + my $bookings_allowed_total = $rights->{bookings_allowed_total} || undef; + my $bookings_per_item = $rights->{bookings_per_item} || undef; + my $bookings_period_length = $rights->{bookings_period_length} || 0; + + return { status => 'noBookingsAllowed' } if defined($bookings_allowed_total) && $bookings_allowed_total == 0; + + my $bookings_per_item_count = Koha::Bookings->search( { patron_id => $patron->borrowernumber, item_id => $item->itemnumber } )->count(); + return { status => 'tooManyBookings', limit => $bookings_per_item, rule => 'bookings_per_item' } if defined($bookings_per_item) && $bookings_per_item <= $bookings_per_item_count; + + + my $querycount; + if (C4::Context->preference('item-level_itypes')) { + $querycount = q{ + SELECT count(*) AS count + FROM bookings AS b + LEFT JOIN items AS i ON (b.item_id=i.itemnumber) + WHERE b.patron_id = ? + AND i.itype = ? + }; + } else { + $querycount = q{ + SELECT count(*) AS count + FROM bookings AS b + LEFT JOIN biblioitems AS bi ON (b.biblio_id=bi.biblionumber) + WHERE b.patron_id = ? + AND bi.itemtype = ? + }; + } + + my $sthcount = $dbh->prepare($querycount); + $sthcount->execute( $patron->borrowernumber, $item->effective_itemtype ); + my $total_bookings_count = $sthcount->fetchrow_hashref()->{count}; + + return { status => 'tooManyBookings', limit => $bookings_allowed_total, rule => 'bookings_allowed_total' } if defined($bookings_allowed_total) && $bookings_allowed_total <= $total_bookings_count; + + my $start_date = dt_from_string( $self->start_date ); + my $end_date = dt_from_string( $self->end_date ); + my $duration = $end_date->delta_days($start_date); + + my $delta_days = $duration->in_units('days'); + + return { status => 'bookingPeriodNotValid'} if $bookings_period_length == 0; + return { status => 'tooLongBookingPeriod', limit => $bookings_period_length } if $delta_days > $bookings_period_length; + + return { status => 'OK' }; +} + =head3 biblio Returns the related Koha::Biblio object for this booking @@ -144,6 +240,9 @@ sub store { # FIXME: We should be able to combine the above two functions into one + my $canBeBooked = can_be_booked_in_advance( $self ); + Koha::Exceptions::Booking::Rule->throw( $canBeBooked ) if $canBeBooked->{'status'} ne "OK"; + # Assign item at booking time if ( !$self->item_id ) { $self->_assign_item_for_booking; diff --git a/Koha/CirculationRules.pm b/Koha/CirculationRules.pm index d558c1e781..1a27b21260 100644 --- a/Koha/CirculationRules.pm +++ b/Koha/CirculationRules.pm @@ -222,6 +222,15 @@ our $RULE_KINDS = { bookings_trail_period => { scope => [ 'branchcode', 'itemtype' ], }, + bookings_allowed_total => { + scope => [ 'branchcode', 'categorycode', 'itemtype' ], + }, + bookings_per_item => { + scope => [ 'branchcode', 'categorycode', 'itemtype' ], + }, + bookings_period_length => { + scope => [ 'branchcode', 'categorycode', 'itemtype' ], + }, # Not included (deprecated?): # * accountsent # * reservecharge diff --git a/Koha/Exceptions/Booking.pm b/Koha/Exceptions/Booking.pm index 79e5b6ad8c..bad8f4037e 100644 --- a/Koha/Exceptions/Booking.pm +++ b/Koha/Exceptions/Booking.pm @@ -8,6 +8,10 @@ use Exception::Class ( isa => 'Koha::Exceptions::Booking', description => "Adding or updating the booking would result in a clash" }, + 'Koha::Exceptions::Booking::Rule' => { + isa => 'Koha::Exceptions::Booking', + description => "Booking rejected by circulation rules" + } ); 1; diff --git a/Koha/REST/V1/Bookings.pm b/Koha/REST/V1/Bookings.pm index 945614cd90..8682e36ac3 100644 --- a/Koha/REST/V1/Bookings.pm +++ b/Koha/REST/V1/Bookings.pm @@ -96,6 +96,24 @@ sub add { error => "Duplicate booking_id", } ); + } elsif ( blessed $_ and $_->isa('Koha::Exceptions::Booking::Rule') ) { + my $error_code = $_->{'message'}->{'status'}; + my $limit = $_->{'message'}->{'limit'} // ''; + my $rule = $_->{'message'}->{'rule'} // ''; + my %error_strings = ( + 'noBookingsAllowed' => 'Bookings are not allowed according to circulation rules', + 'tooManyBookings' => sprintf('Patron has reached the maximum of booking according to circulation rules : %s maximum (%s)', $limit, $rule), + 'tooLongBookingPeriod' => sprintf('Booking period exceed booking period limit according to circulation rules : %s day(s)', $limit), + 'bookingPeriodNotValid' => sprintf('Booking period must be valid'), + 'ageRestricted' => "Age restricted", + ); + + return $c->render( + status => 403, + openapi => { + error => $error_strings{$error_code}, + } + ); } return $c->unhandled_exception($_); diff --git a/admin/smart-rules.pl b/admin/smart-rules.pl index 6a0895448f..964fccf9fb 100755 --- a/admin/smart-rules.pl +++ b/admin/smart-rules.pl @@ -117,6 +117,9 @@ if ($op eq 'cud-delete') { recall_shelf_time => undef, decreaseloanholds => undef, holds_pickup_period => undef, + bookings_allowed_total => undef, + bookings_per_item => undef, + bookings_period_length => undef, } } ); @@ -314,6 +317,9 @@ elsif ( $op eq 'cud-add' ) { my $recall_overdue_fine = $input->param('recall_overdue_fine'); my $recall_shelf_time = $input->param('recall_shelf_time'); my $holds_pickup_period = strip_non_numeric( scalar $input->param('holds_pickup_period') ); + my $bookings_allowed_total = strip_non_numeric( scalar $input->param('bookings_allowed_total') ); + my $bookings_per_item = strip_non_numeric( scalar $input->param('bookings_per_item') ); + my $bookings_period_length = $input->param('bookings_period_length') || 0; my $rules = { maxissueqty => $maxissueqty, @@ -356,6 +362,9 @@ elsif ( $op eq 'cud-add' ) { recall_overdue_fine => $recall_overdue_fine, recall_shelf_time => $recall_shelf_time, holds_pickup_period => $holds_pickup_period, + bookings_allowed_total => $bookings_allowed_total, + bookings_per_item => $bookings_per_item, + bookings_period_length => $bookings_period_length, }; Koha::CirculationRules->set_rules( diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smart-rules.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smart-rules.tt index 0b143b0cea..ec961564ff 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smart-rules.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smart-rules.tt @@ -143,6 +143,9 @@ On shelf holds allowed OPAC item level holds Holds pickup period (day) + Bookings allowed (total) + Bookings per item (total) + Bookings period length (day) [% IF Koha.Preference('ArticleRequests') %] Article requests [% END %] @@ -204,8 +207,11 @@ [% SET recall_overdue_fine = all_rules.$c.$i.recall_overdue_fine %] [% SET recall_shelf_time = all_rules.$c.$i.recall_shelf_time %] [% SET holds_pickup_period = all_rules.$c.$i.holds_pickup_period %] + [% SET bookings_allowed_total = all_rules.$c.$i.bookings_allowed_total %] + [% SET bookings_per_item = all_rules.$c.$i.bookings_per_item %] + [% SET bookings_period_length = all_rules.$c.$i.bookings_period_length %] - [% SET show_rule = note || maxissueqty || maxonsiteissueqty || issuelength || daysmode || lengthunit || hardduedate || hardduedatecompare || fine || chargeperiod || chargeperiod_charge_at || firstremind || overduefinescap || cap_fine_to_replacement_price || finedays || maxsuspensiondays || suspension_chargeperiod || renewalsallowed || unseenrenewalsallowed || renewalperiod || norenewalbefore || noautorenewalbefore || auto_renew || no_auto_renewal_after || no_auto_renewal_after_hard_limit || reservesallowed || holds_per_day || holds_per_record || onshelfholds || opacitemholds || article_requests || rentaldiscount || decreaseloanholds || recalls_allowed || recalls_per_record || on_shelf_recalls || recall_due_date_interval || recall_overdue_fine || recall_shelf_time || holds_pickup_period %] + [% SET show_rule = note || maxissueqty || maxonsiteissueqty || issuelength || daysmode || lengthunit || hardduedate || hardduedatecompare || fine || chargeperiod || chargeperiod_charge_at || firstremind || overduefinescap || cap_fine_to_replacement_price || finedays || maxsuspensiondays || suspension_chargeperiod || renewalsallowed || unseenrenewalsallowed || renewalperiod || norenewalbefore || noautorenewalbefore || auto_renew || no_auto_renewal_after || no_auto_renewal_after_hard_limit || reservesallowed || holds_per_day || holds_per_record || onshelfholds || opacitemholds || article_requests || rentaldiscount || decreaseloanholds || recalls_allowed || recalls_per_record || on_shelf_recalls || recall_due_date_interval || recall_overdue_fine || recall_shelf_time || holds_pickup_period || bookings_allowed_total || bookings_per_item || bookings_period_length %] [% IF show_rule %] [% SET row_count = row_count + 1 %] @@ -378,6 +384,27 @@ [% holds_pickup_period | html %] [% END %] + + [% IF bookings_allowed_total.defined && bookings_allowed_total != '' %] + [% bookings_allowed_total | html %] + [% ELSE %] + Unlimited + [% END %] + + + [% IF bookings_per_item.defined && bookings_per_item != '' %] + [% bookings_per_item | html %] + [% ELSE %] + Unlimited + [% END %] + + + [% IF bookings_period_length.defined && bookings_period_length != '' %] + [% bookings_period_length | html %] + [% ELSE %] + Not defined + [% END %] + [% IF Koha.Preference('ArticleRequests') %] [% IF article_requests == 'no' %] @@ -528,6 +555,9 @@ + + + [% IF Koha.Preference('ArticleRequests') %]