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 Koha::Cache::Memory::Lite;
28 use base qw(Koha::Objects);
30 use constant GUESSED_ITEMTYPES_KEY => 'Koha_CirculationRules_last_guess';
34 Koha::CirculationRules - Koha CirculationRule Object set class
44 This structure describes the possible rules that may be set, and what scopes they can be set at.
46 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.
52 scope => [ 'branchcode' ],
55 scope => [ 'branchcode' ],
57 patron_maxissueqty => {
58 scope => [ 'branchcode', 'categorycode' ],
60 patron_maxonsiteissueqty => {
61 scope => [ 'branchcode', 'categorycode' ],
64 scope => [ 'branchcode', 'categorycode' ],
68 scope => [ 'branchcode', 'itemtype' ],
71 hold_fulfillment_policy => {
72 scope => [ 'branchcode', 'itemtype' ],
76 scope => [ 'branchcode', 'itemtype' ],
81 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
83 article_request_fee => {
84 scope => [ 'branchcode', 'categorycode' ],
86 open_article_requests_limit => {
87 scope => [ 'branchcode', 'categorycode' ],
91 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
93 cap_fine_to_replacement_price => {
94 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
97 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
99 chargeperiod_charge_at => {
100 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
103 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
106 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
109 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
112 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
114 hardduedatecompare => {
115 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
117 waiting_hold_cancellation => {
118 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
122 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
124 holds_per_record => {
125 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
128 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
131 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
134 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
137 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
139 maxonsiteissueqty => {
140 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
142 maxsuspensiondays => {
143 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
145 no_auto_renewal_after => {
146 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
148 no_auto_renewal_after_hard_limit => {
149 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
152 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
155 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
158 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
161 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
164 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
167 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
169 unseen_renewals_allowed => {
170 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
173 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
177 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
179 suspension_chargeperiod => {
180 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
182 note => { # This is not really a rule. Maybe we will want to separate this later.
183 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
185 decreaseloanholds => {
186 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
189 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
191 recalls_per_record => {
192 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
194 on_shelf_recalls => {
195 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
197 recall_due_date_interval => {
198 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
200 recall_overdue_fine => {
201 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
203 recall_shelf_time => {
204 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
206 # Not included (deprecated?):
216 =head3 get_effective_rule
218 my $effective_rule = Koha::CirculationRules->get_effective_rule(
221 categorycode => $categorycode,
222 itemtype => $itemtype,
223 branchcode => $branchcode
227 Return the effective rule object for the rule associated with the criteria passed.
232 sub get_effective_rule {
233 my ( $self, $params ) = @_;
235 $params->{categorycode} //= undef;
236 $params->{branchcode} //= undef;
237 $params->{itemtype} //= undef;
239 my $rule_name = $params->{rule_name};
240 my $categorycode = $params->{categorycode};
241 my $itemtype = $params->{itemtype};
242 my $branchcode = $params->{branchcode};
244 Koha::Exceptions::MissingParameter->throw(
245 "Required parameter 'rule_name' missing")
248 for my $v ( $branchcode, $categorycode, $itemtype ) {
249 $v = undef if $v and $v eq '*';
252 my $order_by = $params->{order_by}
253 // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
256 $search_params->{rule_name} = $rule_name;
258 $search_params->{categorycode} = defined $categorycode ? [ $categorycode, undef ] : undef;
259 $search_params->{itemtype} = defined $itemtype ? [ $itemtype, undef ] : undef;
260 $search_params->{branchcode} = defined $branchcode ? [ $branchcode, undef ] : undef;
262 my $rule = $self->search(
265 order_by => $order_by,
273 =head3 get_effective_rule_value
275 my $effective_rule_value = Koha::CirculationRules->get_effective_rule_value(
278 categorycode => $categorycode,
279 itemtype => $itemtype,
280 branchcode => $branchcode
284 Return the effective value for the rule associated with the criteria passed.
286 This is a cached method so should be used in preference to get_effective_rule where possible
291 sub get_effective_rule_value {
292 my ( $self, $params ) = @_;
294 my $rule_name = $params->{rule_name};
295 my $categorycode = $params->{categorycode};
296 my $itemtype = $params->{itemtype};
297 my $branchcode = $params->{branchcode};
299 my $memory_cache = Koha::Cache::Memory::Lite->get_instance;
300 my $cache_key = sprintf "CircRules:%s:%s:%s:%s", $rule_name // q{},
301 $categorycode // q{}, $branchcode // q{}, $itemtype // q{};
303 my $cached = $memory_cache->get_from_cache($cache_key);
304 return $cached if $cached;
306 my $rule = $self->get_effective_rule($params);
308 my $value= $rule ? $rule->rule_value : undef;
309 $memory_cache->set_in_cache( $cache_key, $value );
313 =head3 get_effective_rules
317 sub get_effective_rules {
318 my ( $self, $params ) = @_;
320 my $rules = $params->{rules};
321 my $categorycode = $params->{categorycode};
322 my $itemtype = $params->{itemtype};
323 my $branchcode = $params->{branchcode};
326 foreach my $rule (@$rules) {
327 my $effective_rule = $self->get_effective_rule_value(
330 categorycode => $categorycode,
331 itemtype => $itemtype,
332 branchcode => $branchcode,
336 $r->{$rule} = $effective_rule if defined $effective_rule;
347 my ( $self, $params ) = @_;
349 for my $mandatory_parameter (qw( rule_name rule_value ) ) {
350 Koha::Exceptions::MissingParameter->throw(
351 "Required parameter '$mandatory_parameter' missing")
352 unless exists $params->{$mandatory_parameter};
355 my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
356 Koha::Exceptions::MissingParameter->throw(
357 "set_rule given unknown rule '$params->{rule_name}'!")
358 unless defined $kind_info;
360 # Enforce scope; a rule should be set for its defined scope, no more, no less.
361 foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
362 if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
363 croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
364 unless exists $params->{$scope_level};
366 croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
367 if exists $params->{$scope_level};
371 my $branchcode = $params->{branchcode};
372 my $categorycode = $params->{categorycode};
373 my $itemtype = $params->{itemtype};
374 my $rule_name = $params->{rule_name};
375 my $rule_value = $params->{rule_value};
376 my $can_be_blank = defined $kind_info->{can_be_blank} ? $kind_info->{can_be_blank} : 1;
377 $rule_value = undef if defined $rule_value && $rule_value eq "" && !$can_be_blank;
379 for my $v ( $branchcode, $categorycode, $itemtype ) {
380 $v = undef if $v and $v eq '*';
382 my $rule = $self->search(
384 rule_name => $rule_name,
385 branchcode => $branchcode,
386 categorycode => $categorycode,
387 itemtype => $itemtype,
392 if ( defined $rule_value ) {
393 $rule->rule_value($rule_value);
401 if ( defined $rule_value ) {
402 $rule = Koha::CirculationRule->new(
404 branchcode => $branchcode,
405 categorycode => $categorycode,
406 itemtype => $itemtype,
407 rule_name => $rule_name,
408 rule_value => $rule_value,
415 my $memory_cache = Koha::Cache::Memory::Lite->get_instance;
416 for my $k ( $memory_cache->all_keys ) {
417 $memory_cache->clear_from_cache($k) if $k =~ m{^CircRules:};
428 my ( $self, $params ) = @_;
431 $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
432 $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
433 $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
434 my $rules = $params->{rules};
436 my $rule_objects = [];
437 while ( my ( $rule_name, $rule_value ) = each %$rules ) {
438 my $rule_object = Koha::CirculationRules->set_rule(
441 rule_name => $rule_name,
442 rule_value => $rule_value,
445 push( @$rule_objects, $rule_object );
448 return $rule_objects;
453 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
460 while ( my $rule = $self->next ){
467 Clone a set of circulation rules to another branch
472 my ( $self, $to_branch ) = @_;
474 while ( my $rule = $self->next ){
475 $rule->clone($to_branch);
479 =head2 get_return_branch_policy
481 my $returnbranch = Koha::CirculationRules->get_return_branch_policy($item);
483 Returns the branch to use for returning the item based on the
484 item type, and a branch selected via CircControlReturnsBranch.
486 The return value is the branch to which to return the item. Possible values:
487 noreturn: do not return, let item remain where checked in (floating collections)
488 homebranch: return to item's home branch
489 holdingbranch: return to issuer branch
491 This searches branchitemrules in the following order:
492 * Same branchcode and itemtype
493 * Same branchcode, itemtype '*'
494 * branchcode '*', same itemtype
495 * branchcode '*' and itemtype '*'
499 sub get_return_branch_policy {
500 my ( $self, $item ) = @_;
502 my $pref = C4::Context->preference('CircControlReturnsBranch');
505 $pref eq 'ItemHomeLibrary' ? $item->homebranch
506 : $pref eq 'ItemHoldingLibrary' ? $item->holdingbranch
507 : $pref eq 'CheckInLibrary' ? C4::Context->userenv
508 ? C4::Context->userenv->{branch}
512 my $itemtype = $item->effective_itemtype;
514 my $rule = Koha::CirculationRules->get_effective_rule(
516 rule_name => 'returnbranch',
517 itemtype => $itemtype,
518 branchcode => $branchcode,
522 return $rule ? $rule->rule_value : 'homebranch';
526 =head3 get_opacitemholds_policy
528 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
530 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
531 and the "Item level holds" (opacitemholds).
532 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
536 sub get_opacitemholds_policy {
537 my ( $class, $params ) = @_;
539 my $item = $params->{item};
540 my $patron = $params->{patron};
542 return unless $item or $patron;
544 my $rule = Koha::CirculationRules->get_effective_rule(
546 categorycode => $patron->categorycode,
547 itemtype => $item->effective_itemtype,
548 branchcode => $item->homebranch,
549 rule_name => 'opacitemholds',
553 return $rule ? $rule->rule_value : undef;
556 =head3 get_onshelfholds_policy
558 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
562 sub get_onshelfholds_policy {
563 my ( $class, $params ) = @_;
564 my $item = $params->{item};
565 my $itemtype = $item->effective_itemtype;
566 my $patron = $params->{patron};
567 my $rule = Koha::CirculationRules->get_effective_rule(
569 categorycode => ( $patron ? $patron->categorycode : undef ),
570 itemtype => $itemtype,
571 branchcode => $item->holdingbranch,
572 rule_name => 'onshelfholds',
575 return $rule ? $rule->rule_value : 0;
578 =head3 get_lostreturn_policy
580 my $lostrefund_policy = Koha::CirculationRules->get_lostreturn_policy( { return_branch => $return_branch, item => $item } );
586 =item '0' - Do not refund
588 =item 'refund' - Refund the lost item charge
590 =item 'restore' - Refund the lost item charge and restore the original overdue fine
592 =item 'charge' - Refund the lost item charge and charge a new overdue fine
598 sub get_lostreturn_policy {
599 my ( $class, $params ) = @_;
601 my $item = $params->{item};
603 my $behaviour = C4::Context->preference( 'RefundLostOnReturnControl' ) // 'CheckinLibrary';
604 my $behaviour_mapping = {
605 CheckinLibrary => $params->{'return_branch'} // $item->homebranch,
606 ItemHomeBranch => $item->homebranch,
607 ItemHoldingBranch => $item->holdingbranch
610 my $branch = $behaviour_mapping->{ $behaviour };
612 my $rule = Koha::CirculationRules->get_effective_rule(
614 branchcode => $branch,
615 rule_name => 'lostreturn',
619 return $rule ? $rule->rule_value : 'refund';
622 =head3 get_processingreturn_policy
624 my $processingrefund_policy = Koha::CirculationRules->get_processingreturn_policy( { return_branch => $return_branch, item => $item } );
630 =item '0' - Do not refund
632 =item 'refund' - Refund the lost item processing charge
634 =item 'restore' - Refund the lost item processing charge and restore the original overdue fine
636 =item 'charge' - Refund the lost item processing charge and charge a new overdue fine
642 sub get_processingreturn_policy {
643 my ( $class, $params ) = @_;
645 my $item = $params->{item};
647 my $behaviour = C4::Context->preference( 'RefundLostOnReturnControl' ) // 'CheckinLibrary';
648 my $behaviour_mapping = {
649 CheckinLibrary => $params->{'return_branch'} // $item->homebranch,
650 ItemHomeBranch => $item->homebranch,
651 ItemHoldingBranch => $item->holdingbranch
654 my $branch = $behaviour_mapping->{ $behaviour };
656 my $rule = Koha::CirculationRules->get_effective_rule(
658 branchcode => $branch,
659 rule_name => 'processingreturn',
663 return $rule ? $rule->rule_value : 'refund';
666 =head3 article_requestable_rules
668 Return rules that allow article requests, optionally filtered by
671 Use with care; see guess_article_requestable_itemtypes.
675 sub article_requestable_rules {
676 my ( $class, $params ) = @_;
677 my $category = $params->{categorycode};
679 return if !C4::Context->preference('ArticleRequests');
680 return $class->search({
681 $category ? ( categorycode => [ $category, undef ] ) : (),
682 rule_name => 'article_requests',
683 rule_value => { '!=' => 'no' },
687 =head3 guess_article_requestable_itemtypes
689 Return item types in a hashref that are likely possible to be
690 'article requested'. Constructed by an intelligent guess in the
691 issuing rules (see article_requestable_rules).
693 Note: pref ArticleRequestsLinkControl overrides the algorithm.
695 Optional parameters: categorycode.
697 Note: the routine is used in opac-search to obtain a reasonable
698 estimate within performance borders (not looking at all items but
699 just using default itemtype). Also we are not looking at the
700 branchcode here, since home or holding branch of the item is
701 leading and branch may be unknown too (anonymous opac session).
705 sub guess_article_requestable_itemtypes {
706 my ( $class, $params ) = @_;
707 my $category = $params->{categorycode};
708 return {} if !C4::Context->preference('ArticleRequests');
709 return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
711 my $cache = Koha::Caches->get_instance;
712 my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
713 my $key = $category || '*';
714 return $last_article_requestable_guesses->{$key}
715 if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
718 my $rules = $class->article_requestable_rules({
719 $category ? ( categorycode => $category ) : (),
721 return $res if !$rules;
722 foreach my $rule ( $rules->as_list ) {
723 $res->{ $rule->itemtype // '*' } = 1;
725 $last_article_requestable_guesses->{$key} = $res;
726 $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
730 =head3 get_effective_daysmode
732 Return the value for daysmode defined in the circulation rules.
733 If not defined (or empty string), the value of the system preference useDaysMode is returned
737 sub get_effective_daysmode {
738 my ( $class, $params ) = @_;
740 my $categorycode = $params->{categorycode};
741 my $itemtype = $params->{itemtype};
742 my $branchcode = $params->{branchcode};
744 my $daysmode_rule = $class->get_effective_rule(
746 categorycode => $categorycode,
747 itemtype => $itemtype,
748 branchcode => $branchcode,
749 rule_name => 'daysmode',
753 return ( defined($daysmode_rule)
754 and $daysmode_rule->rule_value ne '' )
755 ? $daysmode_rule->rule_value
756 : C4::Context->preference('useDaysMode');
766 return 'CirculationRule';
774 return 'Koha::CirculationRule';