1 package Koha::CirculationRules;
3 # Copyright ByWater Solutions 2017
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
24 use Koha::CirculationRule;
26 use base qw(Koha::Objects);
28 use constant GUESSED_ITEMTYPES_KEY => 'Koha_IssuingRules_last_guess';
32 Koha::CirculationRules - Koha CirculationRule Object set class
42 This structure describes the possible rules that may be set, and what scopes they can be set at.
44 Any attempt to set a rule with a nonsensical scope (for instance, setting the C<patron_maxissueqty> for a branchcode and itemtype), is an error.
50 scope => [ 'branchcode' ],
53 patron_maxissueqty => {
54 scope => [ 'branchcode', 'categorycode' ],
56 patron_maxonsiteissueqty => {
57 scope => [ 'branchcode', 'categorycode' ],
60 scope => [ 'branchcode', 'categorycode' ],
64 scope => [ 'branchcode', 'itemtype' ],
67 hold_fulfillment_policy => {
68 scope => [ 'branchcode', 'itemtype' ],
72 scope => [ 'branchcode', 'itemtype' ],
77 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
79 article_request_fee => {
80 scope => [ 'branchcode', 'categorycode' ],
82 open_article_requests_limit => {
83 scope => [ 'branchcode', 'categorycode' ],
87 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
89 cap_fine_to_replacement_price => {
90 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
93 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
95 chargeperiod_charge_at => {
96 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
99 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
102 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
105 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
108 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
110 hardduedatecompare => {
111 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
114 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
116 holds_per_record => {
117 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
120 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
123 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
126 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
129 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
131 maxonsiteissueqty => {
132 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
134 maxsuspensiondays => {
135 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
137 no_auto_renewal_after => {
138 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
140 no_auto_renewal_after_hard_limit => {
141 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
144 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
147 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
150 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
153 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
156 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
159 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
161 unseen_renewals_allowed => {
162 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
165 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
169 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
171 suspension_chargeperiod => {
172 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
174 note => { # This is not really a rule. Maybe we will want to separate this later.
175 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
177 decreaseloanholds => {
178 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
181 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
183 recalls_per_record => {
184 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
186 on_shelf_recalls => {
187 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
189 recall_due_date_interval => {
190 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
192 recall_overdue_fine => {
193 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
195 recall_shelf_time => {
196 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
198 # Not included (deprecated?):
208 =head3 get_effective_rule
212 sub get_effective_rule {
213 my ( $self, $params ) = @_;
215 $params->{categorycode} //= undef;
216 $params->{branchcode} //= undef;
217 $params->{itemtype} //= undef;
219 my $rule_name = $params->{rule_name};
220 my $categorycode = $params->{categorycode};
221 my $itemtype = $params->{itemtype};
222 my $branchcode = $params->{branchcode};
224 Koha::Exceptions::MissingParameter->throw(
225 "Required parameter 'rule_name' missing")
228 for my $v ( $branchcode, $categorycode, $itemtype ) {
229 $v = undef if $v and $v eq '*';
232 my $order_by = $params->{order_by}
233 // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
236 $search_params->{rule_name} = $rule_name;
238 $search_params->{categorycode} = defined $categorycode ? [ $categorycode, undef ] : undef;
239 $search_params->{itemtype} = defined $itemtype ? [ $itemtype, undef ] : undef;
240 $search_params->{branchcode} = defined $branchcode ? [ $branchcode, undef ] : undef;
242 my $rule = $self->search(
245 order_by => $order_by,
253 =head3 get_effective_rules
257 sub get_effective_rules {
258 my ( $self, $params ) = @_;
260 my $rules = $params->{rules};
261 my $categorycode = $params->{categorycode};
262 my $itemtype = $params->{itemtype};
263 my $branchcode = $params->{branchcode};
266 foreach my $rule (@$rules) {
267 my $effective_rule = $self->get_effective_rule(
270 categorycode => $categorycode,
271 itemtype => $itemtype,
272 branchcode => $branchcode,
276 $r->{$rule} = $effective_rule->rule_value if $effective_rule;
287 my ( $self, $params ) = @_;
289 for my $mandatory_parameter (qw( rule_name rule_value ) ) {
290 Koha::Exceptions::MissingParameter->throw(
291 "Required parameter '$mandatory_parameter' missing")
292 unless exists $params->{$mandatory_parameter};
295 my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
296 Koha::Exceptions::MissingParameter->throw(
297 "set_rule given unknown rule '$params->{rule_name}'!")
298 unless defined $kind_info;
300 # Enforce scope; a rule should be set for its defined scope, no more, no less.
301 foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
302 if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
303 croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
304 unless exists $params->{$scope_level};
306 croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
307 if exists $params->{$scope_level};
311 my $branchcode = $params->{branchcode};
312 my $categorycode = $params->{categorycode};
313 my $itemtype = $params->{itemtype};
314 my $rule_name = $params->{rule_name};
315 my $rule_value = $params->{rule_value};
316 my $can_be_blank = defined $kind_info->{can_be_blank} ? $kind_info->{can_be_blank} : 1;
317 $rule_value = undef if defined $rule_value && $rule_value eq "" && !$can_be_blank;
319 for my $v ( $branchcode, $categorycode, $itemtype ) {
320 $v = undef if $v and $v eq '*';
322 my $rule = $self->search(
324 rule_name => $rule_name,
325 branchcode => $branchcode,
326 categorycode => $categorycode,
327 itemtype => $itemtype,
332 if ( defined $rule_value ) {
333 $rule->rule_value($rule_value);
341 if ( defined $rule_value ) {
342 $rule = Koha::CirculationRule->new(
344 branchcode => $branchcode,
345 categorycode => $categorycode,
346 itemtype => $itemtype,
347 rule_name => $rule_name,
348 rule_value => $rule_value,
363 my ( $self, $params ) = @_;
366 $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
367 $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
368 $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
369 my $rules = $params->{rules};
371 my $rule_objects = [];
372 while ( my ( $rule_name, $rule_value ) = each %$rules ) {
373 my $rule_object = Koha::CirculationRules->set_rule(
376 rule_name => $rule_name,
377 rule_value => $rule_value,
380 push( @$rule_objects, $rule_object );
383 return $rule_objects;
388 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
395 while ( my $rule = $self->next ){
402 Clone a set of circulation rules to another branch
407 my ( $self, $to_branch ) = @_;
409 while ( my $rule = $self->next ){
410 $rule->clone($to_branch);
414 =head3 get_opacitemholds_policy
416 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
418 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
419 and the "Item level holds" (opacitemholds).
420 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
424 sub get_opacitemholds_policy {
425 my ( $class, $params ) = @_;
427 my $item = $params->{item};
428 my $patron = $params->{patron};
430 return unless $item or $patron;
432 my $rule = Koha::CirculationRules->get_effective_rule(
434 categorycode => $patron->categorycode,
435 itemtype => $item->effective_itemtype,
436 branchcode => $item->homebranch,
437 rule_name => 'opacitemholds',
441 return $rule ? $rule->rule_value : undef;
444 =head3 get_onshelfholds_policy
446 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
450 sub get_onshelfholds_policy {
451 my ( $class, $params ) = @_;
452 my $item = $params->{item};
453 my $itemtype = $item->effective_itemtype;
454 my $patron = $params->{patron};
455 my $rule = Koha::CirculationRules->get_effective_rule(
457 categorycode => ( $patron ? $patron->categorycode : undef ),
458 itemtype => $itemtype,
459 branchcode => $item->holdingbranch,
460 rule_name => 'onshelfholds',
463 return $rule ? $rule->rule_value : 0;
466 =head3 get_lostreturn_policy
468 my $lostrefund_policy = Koha::CirculationRules->get_lostreturn_policy( { return_branch => $return_branch, item => $item } );
474 =item '0' - Do not refund
476 =item 'refund' - Refund the lost item charge
478 =item 'restore' - Refund the lost item charge and restore the original overdue fine
480 =item 'charge' - Refund the lost item charge and charge a new overdue fine
486 sub get_lostreturn_policy {
487 my ( $class, $params ) = @_;
489 my $item = $params->{item};
491 my $behaviour = C4::Context->preference( 'RefundLostOnReturnControl' ) // 'CheckinLibrary';
492 my $behaviour_mapping = {
493 CheckinLibrary => $params->{'return_branch'} // $item->homebranch,
494 ItemHomeBranch => $item->homebranch,
495 ItemHoldingBranch => $item->holdingbranch
498 my $branch = $behaviour_mapping->{ $behaviour };
500 my $rule = Koha::CirculationRules->get_effective_rule(
502 branchcode => $branch,
503 rule_name => 'lostreturn',
507 return $rule ? $rule->rule_value : 'refund';
510 =head3 article_requestable_rules
512 Return rules that allow article requests, optionally filtered by
515 Use with care; see guess_article_requestable_itemtypes.
519 sub article_requestable_rules {
520 my ( $class, $params ) = @_;
521 my $category = $params->{categorycode};
523 return if !C4::Context->preference('ArticleRequests');
524 return $class->search({
525 $category ? ( categorycode => [ $category, undef ] ) : (),
526 rule_name => 'article_requests',
527 rule_value => { '!=' => 'no' },
531 =head3 guess_article_requestable_itemtypes
533 Return item types in a hashref that are likely possible to be
534 'article requested'. Constructed by an intelligent guess in the
535 issuing rules (see article_requestable_rules).
537 Note: pref ArticleRequestsLinkControl overrides the algorithm.
539 Optional parameters: categorycode.
541 Note: the routine is used in opac-search to obtain a reasonable
542 estimate within performance borders (not looking at all items but
543 just using default itemtype). Also we are not looking at the
544 branchcode here, since home or holding branch of the item is
545 leading and branch may be unknown too (anonymous opac session).
549 sub guess_article_requestable_itemtypes {
550 my ( $class, $params ) = @_;
551 my $category = $params->{categorycode};
552 return {} if !C4::Context->preference('ArticleRequests');
553 return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
555 my $cache = Koha::Caches->get_instance;
556 my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
557 my $key = $category || '*';
558 return $last_article_requestable_guesses->{$key}
559 if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
562 my $rules = $class->article_requestable_rules({
563 $category ? ( categorycode => $category ) : (),
565 return $res if !$rules;
566 foreach my $rule ( $rules->as_list ) {
567 $res->{ $rule->itemtype // '*' } = 1;
569 $last_article_requestable_guesses->{$key} = $res;
570 $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
574 =head3 get_daysmode_effective_value
576 Return the value for daysmode defined in the circulation rules.
577 If not defined (or empty string), the value of the system preference useDaysMode is returned
581 sub get_effective_daysmode {
582 my ( $class, $params ) = @_;
584 my $categorycode = $params->{categorycode};
585 my $itemtype = $params->{itemtype};
586 my $branchcode = $params->{branchcode};
588 my $daysmode_rule = $class->get_effective_rule(
590 categorycode => $categorycode,
591 itemtype => $itemtype,
592 branchcode => $branchcode,
593 rule_name => 'daysmode',
597 return ( defined($daysmode_rule)
598 and $daysmode_rule->rule_value ne '' )
599 ? $daysmode_rule->rule_value
600 : C4::Context->preference('useDaysMode');
610 return 'CirculationRule';
618 return 'Koha::CirculationRule';