Bugzilla – Attachment 184855 Details for
Bug 36271
Bookings should have circulation rules
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 36271: Add circulation rules for booking
Bug-36271-Add-circulation-rules-for-booking.patch (text/plain), 19.87 KB, created by
Thibaud Guillot (thibaud_g)
on 2025-07-30 07:09:01 UTC
(
hide
)
Description:
Bug 36271: Add circulation rules for booking
Filename:
MIME Type:
Creator:
Thibaud Guillot (thibaud_g)
Created:
2025-07-30 07:09:01 UTC
Size:
19.87 KB
patch
obsolete
>From 41a7c59e6ac51a9276a98c60cd85560844f0950d Mon Sep 17 00:00:00 2001 >From: Thibaud Guillot <thibaud.guillot@biblibre.com> >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 / bookings_period_length > >By branches, you can set quotas either generally or by item. > >Note that 'Booking period length' add a way to limit the booking period, >pre or post process is used (I think :) ) only for staff usage to >prepare items, here it's a way of restricting the possible length of time during which you can book. > >Test plan: > >1) Apply this patch >2) Set values for these new rules >3) Try to perform some bookings with different rules values (also on the > OPAC side) > >Sponsored by: Association de Gestion des Ã…uvres Sociales d'Inria (AGOS) >--- > Koha/Booking.pm | 97 +++++++++++++++++++ > Koha/CirculationRules.pm | 13 ++- > Koha/Exceptions/Booking.pm | 4 + > Koha/REST/V1/Bookings.pm | 8 ++ > admin/smart-rules.pl | 9 ++ > .../prog/en/modules/admin/smart-rules.tt | 35 ++++++- > .../prog/js/modals/place_booking.js | 33 ++++++- > .../bootstrap/js/modals/place_booking.js | 30 +++++- > 8 files changed, 222 insertions(+), 7 deletions(-) > >diff --git a/Koha/Booking.pm b/Koha/Booking.pm >index ab5a418047..f769a00da5 100644 >--- a/Koha/Booking.pm >+++ b/Koha/Booking.pm >@@ -24,12 +24,18 @@ 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 List::Util qw(any); > > use base qw(Koha::Object); >+use List::Util qw(min); > > =head1 NAME > >@@ -39,6 +45,94 @@ 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. >+=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}; >+ my $bookings_per_item = $rights->{bookings_per_item}; >+ 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 >@@ -151,6 +245,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 7bae491af5..1037de2fb6 100644 >--- a/Koha/CirculationRules.pm >+++ b/Koha/CirculationRules.pm >@@ -227,7 +227,18 @@ our $RULE_KINDS = { > bookings_trail_period => { > scope => [ 'branchcode', 'itemtype' ], > }, >- >+ bookings_allowed_total => { >+ scope => [ 'branchcode', 'categorycode', 'itemtype' ], >+ can_be_blank => 0, >+ }, >+ bookings_per_item => { >+ scope => [ 'branchcode', 'categorycode', 'itemtype' ], >+ can_be_blank => 0, >+ }, >+ bookings_period_length => { >+ scope => [ 'branchcode', 'categorycode', 'itemtype' ], >+ can_be_blank => 0, >+ }, > # 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 54d20116f2..68e019e1e2 100644 >--- a/Koha/REST/V1/Bookings.pm >+++ b/Koha/REST/V1/Bookings.pm >@@ -101,6 +101,14 @@ sub add { > error => "Duplicate booking_id", > } > ); >+ } elsif ( blessed $_ and $_->isa('Koha::Exceptions::Booking::Rule') ) { >+ return $c->render( >+ status => 403, >+ openapi => { >+ error => $_->{'message'}->{'status'}, >+ limit => $_->{'message'}->{'limit'} >+ } >+ ); > } > > return $c->unhandled_exception($_); >diff --git a/admin/smart-rules.pl b/admin/smart-rules.pl >index 2c1ed57a44..089028264a 100755 >--- a/admin/smart-rules.pl >+++ b/admin/smart-rules.pl >@@ -121,6 +121,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 > } > } > ); >@@ -318,6 +321,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, >@@ -361,6 +367,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 4930108177..bdfc26e1f1 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 >@@ -156,6 +156,9 @@ > <th>On shelf holds allowed</th> > <th>OPAC item level holds</th> > <th>Holds pickup period (day)</th> >+ <th>Bookings allowed (total)</th> >+ <th>Bookings per item (total)</th> >+ <th>Bookings period length (day)</th> > [% IF Koha.Preference('ArticleRequests') %] > <th>Article requests</th> > [% END %] >@@ -218,8 +221,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 || expire_reserves_charge || 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 || expire_reserves_charge || 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 %] > <tr row_countd="row_[% row_count | html %]"> >@@ -412,6 +418,27 @@ > [% holds_pickup_period | html %] > [% END %] > </td> >+ <td> >+ [% IF bookings_allowed_total.defined && bookings_allowed_total != '' %] >+ [% bookings_allowed_total | html %] >+ [% ELSE %] >+ <span>Unlimited</span> >+ [% END %] >+ </td> >+ <td> >+ [% IF bookings_per_item.defined && bookings_per_item != '' %] >+ [% bookings_per_item | html %] >+ [% ELSE %] >+ <span>Unlimited</span> >+ [% END %] >+ </td> >+ <td> >+ [% IF bookings_period_length.defined && bookings_period_length != '' %] >+ [% bookings_period_length | html %] >+ [% ELSE %] >+ <span>Not defined</span> >+ [% END %] >+ </td> > [% IF Koha.Preference('ArticleRequests') %] > <td data-code="[% article_requests | html %]"> > [% IF article_requests == 'no' %] >@@ -565,6 +592,9 @@ > </select> > </td> > <td><input type="text" name="holds_pickup_period" id="holds_pickup_period" size="2" /></td> >+ <td><input type="text" name="bookings_allowed_total" id="bookings_allowed_total" size="2" /></td> >+ <td><input type="text" name="bookings_per_item" id="bookings_per_item" size="2" /></td> >+ <td><input type="text" name="bookings_period_length" id="bookings_period_length" size="3" /></td> > [% IF Koha.Preference('ArticleRequests') %] > <td> > <select id="article_requests" name="article_requests"> >@@ -637,6 +667,9 @@ > <th>On shelf holds allowed</th> > <th>OPAC item level holds</th> > <th>Holds pickup period (day)</th> >+ <th>Bookings allowed (total)</th> >+ <th>Bookings per item (total)</th> >+ <th>Bookings period length (day)</th> > [% IF Koha.Preference('ArticleRequests') %] > <th>Article requests</th> > [% END %] >diff --git a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js b/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js >index 634cce0c36..63de484a34 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js >@@ -1204,10 +1204,37 @@ $("#placeBookingForm").on("submit", function (e) { > }); > > posting.fail(function (data) { >+ const error = data.responseJSON.error; >+ const limit = data.responseJSON.limit; >+ >+ let displayMessage; >+ >+ switch (error) { >+ case 'noBookingsAllowed': >+ displayMessage = __("Bookings are not allowed according to circulation rules"); >+ break; >+ case 'tooManyBookings': >+ displayMessage = __("Patron has reached the maximum of booking according to circulation rules"); >+ break; >+ case 'ageRestricted': >+ displayMessage = __("Age restricted"); >+ break; >+ case 'tooLongBookingPeriod': >+ displayMessage = __("Booking period exceed booking period limit according to circulation rules"); >+ break; >+ case 'bookingPeriodNotValid': >+ displayMessage = __("Booking period must be valid"); >+ break; >+ default: >+ displayMessage = __("Failure"); >+ } >+ >+ if (limit) { >+ displayMessage += ` (${limit})`; >+ } >+ > $("#booking_result").replaceWith( >- '<div id="booking_result" class="alert alert-danger">' + >- __("Failure") + >- "</div>" >+ `<div id="booking_result" class="alert alert-danger">${__(displayMessage)}</div>` > ); > }); > } else { >diff --git a/koha-tmpl/opac-tmpl/bootstrap/js/modals/place_booking.js b/koha-tmpl/opac-tmpl/bootstrap/js/modals/place_booking.js >index 71f7a26f39..070ddfc55d 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/js/modals/place_booking.js >+++ b/koha-tmpl/opac-tmpl/bootstrap/js/modals/place_booking.js >@@ -234,8 +234,34 @@ $("#opac-add-form-booking").on('submit', function(e) { > > request.fail(function (data) { > var error = data.responseJSON.error; >- var errorMessage = error ? error : "Failure"; >- errors.push(item_id + " - " + errorMessage); >+ var limit = data.responseJSON.limit; >+ let displayMessage; >+ >+ switch (error) { >+ case 'noBookingsAllowed': >+ displayMessage = __("Bookings are not allowed according to circulation rules"); >+ break; >+ case 'tooManyBookings': >+ displayMessage = __("Patron has reached the maximum of booking according to circulation rules"); >+ break; >+ case 'ageRestricted': >+ displayMessage = __("Age restricted"); >+ break; >+ case 'tooLongBookingPeriod': >+ displayMessage = __("Booking period exceed booking period limit according to circulation rules"); >+ break; >+ case 'bookingPeriodNotValid': >+ displayMessage = __("Booking period must be valid"); >+ break; >+ default: >+ displayMessage = __("Failure"); >+ } >+ >+ if (limit) { >+ displayMessage += ` (${limit})`; >+ } >+ >+ errors.push(`${displayMessage} - (${item_id})`); > }); > }); > >-- >2.39.5
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 36271
:
163546
|
164627
|
165061
|
165063
|
166809
|
166907
|
166930
|
166931
|
166932
|
166933
|
166952
|
166957
|
166962
|
170197
|
172866
|
172924
| 184855