From 3302bfe4fa8650bd16765e588330ec2322e6e222 Mon Sep 17 00:00:00 2001 From: Thibaud Guillot Date: Mon, 18 Mar 2024 14:40:21 +0100 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) --- Koha/Booking.pm | 88 +++++++++++++++++++ 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 +++++++- 6 files changed, 162 insertions(+), 1 deletion(-) diff --git a/Koha/Booking.pm b/Koha/Booking.pm index f1004ae1f73..e7d4f0e96cc 100644 --- a/Koha/Booking.pm +++ b/Koha/Booking.pm @@ -21,8 +21,15 @@ use Modern::Perl; use Koha::Exceptions::Booking; use Koha::DateUtils qw( dt_from_string ); +use Koha::Bookings; +use Koha::CirculationRules; +use Koha::Cache::Memory::Lite; + +use C4::Circulation; +use C4::Biblio; use base qw(Koha::Object); +use List::Util qw(min); =head1 NAME @@ -32,6 +39,84 @@ 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 = 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 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 @@ -139,6 +224,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 1d074768754..642eec7c124 100644 --- a/Koha/CirculationRules.pm +++ b/Koha/CirculationRules.pm @@ -216,6 +216,15 @@ our $RULE_KINDS = { holds_pickup_period => { scope => [ 'branchcode', 'categorycode', '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 79e5b6ad8cf..bad8f4037ec 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 44e381ef1b9..d96aa70ddac 100644 --- a/Koha/REST/V1/Bookings.pm +++ b/Koha/REST/V1/Bookings.pm @@ -95,6 +95,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 efe1b91dfb5..4299e1d43d3 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, } } ); @@ -302,6 +305,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, @@ -344,6 +350,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 8d0562edab8..5bfcfbc9389 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 %] @@ -372,6 +378,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' %] @@ -522,6 +549,9 @@ + + + [% IF Koha.Preference('ArticleRequests') %]