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' ],
115 waiting_hold_cancellation => {
116 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
120 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
122 holds_per_record => {
123 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
126 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
129 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
132 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
135 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
137 maxonsiteissueqty => {
138 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
140 maxsuspensiondays => {
141 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
143 no_auto_renewal_after => {
144 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
146 no_auto_renewal_after_hard_limit => {
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' ],
162 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
165 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
167 unseen_renewals_allowed => {
168 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
171 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
175 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
177 suspension_chargeperiod => {
178 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
180 note => { # This is not really a rule. Maybe we will want to separate this later.
181 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
183 decreaseloanholds => {
184 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
187 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
189 recalls_per_record => {
190 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
192 on_shelf_recalls => {
193 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
195 recall_due_date_interval => {
196 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
198 recall_overdue_fine => {
199 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
201 recall_shelf_time => {
202 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
204 # Not included (deprecated?):
214 =head3 get_effective_rule
216 my $effective_rule = Koha::CirculationRules->get_effective_rule(
219 categorycode => $categorycode,
220 itemtype => $itemtype,
221 branchcode => $branchcode
225 Return the effective rule object for the rule associated with the criteria passed.
230 sub get_effective_rule {
231 my ( $self, $params ) = @_;
233 $params->{categorycode} //= undef;
234 $params->{branchcode} //= undef;
235 $params->{itemtype} //= undef;
237 my $rule_name = $params->{rule_name};
238 my $categorycode = $params->{categorycode};
239 my $itemtype = $params->{itemtype};
240 my $branchcode = $params->{branchcode};
242 Koha::Exceptions::MissingParameter->throw(
243 "Required parameter 'rule_name' missing")
246 for my $v ( $branchcode, $categorycode, $itemtype ) {
247 $v = undef if $v and $v eq '*';
250 my $order_by = $params->{order_by}
251 // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
254 $search_params->{rule_name} = $rule_name;
256 $search_params->{categorycode} = defined $categorycode ? [ $categorycode, undef ] : undef;
257 $search_params->{itemtype} = defined $itemtype ? [ $itemtype, undef ] : undef;
258 $search_params->{branchcode} = defined $branchcode ? [ $branchcode, undef ] : undef;
260 my $rule = $self->search(
263 order_by => $order_by,
271 =head3 get_effective_rule_value
273 my $effective_rule_value = Koha::CirculationRules->get_effective_rule_value(
276 categorycode => $categorycode,
277 itemtype => $itemtype,
278 branchcode => $branchcode
282 Return the effective value for the rule associated with the criteria passed.
284 This is a cached method so should be used in preference to get_effective_rule where possible
289 sub get_effective_rule_value {
290 my ( $self, $params ) = @_;
292 my $rule_name = $params->{rule_name};
293 my $categorycode = $params->{categorycode};
294 my $itemtype = $params->{itemtype};
295 my $branchcode = $params->{branchcode};
297 my $memory_cache = Koha::Cache::Memory::Lite->get_instance;
298 my $cache_key = sprintf "CircRules:%s:%s:%s:%s", $rule_name // q{},
299 $categorycode // q{}, $branchcode // q{}, $itemtype // q{};
301 my $cached = $memory_cache->get_from_cache($cache_key);
302 return $cached if $cached;
304 my $rule = $self->get_effective_rule($params);
306 my $value= $rule ? $rule->rule_value : undef;
307 $memory_cache->set_in_cache( $cache_key, $value );
311 =head3 get_effective_rules
315 sub get_effective_rules {
316 my ( $self, $params ) = @_;
318 my $rules = $params->{rules};
319 my $categorycode = $params->{categorycode};
320 my $itemtype = $params->{itemtype};
321 my $branchcode = $params->{branchcode};
324 foreach my $rule (@$rules) {
325 my $effective_rule = $self->get_effective_rule_value(
328 categorycode => $categorycode,
329 itemtype => $itemtype,
330 branchcode => $branchcode,
334 $r->{$rule} = $effective_rule if defined $effective_rule;
345 my ( $self, $params ) = @_;
347 for my $mandatory_parameter (qw( rule_name rule_value ) ) {
348 Koha::Exceptions::MissingParameter->throw(
349 "Required parameter '$mandatory_parameter' missing")
350 unless exists $params->{$mandatory_parameter};
353 my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
354 Koha::Exceptions::MissingParameter->throw(
355 "set_rule given unknown rule '$params->{rule_name}'!")
356 unless defined $kind_info;
358 # Enforce scope; a rule should be set for its defined scope, no more, no less.
359 foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
360 if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
361 croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
362 unless exists $params->{$scope_level};
364 croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
365 if exists $params->{$scope_level};
369 my $branchcode = $params->{branchcode};
370 my $categorycode = $params->{categorycode};
371 my $itemtype = $params->{itemtype};
372 my $rule_name = $params->{rule_name};
373 my $rule_value = $params->{rule_value};
374 my $can_be_blank = defined $kind_info->{can_be_blank} ? $kind_info->{can_be_blank} : 1;
375 $rule_value = undef if defined $rule_value && $rule_value eq "" && !$can_be_blank;
377 for my $v ( $branchcode, $categorycode, $itemtype ) {
378 $v = undef if $v and $v eq '*';
380 my $rule = $self->search(
382 rule_name => $rule_name,
383 branchcode => $branchcode,
384 categorycode => $categorycode,
385 itemtype => $itemtype,
390 if ( defined $rule_value ) {
391 $rule->rule_value($rule_value);
399 if ( defined $rule_value ) {
400 $rule = Koha::CirculationRule->new(
402 branchcode => $branchcode,
403 categorycode => $categorycode,
404 itemtype => $itemtype,
405 rule_name => $rule_name,
406 rule_value => $rule_value,
413 my $memory_cache = Koha::Cache::Memory::Lite->get_instance;
414 for my $k ( $memory_cache->all_keys ) {
415 $memory_cache->clear_from_cache($k) if $k =~ m{^CircRules:};
426 my ( $self, $params ) = @_;
429 $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
430 $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
431 $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
432 my $rules = $params->{rules};
434 my $rule_objects = [];
435 while ( my ( $rule_name, $rule_value ) = each %$rules ) {
436 my $rule_object = Koha::CirculationRules->set_rule(
439 rule_name => $rule_name,
440 rule_value => $rule_value,
443 push( @$rule_objects, $rule_object );
446 return $rule_objects;
451 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
458 while ( my $rule = $self->next ){
465 Clone a set of circulation rules to another branch
470 my ( $self, $to_branch ) = @_;
472 while ( my $rule = $self->next ){
473 $rule->clone($to_branch);
477 =head3 get_opacitemholds_policy
479 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
481 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
482 and the "Item level holds" (opacitemholds).
483 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
487 sub get_opacitemholds_policy {
488 my ( $class, $params ) = @_;
490 my $item = $params->{item};
491 my $patron = $params->{patron};
493 return unless $item or $patron;
495 my $rule = Koha::CirculationRules->get_effective_rule(
497 categorycode => $patron->categorycode,
498 itemtype => $item->effective_itemtype,
499 branchcode => $item->homebranch,
500 rule_name => 'opacitemholds',
504 return $rule ? $rule->rule_value : undef;
507 =head3 get_onshelfholds_policy
509 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
513 sub get_onshelfholds_policy {
514 my ( $class, $params ) = @_;
515 my $item = $params->{item};
516 my $itemtype = $item->effective_itemtype;
517 my $patron = $params->{patron};
518 my $rule = Koha::CirculationRules->get_effective_rule(
520 categorycode => ( $patron ? $patron->categorycode : undef ),
521 itemtype => $itemtype,
522 branchcode => $item->holdingbranch,
523 rule_name => 'onshelfholds',
526 return $rule ? $rule->rule_value : 0;
529 =head3 get_lostreturn_policy
531 my $lostrefund_policy = Koha::CirculationRules->get_lostreturn_policy( { return_branch => $return_branch, item => $item } );
537 =item '0' - Do not refund
539 =item 'refund' - Refund the lost item charge
541 =item 'restore' - Refund the lost item charge and restore the original overdue fine
543 =item 'charge' - Refund the lost item charge and charge a new overdue fine
549 sub get_lostreturn_policy {
550 my ( $class, $params ) = @_;
552 my $item = $params->{item};
554 my $behaviour = C4::Context->preference( 'RefundLostOnReturnControl' ) // 'CheckinLibrary';
555 my $behaviour_mapping = {
556 CheckinLibrary => $params->{'return_branch'} // $item->homebranch,
557 ItemHomeBranch => $item->homebranch,
558 ItemHoldingBranch => $item->holdingbranch
561 my $branch = $behaviour_mapping->{ $behaviour };
563 my $rule = Koha::CirculationRules->get_effective_rule(
565 branchcode => $branch,
566 rule_name => 'lostreturn',
570 return $rule ? $rule->rule_value : 'refund';
573 =head3 article_requestable_rules
575 Return rules that allow article requests, optionally filtered by
578 Use with care; see guess_article_requestable_itemtypes.
582 sub article_requestable_rules {
583 my ( $class, $params ) = @_;
584 my $category = $params->{categorycode};
586 return if !C4::Context->preference('ArticleRequests');
587 return $class->search({
588 $category ? ( categorycode => [ $category, undef ] ) : (),
589 rule_name => 'article_requests',
590 rule_value => { '!=' => 'no' },
594 =head3 guess_article_requestable_itemtypes
596 Return item types in a hashref that are likely possible to be
597 'article requested'. Constructed by an intelligent guess in the
598 issuing rules (see article_requestable_rules).
600 Note: pref ArticleRequestsLinkControl overrides the algorithm.
602 Optional parameters: categorycode.
604 Note: the routine is used in opac-search to obtain a reasonable
605 estimate within performance borders (not looking at all items but
606 just using default itemtype). Also we are not looking at the
607 branchcode here, since home or holding branch of the item is
608 leading and branch may be unknown too (anonymous opac session).
612 sub guess_article_requestable_itemtypes {
613 my ( $class, $params ) = @_;
614 my $category = $params->{categorycode};
615 return {} if !C4::Context->preference('ArticleRequests');
616 return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
618 my $cache = Koha::Caches->get_instance;
619 my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
620 my $key = $category || '*';
621 return $last_article_requestable_guesses->{$key}
622 if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
625 my $rules = $class->article_requestable_rules({
626 $category ? ( categorycode => $category ) : (),
628 return $res if !$rules;
629 foreach my $rule ( $rules->as_list ) {
630 $res->{ $rule->itemtype // '*' } = 1;
632 $last_article_requestable_guesses->{$key} = $res;
633 $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
637 =head3 get_effective_daysmode
639 Return the value for daysmode defined in the circulation rules.
640 If not defined (or empty string), the value of the system preference useDaysMode is returned
644 sub get_effective_daysmode {
645 my ( $class, $params ) = @_;
647 my $categorycode = $params->{categorycode};
648 my $itemtype = $params->{itemtype};
649 my $branchcode = $params->{branchcode};
651 my $daysmode_rule = $class->get_effective_rule(
653 categorycode => $categorycode,
654 itemtype => $itemtype,
655 branchcode => $branchcode,
656 rule_name => 'daysmode',
660 return ( defined($daysmode_rule)
661 and $daysmode_rule->rule_value ne '' )
662 ? $daysmode_rule->rule_value
663 : C4::Context->preference('useDaysMode');
673 return 'CirculationRule';
681 return 'Koha::CirculationRule';