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 patron_maxissueqty => {
56 scope => [ 'branchcode', 'categorycode' ],
58 patron_maxonsiteissueqty => {
59 scope => [ 'branchcode', 'categorycode' ],
62 scope => [ 'branchcode', 'categorycode' ],
66 scope => [ 'branchcode', 'itemtype' ],
69 hold_fulfillment_policy => {
70 scope => [ 'branchcode', 'itemtype' ],
74 scope => [ 'branchcode', 'itemtype' ],
79 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
81 article_request_fee => {
82 scope => [ 'branchcode', 'categorycode' ],
84 open_article_requests_limit => {
85 scope => [ 'branchcode', 'categorycode' ],
89 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
91 cap_fine_to_replacement_price => {
92 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
95 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
97 chargeperiod_charge_at => {
98 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
101 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
104 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
107 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
110 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
112 hardduedatecompare => {
113 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
116 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
118 holds_per_record => {
119 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
122 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
125 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
128 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
131 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
133 maxonsiteissueqty => {
134 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
136 maxsuspensiondays => {
137 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
139 no_auto_renewal_after => {
140 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
142 no_auto_renewal_after_hard_limit => {
143 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
146 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
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' ],
163 unseen_renewals_allowed => {
164 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
167 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
171 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
173 suspension_chargeperiod => {
174 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
176 note => { # This is not really a rule. Maybe we will want to separate this later.
177 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
179 decreaseloanholds => {
180 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
183 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
185 recalls_per_record => {
186 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
188 on_shelf_recalls => {
189 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
191 recall_due_date_interval => {
192 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
194 recall_overdue_fine => {
195 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
197 recall_shelf_time => {
198 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
200 # Not included (deprecated?):
210 =head3 get_effective_rule
212 my $effective_rule = Koha::CirculationRules->get_effective_rule(
215 categorycode => $categorycode,
216 itemtype => $itemtype,
217 branchcode => $branchcode
221 Return the effective rule object for the rule associated with the criteria passed.
226 sub get_effective_rule {
227 my ( $self, $params ) = @_;
229 $params->{categorycode} //= undef;
230 $params->{branchcode} //= undef;
231 $params->{itemtype} //= undef;
233 my $rule_name = $params->{rule_name};
234 my $categorycode = $params->{categorycode};
235 my $itemtype = $params->{itemtype};
236 my $branchcode = $params->{branchcode};
238 Koha::Exceptions::MissingParameter->throw(
239 "Required parameter 'rule_name' missing")
242 for my $v ( $branchcode, $categorycode, $itemtype ) {
243 $v = undef if $v and $v eq '*';
246 my $order_by = $params->{order_by}
247 // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
250 $search_params->{rule_name} = $rule_name;
252 $search_params->{categorycode} = defined $categorycode ? [ $categorycode, undef ] : undef;
253 $search_params->{itemtype} = defined $itemtype ? [ $itemtype, undef ] : undef;
254 $search_params->{branchcode} = defined $branchcode ? [ $branchcode, undef ] : undef;
256 my $rule = $self->search(
259 order_by => $order_by,
267 =head3 get_effective_rule_value
269 my $effective_rule_value = Koha::CirculationRules->get_effective_rule_value(
272 categorycode => $categorycode,
273 itemtype => $itemtype,
274 branchcode => $branchcode
278 Return the effective value for the rule associated with the criteria passed.
280 This is a cached method so should be used in preference to get_effective_rule where possible
285 sub get_effective_rule_value {
286 my ( $self, $params ) = @_;
288 my $rule_name = $params->{rule_name};
289 my $categorycode = $params->{categorycode};
290 my $itemtype = $params->{itemtype};
291 my $branchcode = $params->{branchcode};
293 my $memory_cache = Koha::Cache::Memory::Lite->get_instance;
294 my $cache_key = sprintf "CircRules:%s:%s:%s:%s", $rule_name // q{},
295 $categorycode // q{}, $branchcode // q{}, $itemtype // q{};
297 my $cached = $memory_cache->get_from_cache($cache_key);
298 return $cached if $cached;
300 my $rule = $self->get_effective_rule($params);
302 my $value= $rule ? $rule->rule_value : undef;
303 $memory_cache->set_in_cache( $cache_key, $value );
307 =head3 get_effective_rules
311 sub get_effective_rules {
312 my ( $self, $params ) = @_;
314 my $rules = $params->{rules};
315 my $categorycode = $params->{categorycode};
316 my $itemtype = $params->{itemtype};
317 my $branchcode = $params->{branchcode};
320 foreach my $rule (@$rules) {
321 my $effective_rule = $self->get_effective_rule_value(
324 categorycode => $categorycode,
325 itemtype => $itemtype,
326 branchcode => $branchcode,
330 $r->{$rule} = $effective_rule if defined $effective_rule;
341 my ( $self, $params ) = @_;
343 for my $mandatory_parameter (qw( rule_name rule_value ) ) {
344 Koha::Exceptions::MissingParameter->throw(
345 "Required parameter '$mandatory_parameter' missing")
346 unless exists $params->{$mandatory_parameter};
349 my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
350 Koha::Exceptions::MissingParameter->throw(
351 "set_rule given unknown rule '$params->{rule_name}'!")
352 unless defined $kind_info;
354 # Enforce scope; a rule should be set for its defined scope, no more, no less.
355 foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
356 if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
357 croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
358 unless exists $params->{$scope_level};
360 croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
361 if exists $params->{$scope_level};
365 my $branchcode = $params->{branchcode};
366 my $categorycode = $params->{categorycode};
367 my $itemtype = $params->{itemtype};
368 my $rule_name = $params->{rule_name};
369 my $rule_value = $params->{rule_value};
370 my $can_be_blank = defined $kind_info->{can_be_blank} ? $kind_info->{can_be_blank} : 1;
371 $rule_value = undef if defined $rule_value && $rule_value eq "" && !$can_be_blank;
373 for my $v ( $branchcode, $categorycode, $itemtype ) {
374 $v = undef if $v and $v eq '*';
376 my $rule = $self->search(
378 rule_name => $rule_name,
379 branchcode => $branchcode,
380 categorycode => $categorycode,
381 itemtype => $itemtype,
386 if ( defined $rule_value ) {
387 $rule->rule_value($rule_value);
395 if ( defined $rule_value ) {
396 $rule = Koha::CirculationRule->new(
398 branchcode => $branchcode,
399 categorycode => $categorycode,
400 itemtype => $itemtype,
401 rule_name => $rule_name,
402 rule_value => $rule_value,
409 my $memory_cache = Koha::Cache::Memory::Lite->get_instance;
410 for my $k ( $memory_cache->all_keys ) {
411 $memory_cache->clear_from_cache($k) if $k =~ m{^CircRules:};
422 my ( $self, $params ) = @_;
425 $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
426 $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
427 $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
428 my $rules = $params->{rules};
430 my $rule_objects = [];
431 while ( my ( $rule_name, $rule_value ) = each %$rules ) {
432 my $rule_object = Koha::CirculationRules->set_rule(
435 rule_name => $rule_name,
436 rule_value => $rule_value,
439 push( @$rule_objects, $rule_object );
442 return $rule_objects;
447 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
454 while ( my $rule = $self->next ){
461 Clone a set of circulation rules to another branch
466 my ( $self, $to_branch ) = @_;
468 while ( my $rule = $self->next ){
469 $rule->clone($to_branch);
473 =head3 get_opacitemholds_policy
475 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
477 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
478 and the "Item level holds" (opacitemholds).
479 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
483 sub get_opacitemholds_policy {
484 my ( $class, $params ) = @_;
486 my $item = $params->{item};
487 my $patron = $params->{patron};
489 return unless $item or $patron;
491 my $rule = Koha::CirculationRules->get_effective_rule(
493 categorycode => $patron->categorycode,
494 itemtype => $item->effective_itemtype,
495 branchcode => $item->homebranch,
496 rule_name => 'opacitemholds',
500 return $rule ? $rule->rule_value : undef;
503 =head3 get_onshelfholds_policy
505 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
509 sub get_onshelfholds_policy {
510 my ( $class, $params ) = @_;
511 my $item = $params->{item};
512 my $itemtype = $item->effective_itemtype;
513 my $patron = $params->{patron};
514 my $rule = Koha::CirculationRules->get_effective_rule(
516 categorycode => ( $patron ? $patron->categorycode : undef ),
517 itemtype => $itemtype,
518 branchcode => $item->holdingbranch,
519 rule_name => 'onshelfholds',
522 return $rule ? $rule->rule_value : 0;
525 =head3 get_lostreturn_policy
527 my $lostrefund_policy = Koha::CirculationRules->get_lostreturn_policy( { return_branch => $return_branch, item => $item } );
533 =item '0' - Do not refund
535 =item 'refund' - Refund the lost item charge
537 =item 'restore' - Refund the lost item charge and restore the original overdue fine
539 =item 'charge' - Refund the lost item charge and charge a new overdue fine
545 sub get_lostreturn_policy {
546 my ( $class, $params ) = @_;
548 my $item = $params->{item};
550 my $behaviour = C4::Context->preference( 'RefundLostOnReturnControl' ) // 'CheckinLibrary';
551 my $behaviour_mapping = {
552 CheckinLibrary => $params->{'return_branch'} // $item->homebranch,
553 ItemHomeBranch => $item->homebranch,
554 ItemHoldingBranch => $item->holdingbranch
557 my $branch = $behaviour_mapping->{ $behaviour };
559 my $rule = Koha::CirculationRules->get_effective_rule(
561 branchcode => $branch,
562 rule_name => 'lostreturn',
566 return $rule ? $rule->rule_value : 'refund';
569 =head3 article_requestable_rules
571 Return rules that allow article requests, optionally filtered by
574 Use with care; see guess_article_requestable_itemtypes.
578 sub article_requestable_rules {
579 my ( $class, $params ) = @_;
580 my $category = $params->{categorycode};
582 return if !C4::Context->preference('ArticleRequests');
583 return $class->search({
584 $category ? ( categorycode => [ $category, undef ] ) : (),
585 rule_name => 'article_requests',
586 rule_value => { '!=' => 'no' },
590 =head3 guess_article_requestable_itemtypes
592 Return item types in a hashref that are likely possible to be
593 'article requested'. Constructed by an intelligent guess in the
594 issuing rules (see article_requestable_rules).
596 Note: pref ArticleRequestsLinkControl overrides the algorithm.
598 Optional parameters: categorycode.
600 Note: the routine is used in opac-search to obtain a reasonable
601 estimate within performance borders (not looking at all items but
602 just using default itemtype). Also we are not looking at the
603 branchcode here, since home or holding branch of the item is
604 leading and branch may be unknown too (anonymous opac session).
608 sub guess_article_requestable_itemtypes {
609 my ( $class, $params ) = @_;
610 my $category = $params->{categorycode};
611 return {} if !C4::Context->preference('ArticleRequests');
612 return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
614 my $cache = Koha::Caches->get_instance;
615 my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
616 my $key = $category || '*';
617 return $last_article_requestable_guesses->{$key}
618 if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
621 my $rules = $class->article_requestable_rules({
622 $category ? ( categorycode => $category ) : (),
624 return $res if !$rules;
625 foreach my $rule ( $rules->as_list ) {
626 $res->{ $rule->itemtype // '*' } = 1;
628 $last_article_requestable_guesses->{$key} = $res;
629 $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
633 =head3 get_effective_daysmode
635 Return the value for daysmode defined in the circulation rules.
636 If not defined (or empty string), the value of the system preference useDaysMode is returned
640 sub get_effective_daysmode {
641 my ( $class, $params ) = @_;
643 my $categorycode = $params->{categorycode};
644 my $itemtype = $params->{itemtype};
645 my $branchcode = $params->{branchcode};
647 my $daysmode_rule = $class->get_effective_rule(
649 categorycode => $categorycode,
650 itemtype => $itemtype,
651 branchcode => $branchcode,
652 rule_name => 'daysmode',
656 return ( defined($daysmode_rule)
657 and $daysmode_rule->rule_value ne '' )
658 ? $daysmode_rule->rule_value
659 : C4::Context->preference('useDaysMode');
669 return 'CirculationRule';
677 return 'Koha::CirculationRule';