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 =head2 get_return_branch_policy
479 my $returnbranch = Koha::CirculationRules->get_return_branch_policy($item);
481 Returns the branch to use for returning the item based on the
482 item type, and a branch selected via CircControlReturnsBranch.
484 The return value is the branch to which to return the item. Possible values:
485 noreturn: do not return, let item remain where checked in (floating collections)
486 homebranch: return to item's home branch
487 holdingbranch: return to issuer branch
489 This searches branchitemrules in the following order:
490 * Same branchcode and itemtype
491 * Same branchcode, itemtype '*'
492 * branchcode '*', same itemtype
493 * branchcode '*' and itemtype '*'
497 sub get_return_branch_policy {
498 my ( $self, $item ) = @_;
500 my $pref = C4::Context->preference('CircControlReturnsBranch');
503 $pref eq 'ItemHomeLibrary' ? $item->homebranch
504 : $pref eq 'ItemHoldingLibrary' ? $item->holdingbranch
505 : $pref eq 'CheckInLibrary' ? C4::Context->userenv
506 ? C4::Context->userenv->{branch}
510 my $itemtype = $item->effective_itemtype;
512 my $rule = Koha::CirculationRules->get_effective_rule(
514 rule_name => 'returnbranch',
515 itemtype => $itemtype,
516 branchcode => $branchcode,
520 return $rule ? $rule->rule_value : 'homebranch';
524 =head3 get_opacitemholds_policy
526 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
528 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
529 and the "Item level holds" (opacitemholds).
530 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
534 sub get_opacitemholds_policy {
535 my ( $class, $params ) = @_;
537 my $item = $params->{item};
538 my $patron = $params->{patron};
540 return unless $item or $patron;
542 my $rule = Koha::CirculationRules->get_effective_rule(
544 categorycode => $patron->categorycode,
545 itemtype => $item->effective_itemtype,
546 branchcode => $item->homebranch,
547 rule_name => 'opacitemholds',
551 return $rule ? $rule->rule_value : undef;
554 =head3 get_onshelfholds_policy
556 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
560 sub get_onshelfholds_policy {
561 my ( $class, $params ) = @_;
562 my $item = $params->{item};
563 my $itemtype = $item->effective_itemtype;
564 my $patron = $params->{patron};
565 my $rule = Koha::CirculationRules->get_effective_rule(
567 categorycode => ( $patron ? $patron->categorycode : undef ),
568 itemtype => $itemtype,
569 branchcode => $item->holdingbranch,
570 rule_name => 'onshelfholds',
573 return $rule ? $rule->rule_value : 0;
576 =head3 get_lostreturn_policy
578 my $lostrefund_policy = Koha::CirculationRules->get_lostreturn_policy( { return_branch => $return_branch, item => $item } );
584 =item '0' - Do not refund
586 =item 'refund' - Refund the lost item charge
588 =item 'restore' - Refund the lost item charge and restore the original overdue fine
590 =item 'charge' - Refund the lost item charge and charge a new overdue fine
596 sub get_lostreturn_policy {
597 my ( $class, $params ) = @_;
599 my $item = $params->{item};
601 my $behaviour = C4::Context->preference( 'RefundLostOnReturnControl' ) // 'CheckinLibrary';
602 my $behaviour_mapping = {
603 CheckinLibrary => $params->{'return_branch'} // $item->homebranch,
604 ItemHomeBranch => $item->homebranch,
605 ItemHoldingBranch => $item->holdingbranch
608 my $branch = $behaviour_mapping->{ $behaviour };
610 my $rule = Koha::CirculationRules->get_effective_rule(
612 branchcode => $branch,
613 rule_name => 'lostreturn',
617 return $rule ? $rule->rule_value : 'refund';
620 =head3 article_requestable_rules
622 Return rules that allow article requests, optionally filtered by
625 Use with care; see guess_article_requestable_itemtypes.
629 sub article_requestable_rules {
630 my ( $class, $params ) = @_;
631 my $category = $params->{categorycode};
633 return if !C4::Context->preference('ArticleRequests');
634 return $class->search({
635 $category ? ( categorycode => [ $category, undef ] ) : (),
636 rule_name => 'article_requests',
637 rule_value => { '!=' => 'no' },
641 =head3 guess_article_requestable_itemtypes
643 Return item types in a hashref that are likely possible to be
644 'article requested'. Constructed by an intelligent guess in the
645 issuing rules (see article_requestable_rules).
647 Note: pref ArticleRequestsLinkControl overrides the algorithm.
649 Optional parameters: categorycode.
651 Note: the routine is used in opac-search to obtain a reasonable
652 estimate within performance borders (not looking at all items but
653 just using default itemtype). Also we are not looking at the
654 branchcode here, since home or holding branch of the item is
655 leading and branch may be unknown too (anonymous opac session).
659 sub guess_article_requestable_itemtypes {
660 my ( $class, $params ) = @_;
661 my $category = $params->{categorycode};
662 return {} if !C4::Context->preference('ArticleRequests');
663 return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
665 my $cache = Koha::Caches->get_instance;
666 my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
667 my $key = $category || '*';
668 return $last_article_requestable_guesses->{$key}
669 if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
672 my $rules = $class->article_requestable_rules({
673 $category ? ( categorycode => $category ) : (),
675 return $res if !$rules;
676 foreach my $rule ( $rules->as_list ) {
677 $res->{ $rule->itemtype // '*' } = 1;
679 $last_article_requestable_guesses->{$key} = $res;
680 $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
684 =head3 get_effective_daysmode
686 Return the value for daysmode defined in the circulation rules.
687 If not defined (or empty string), the value of the system preference useDaysMode is returned
691 sub get_effective_daysmode {
692 my ( $class, $params ) = @_;
694 my $categorycode = $params->{categorycode};
695 my $itemtype = $params->{itemtype};
696 my $branchcode = $params->{branchcode};
698 my $daysmode_rule = $class->get_effective_rule(
700 categorycode => $categorycode,
701 itemtype => $itemtype,
702 branchcode => $branchcode,
703 rule_name => 'daysmode',
707 return ( defined($daysmode_rule)
708 and $daysmode_rule->rule_value ne '' )
709 ? $daysmode_rule->rule_value
710 : C4::Context->preference('useDaysMode');
720 return 'CirculationRule';
728 return 'Koha::CirculationRule';