From bfbf9fca435a9392985af9431d7da54daed8b8b4 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 4 new circulation rules :) - Bookings allowed (total) / bookings_allowed_total - Bookings per item (total) / bookings_per_item - Bookings per itemtype (total) / bookings per itemtype - Bookings period length (day) / bookings_period_length Test plan: 1) Apply this patch 2) Set values for these new rules ** Note that rules respect the item setting bookable: - bookable to "yes" => bookings per item rule - bookable to "follow item type" => bookings per itemtype rule - bookable to 'no' => no "Bookings" section on menu 3) Try perform a booking now 4) Define a total to 1, 2 per item and 3 per itemtype with 2 days for example for booking period. 5) After 1 an error message showing related to total allowed 6) Set total to 5 and retry on item bookable sets to "Yes" 7) Normally it's works but do it again (to have 3 bookings and an error message appears) 8) Do the same thing on item bookable sets to "Follow item type", the maximum will be 3 now before see the error. ** Note that if bookings total and booking per item/itemtype is set it will be the minimum number. 9)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 | 96 +++++++++++++++++++ Koha/CirculationRules.pm | 12 +++ Koha/Exceptions/Booking.pm | 4 + Koha/REST/V1/Bookings.pm | 17 ++++ admin/smart-rules.pl | 12 +++ .../prog/en/modules/admin/smart-rules.tt | 40 +++++++- .../prog/js/place_booking_modal.js | 4 +- 7 files changed, 183 insertions(+), 2 deletions(-) diff --git a/Koha/Booking.pm b/Koha/Booking.pm index 65d3122b6ee..901f816cdc2 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,92 @@ 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 }, if the borrower has exceeded their maximum booking amount. + { status => tooLongBookingPeriod, limit => $limit }, if the borrower has exceeded their maximum booking period. + { status => } + +=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_per_itemtype', 'bookings_period_length' ] + } + ); + + my $bookings_allowed_total = $rights->{bookings_allowed_total} // 0; + my $bookings_per_item = $rights->{bookings_per_item} // 1; + my $bookings_per_itemtype = $rights->{bookings_per_itemtype} // 1; + my $bookings_period_length = $rights->{bookings_period_length}; + my $booking_limit; + + my $bookable_status = $item->bookable; + + if($bookable_status == 1) { + $booking_limit = $bookings_per_item; + } elsif (!defined($bookable_status)) { + $booking_limit = $bookings_per_itemtype; + } else { + $booking_limit = 0; + } + + if ( defined $bookings_allowed_total && $bookings_allowed_total ne '' ) { + if ( $bookings_allowed_total == 0 ) { + return { status => 'noBookingsAllowed' }; + } else { + $booking_limit = min( $booking_limit, $bookings_allowed_total ); + } + } + + if ( $booking_limit == 0 ) { + return { status => "noBookingsAllowedOnThisItem" }; + } + + my $total_bookings_count = Koha::Bookings->search( { patron_id => $patron->borrowernumber } )->count(); + return { status => 'tooManyBookings', limit => $booking_limit } if $booking_limit <= $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 +219,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..ef53d1c08b0 100644 --- a/Koha/CirculationRules.pm +++ b/Koha/CirculationRules.pm @@ -216,6 +216,18 @@ 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_per_itemtype => { + 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..7cb357c9982 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 %error_strings = ( + 'noBookingsAllowed' => 'Bookings are not allowed according to circulation rules', + 'noBookingsAllowedOnThisItem' => 'Bookings are not allowed on this item according to circulation rules', + 'tooManyBookings' => sprintf('Patron has reached the maximum of booking according to circulation rules : %s maximum', $limit), + '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..a0b6ce21f38 100755 --- a/admin/smart-rules.pl +++ b/admin/smart-rules.pl @@ -117,6 +117,10 @@ if ($op eq 'cud-delete') { recall_shelf_time => undef, decreaseloanholds => undef, holds_pickup_period => undef, + bookings_allowed_total => undef, + bookings_per_item => undef, + bookings_per_itemtype => undef, + bookings_period_length => undef, } } ); @@ -302,6 +306,10 @@ 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_per_itemtype = strip_non_numeric( scalar $input->param('bookings_per_itemtype') ); + my $bookings_period_length = $input->param('bookings_period_length') || 0; my $rules = { maxissueqty => $maxissueqty, @@ -344,6 +352,10 @@ 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_per_itemtype => $bookings_per_itemtype, + 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..9b4ed3721b0 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,10 @@ On shelf holds allowed OPAC item level holds Holds pickup period (day) + Bookings allowed (total) + Bookings per item (total) + Bookings per itemtype (total) + Bookings period length (day) [% IF Koha.Preference('ArticleRequests') %] Article requests [% END %] @@ -212,8 +216,12 @@ [% 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_per_itemtype = all_rules.$c.$i.bookings_per_itemtype %] + [% 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_per_itemtype || bookings_period_length %] [% IF show_rule %] [% SET row_count = row_count + 1 %] @@ -380,6 +388,28 @@ [% 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_per_itemtype.defined && bookings_per_itemtype != '' %] + [% bookings_per_itemtype | html %] + [% ELSE %] + Unlimited + [% END %] + + [% bookings_period_length | html %] [% IF Koha.Preference('ArticleRequests') %] [% IF article_requests == 'no' %] @@ -530,6 +560,10 @@ + + + + [% IF Koha.Preference('ArticleRequests') %]