From 612417b413ed987ffa01b49091b26e78468a1393 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 | 75 +++++++++++++++++++ Koha/CirculationRules.pm | 9 +++ Koha/Exceptions/Booking.pm | 4 + Koha/REST/V1/Bookings.pm | 17 +++++ admin/smart-rules.pl | 9 +++ .../prog/en/modules/admin/smart-rules.tt | 29 ++++++- .../prog/js/place_booking_modal.js | 4 +- 7 files changed, 145 insertions(+), 2 deletions(-) diff --git a/Koha/Booking.pm b/Koha/Booking.pm index 65d3122b6ee..3236ef8537d 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,71 @@ 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. +=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} // 0; + my $bookings_per_item = $rights->{bookings_per_item} // 1; + my $bookings_period_length = $rights->{bookings_period_length}; + + return { status => 'noBookingsAllowed' } if $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 $bookings_per_item <= $bookings_per_item_count; + + my $total_bookings_count = Koha::Bookings->search( { patron_id => $patron->borrowernumber } )->count(); + return { status => 'tooManyBookings', limit => $bookings_allowed_total, rule => 'bookings_allowed_total' } if $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 => '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 @@ -126,6 +198,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 2d1bb25ad83..b47728a03dc 100644 --- a/Koha/REST/V1/Bookings.pm +++ b/Koha/REST/V1/Bookings.pm @@ -98,6 +98,23 @@ 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), + '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 f5f168f44f4..f9c09b92688 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 f536ef97601..dedd6defe80 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 @@ -151,6 +151,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 %] @@ -212,8 +215,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 %] @@ -380,6 +386,21 @@ [% 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 %] + + [% bookings_period_length | html %] [% IF Koha.Preference('ArticleRequests') %] [% IF article_requests == 'no' %] @@ -530,6 +551,9 @@ + + + [% IF Koha.Preference('ArticleRequests') %]