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
214 sub get_effective_rule {
215 my ( $self, $params ) = @_;
217 $params->{categorycode} //= undef;
218 $params->{branchcode} //= undef;
219 $params->{itemtype} //= undef;
221 my $rule_name = $params->{rule_name};
222 my $categorycode = $params->{categorycode};
223 my $itemtype = $params->{itemtype};
224 my $branchcode = $params->{branchcode};
226 Koha::Exceptions::MissingParameter->throw(
227 "Required parameter 'rule_name' missing")
230 for my $v ( $branchcode, $categorycode, $itemtype ) {
231 $v = undef if $v and $v eq '*';
234 my $order_by = $params->{order_by}
235 // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
238 $search_params->{rule_name} = $rule_name;
240 $search_params->{categorycode} = defined $categorycode ? [ $categorycode, undef ] : undef;
241 $search_params->{itemtype} = defined $itemtype ? [ $itemtype, undef ] : undef;
242 $search_params->{branchcode} = defined $branchcode ? [ $branchcode, undef ] : undef;
244 my $rule = $self->search(
247 order_by => $order_by,
255 sub get_effective_rule_value {
256 my ( $self, $params ) = @_;
258 my $rule_name = $params->{rule_name};
259 my $categorycode = $params->{categorycode};
260 my $itemtype = $params->{itemtype};
261 my $branchcode = $params->{branchcode};
263 my $memory_cache = Koha::Cache::Memory::Lite->get_instance;
264 my $cache_key = sprintf "CircRules:%s:%s:%s:%s", $rule_name // q{},
265 $categorycode // q{}, $branchcode // q{}, $itemtype // q{};
267 my $cached = $memory_cache->get_from_cache($cache_key);
268 return $cached if $cached;
270 my $rule = $self->get_effective_rule($params);
272 my $value= $rule ? $rule->rule_value : undef;
273 $memory_cache->set_in_cache( $cache_key, $value );
277 =head3 get_effective_rules
281 sub get_effective_rules {
282 my ( $self, $params ) = @_;
284 my $rules = $params->{rules};
285 my $categorycode = $params->{categorycode};
286 my $itemtype = $params->{itemtype};
287 my $branchcode = $params->{branchcode};
290 foreach my $rule (@$rules) {
291 my $effective_rule = $self->get_effective_rule_value(
294 categorycode => $categorycode,
295 itemtype => $itemtype,
296 branchcode => $branchcode,
300 $r->{$rule} = $effective_rule if defined $effective_rule;
311 my ( $self, $params ) = @_;
313 for my $mandatory_parameter (qw( rule_name rule_value ) ) {
314 Koha::Exceptions::MissingParameter->throw(
315 "Required parameter '$mandatory_parameter' missing")
316 unless exists $params->{$mandatory_parameter};
319 my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
320 Koha::Exceptions::MissingParameter->throw(
321 "set_rule given unknown rule '$params->{rule_name}'!")
322 unless defined $kind_info;
324 # Enforce scope; a rule should be set for its defined scope, no more, no less.
325 foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
326 if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
327 croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
328 unless exists $params->{$scope_level};
330 croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
331 if exists $params->{$scope_level};
335 my $branchcode = $params->{branchcode};
336 my $categorycode = $params->{categorycode};
337 my $itemtype = $params->{itemtype};
338 my $rule_name = $params->{rule_name};
339 my $rule_value = $params->{rule_value};
340 my $can_be_blank = defined $kind_info->{can_be_blank} ? $kind_info->{can_be_blank} : 1;
341 $rule_value = undef if defined $rule_value && $rule_value eq "" && !$can_be_blank;
343 for my $v ( $branchcode, $categorycode, $itemtype ) {
344 $v = undef if $v and $v eq '*';
346 my $rule = $self->search(
348 rule_name => $rule_name,
349 branchcode => $branchcode,
350 categorycode => $categorycode,
351 itemtype => $itemtype,
356 if ( defined $rule_value ) {
357 $rule->rule_value($rule_value);
365 if ( defined $rule_value ) {
366 $rule = Koha::CirculationRule->new(
368 branchcode => $branchcode,
369 categorycode => $categorycode,
370 itemtype => $itemtype,
371 rule_name => $rule_name,
372 rule_value => $rule_value,
379 my $memory_cache = Koha::Cache::Memory::Lite->get_instance;
380 my $cache_key = sprintf "CircRules:%s:%s:%s:%s", $rule_name // q{},
381 $categorycode // q{}, $branchcode // q{}, $itemtype // q{};
383 Koha::Cache::Memory::Lite->flush();
393 my ( $self, $params ) = @_;
396 $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
397 $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
398 $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
399 my $rules = $params->{rules};
401 my $rule_objects = [];
402 while ( my ( $rule_name, $rule_value ) = each %$rules ) {
403 my $rule_object = Koha::CirculationRules->set_rule(
406 rule_name => $rule_name,
407 rule_value => $rule_value,
410 push( @$rule_objects, $rule_object );
413 Koha::Cache::Memory::Lite->flush();
414 return $rule_objects;
419 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
426 while ( my $rule = $self->next ){
433 Clone a set of circulation rules to another branch
438 my ( $self, $to_branch ) = @_;
440 while ( my $rule = $self->next ){
441 $rule->clone($to_branch);
445 =head3 get_opacitemholds_policy
447 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
449 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
450 and the "Item level holds" (opacitemholds).
451 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
455 sub get_opacitemholds_policy {
456 my ( $class, $params ) = @_;
458 my $item = $params->{item};
459 my $patron = $params->{patron};
461 return unless $item or $patron;
463 my $rule = Koha::CirculationRules->get_effective_rule(
465 categorycode => $patron->categorycode,
466 itemtype => $item->effective_itemtype,
467 branchcode => $item->homebranch,
468 rule_name => 'opacitemholds',
472 return $rule ? $rule->rule_value : undef;
475 =head3 get_onshelfholds_policy
477 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
481 sub get_onshelfholds_policy {
482 my ( $class, $params ) = @_;
483 my $item = $params->{item};
484 my $itemtype = $item->effective_itemtype;
485 my $patron = $params->{patron};
486 my $rule = Koha::CirculationRules->get_effective_rule(
488 categorycode => ( $patron ? $patron->categorycode : undef ),
489 itemtype => $itemtype,
490 branchcode => $item->holdingbranch,
491 rule_name => 'onshelfholds',
494 return $rule ? $rule->rule_value : 0;
497 =head3 get_lostreturn_policy
499 my $lostrefund_policy = Koha::CirculationRules->get_lostreturn_policy( { return_branch => $return_branch, item => $item } );
505 =item '0' - Do not refund
507 =item 'refund' - Refund the lost item charge
509 =item 'restore' - Refund the lost item charge and restore the original overdue fine
511 =item 'charge' - Refund the lost item charge and charge a new overdue fine
517 sub get_lostreturn_policy {
518 my ( $class, $params ) = @_;
520 my $item = $params->{item};
522 my $behaviour = C4::Context->preference( 'RefundLostOnReturnControl' ) // 'CheckinLibrary';
523 my $behaviour_mapping = {
524 CheckinLibrary => $params->{'return_branch'} // $item->homebranch,
525 ItemHomeBranch => $item->homebranch,
526 ItemHoldingBranch => $item->holdingbranch
529 my $branch = $behaviour_mapping->{ $behaviour };
531 my $rule = Koha::CirculationRules->get_effective_rule(
533 branchcode => $branch,
534 rule_name => 'lostreturn',
538 return $rule ? $rule->rule_value : 'refund';
541 =head3 article_requestable_rules
543 Return rules that allow article requests, optionally filtered by
546 Use with care; see guess_article_requestable_itemtypes.
550 sub article_requestable_rules {
551 my ( $class, $params ) = @_;
552 my $category = $params->{categorycode};
554 return if !C4::Context->preference('ArticleRequests');
555 return $class->search({
556 $category ? ( categorycode => [ $category, undef ] ) : (),
557 rule_name => 'article_requests',
558 rule_value => { '!=' => 'no' },
562 =head3 guess_article_requestable_itemtypes
564 Return item types in a hashref that are likely possible to be
565 'article requested'. Constructed by an intelligent guess in the
566 issuing rules (see article_requestable_rules).
568 Note: pref ArticleRequestsLinkControl overrides the algorithm.
570 Optional parameters: categorycode.
572 Note: the routine is used in opac-search to obtain a reasonable
573 estimate within performance borders (not looking at all items but
574 just using default itemtype). Also we are not looking at the
575 branchcode here, since home or holding branch of the item is
576 leading and branch may be unknown too (anonymous opac session).
580 sub guess_article_requestable_itemtypes {
581 my ( $class, $params ) = @_;
582 my $category = $params->{categorycode};
583 return {} if !C4::Context->preference('ArticleRequests');
584 return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
586 my $cache = Koha::Caches->get_instance;
587 my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
588 my $key = $category || '*';
589 return $last_article_requestable_guesses->{$key}
590 if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
593 my $rules = $class->article_requestable_rules({
594 $category ? ( categorycode => $category ) : (),
596 return $res if !$rules;
597 foreach my $rule ( $rules->as_list ) {
598 $res->{ $rule->itemtype // '*' } = 1;
600 $last_article_requestable_guesses->{$key} = $res;
601 $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
605 =head3 get_daysmode_effective_value
607 Return the value for daysmode defined in the circulation rules.
608 If not defined (or empty string), the value of the system preference useDaysMode is returned
612 sub get_effective_daysmode {
613 my ( $class, $params ) = @_;
615 my $categorycode = $params->{categorycode};
616 my $itemtype = $params->{itemtype};
617 my $branchcode = $params->{branchcode};
619 my $daysmode_rule = $class->get_effective_rule(
621 categorycode => $categorycode,
622 itemtype => $itemtype,
623 branchcode => $branchcode,
624 rule_name => 'daysmode',
628 return ( defined($daysmode_rule)
629 and $daysmode_rule->rule_value ne '' )
630 ? $daysmode_rule->rule_value
631 : C4::Context->preference('useDaysMode');
641 return 'CirculationRule';
649 return 'Koha::CirculationRule';