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 base qw(Koha::Objects);
28 use constant GUESSED_ITEMTYPES_KEY => 'Koha_IssuingRules_last_guess';
32 Koha::CirculationRules - Koha CirculationRule Object set class
42 This structure describes the possible rules that may be set, and what scopes they can be set at.
44 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.
50 scope => [ 'branchcode' ],
53 patron_maxissueqty => {
54 scope => [ 'branchcode', 'categorycode' ],
56 patron_maxonsiteissueqty => {
57 scope => [ 'branchcode', 'categorycode' ],
60 scope => [ 'branchcode', 'categorycode' ],
64 scope => [ 'branchcode', 'itemtype' ],
67 hold_fulfillment_policy => {
68 scope => [ 'branchcode', 'itemtype' ],
72 scope => [ 'branchcode', 'itemtype' ],
77 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
79 open_article_requests_limit => {
80 scope => [ 'branchcode', 'categorycode' ],
84 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
86 cap_fine_to_replacement_price => {
87 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
90 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
92 chargeperiod_charge_at => {
93 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
96 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
99 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
102 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
105 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
107 hardduedatecompare => {
108 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
111 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
113 holds_per_record => {
114 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
117 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
120 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
123 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
126 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
128 maxonsiteissueqty => {
129 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
131 maxsuspensiondays => {
132 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
134 no_auto_renewal_after => {
135 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
137 no_auto_renewal_after_hard_limit => {
138 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
141 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
144 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
147 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
150 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
153 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
156 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
158 unseen_renewals_allowed => {
159 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
162 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
166 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
168 suspension_chargeperiod => {
169 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
171 note => { # This is not really a rule. Maybe we will want to separate this later.
172 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
174 decreaseloanholds => {
175 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
177 # Not included (deprecated?):
187 =head3 get_effective_rule
191 sub get_effective_rule {
192 my ( $self, $params ) = @_;
194 $params->{categorycode} //= undef;
195 $params->{branchcode} //= undef;
196 $params->{itemtype} //= undef;
198 my $rule_name = $params->{rule_name};
199 my $categorycode = $params->{categorycode};
200 my $itemtype = $params->{itemtype};
201 my $branchcode = $params->{branchcode};
203 Koha::Exceptions::MissingParameter->throw(
204 "Required parameter 'rule_name' missing")
207 for my $v ( $branchcode, $categorycode, $itemtype ) {
208 $v = undef if $v and $v eq '*';
211 my $order_by = $params->{order_by}
212 // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
215 $search_params->{rule_name} = $rule_name;
217 $search_params->{categorycode} = defined $categorycode ? [ $categorycode, undef ] : undef;
218 $search_params->{itemtype} = defined $itemtype ? [ $itemtype, undef ] : undef;
219 $search_params->{branchcode} = defined $branchcode ? [ $branchcode, undef ] : undef;
221 my $rule = $self->search(
224 order_by => $order_by,
232 =head3 get_effective_rules
236 sub get_effective_rules {
237 my ( $self, $params ) = @_;
239 my $rules = $params->{rules};
240 my $categorycode = $params->{categorycode};
241 my $itemtype = $params->{itemtype};
242 my $branchcode = $params->{branchcode};
245 foreach my $rule (@$rules) {
246 my $effective_rule = $self->get_effective_rule(
249 categorycode => $categorycode,
250 itemtype => $itemtype,
251 branchcode => $branchcode,
255 $r->{$rule} = $effective_rule->rule_value if $effective_rule;
266 my ( $self, $params ) = @_;
268 for my $mandatory_parameter (qw( rule_name rule_value ) ) {
269 Koha::Exceptions::MissingParameter->throw(
270 "Required parameter '$mandatory_parameter' missing")
271 unless exists $params->{$mandatory_parameter};
274 my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
275 Koha::Exceptions::MissingParameter->throw(
276 "set_rule given unknown rule '$params->{rule_name}'!")
277 unless defined $kind_info;
279 # Enforce scope; a rule should be set for its defined scope, no more, no less.
280 foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
281 if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
282 croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
283 unless exists $params->{$scope_level};
285 croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
286 if exists $params->{$scope_level};
290 my $branchcode = $params->{branchcode};
291 my $categorycode = $params->{categorycode};
292 my $itemtype = $params->{itemtype};
293 my $rule_name = $params->{rule_name};
294 my $rule_value = $params->{rule_value};
295 my $can_be_blank = defined $kind_info->{can_be_blank} ? $kind_info->{can_be_blank} : 1;
296 $rule_value = undef if defined $rule_value && $rule_value eq "" && !$can_be_blank;
298 for my $v ( $branchcode, $categorycode, $itemtype ) {
299 $v = undef if $v and $v eq '*';
301 my $rule = $self->search(
303 rule_name => $rule_name,
304 branchcode => $branchcode,
305 categorycode => $categorycode,
306 itemtype => $itemtype,
311 if ( defined $rule_value ) {
312 $rule->rule_value($rule_value);
320 if ( defined $rule_value ) {
321 $rule = Koha::CirculationRule->new(
323 branchcode => $branchcode,
324 categorycode => $categorycode,
325 itemtype => $itemtype,
326 rule_name => $rule_name,
327 rule_value => $rule_value,
342 my ( $self, $params ) = @_;
345 $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
346 $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
347 $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
348 my $rules = $params->{rules};
350 my $rule_objects = [];
351 while ( my ( $rule_name, $rule_value ) = each %$rules ) {
352 my $rule_object = Koha::CirculationRules->set_rule(
355 rule_name => $rule_name,
356 rule_value => $rule_value,
359 push( @$rule_objects, $rule_object );
362 return $rule_objects;
367 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
374 while ( my $rule = $self->next ){
381 Clone a set of circulation rules to another branch
386 my ( $self, $to_branch ) = @_;
388 while ( my $rule = $self->next ){
389 $rule->clone($to_branch);
393 =head3 get_opacitemholds_policy
395 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
397 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
398 and the "Item level holds" (opacitemholds).
399 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
403 sub get_opacitemholds_policy {
404 my ( $class, $params ) = @_;
406 my $item = $params->{item};
407 my $patron = $params->{patron};
409 return unless $item or $patron;
411 my $rule = Koha::CirculationRules->get_effective_rule(
413 categorycode => $patron->categorycode,
414 itemtype => $item->effective_itemtype,
415 branchcode => $item->homebranch,
416 rule_name => 'opacitemholds',
420 return $rule ? $rule->rule_value : undef;
423 =head3 get_onshelfholds_policy
425 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
429 sub get_onshelfholds_policy {
430 my ( $class, $params ) = @_;
431 my $item = $params->{item};
432 my $itemtype = $item->effective_itemtype;
433 my $patron = $params->{patron};
434 my $rule = Koha::CirculationRules->get_effective_rule(
436 categorycode => ( $patron ? $patron->categorycode : undef ),
437 itemtype => $itemtype,
438 branchcode => $item->holdingbranch,
439 rule_name => 'onshelfholds',
442 return $rule ? $rule->rule_value : 0;
445 =head3 get_lostreturn_policy
447 my $lostrefund_policy = Koha::CirculationRules->get_lostreturn_policy( { return_branch => $return_branch, item => $item } );
453 =item '0' - Do not refund
455 =item 'refund' - Refund the lost item charge
457 =item 'restore' - Refund the lost item charge and restore the original overdue fine
459 =item 'charge' - Refund the lost item charge and charge a new overdue fine
465 sub get_lostreturn_policy {
466 my ( $class, $params ) = @_;
468 my $item = $params->{item};
470 my $behaviour = C4::Context->preference( 'RefundLostOnReturnControl' ) // 'CheckinLibrary';
471 my $behaviour_mapping = {
472 CheckinLibrary => $params->{'return_branch'} // $item->homebranch,
473 ItemHomeBranch => $item->homebranch,
474 ItemHoldingBranch => $item->holdingbranch
477 my $branch = $behaviour_mapping->{ $behaviour };
479 my $rule = Koha::CirculationRules->get_effective_rule(
481 branchcode => $branch,
482 rule_name => 'lostreturn',
486 return $rule ? $rule->rule_value : 'refund';
489 =head3 article_requestable_rules
491 Return rules that allow article requests, optionally filtered by
494 Use with care; see guess_article_requestable_itemtypes.
498 sub article_requestable_rules {
499 my ( $class, $params ) = @_;
500 my $category = $params->{categorycode};
502 return if !C4::Context->preference('ArticleRequests');
503 return $class->search({
504 $category ? ( categorycode => [ $category, undef ] ) : (),
505 rule_name => 'article_requests',
506 rule_value => { '!=' => 'no' },
510 =head3 guess_article_requestable_itemtypes
512 Return item types in a hashref that are likely possible to be
513 'article requested'. Constructed by an intelligent guess in the
514 issuing rules (see article_requestable_rules).
516 Note: pref ArticleRequestsLinkControl overrides the algorithm.
518 Optional parameters: categorycode.
520 Note: the routine is used in opac-search to obtain a reasonable
521 estimate within performance borders (not looking at all items but
522 just using default itemtype). Also we are not looking at the
523 branchcode here, since home or holding branch of the item is
524 leading and branch may be unknown too (anonymous opac session).
528 sub guess_article_requestable_itemtypes {
529 my ( $class, $params ) = @_;
530 my $category = $params->{categorycode};
531 return {} if !C4::Context->preference('ArticleRequests');
532 return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
534 my $cache = Koha::Caches->get_instance;
535 my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
536 my $key = $category || '*';
537 return $last_article_requestable_guesses->{$key}
538 if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
541 my $rules = $class->article_requestable_rules({
542 $category ? ( categorycode => $category ) : (),
544 return $res if !$rules;
545 foreach my $rule ( $rules->as_list ) {
546 $res->{ $rule->itemtype // '*' } = 1;
548 $last_article_requestable_guesses->{$key} = $res;
549 $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
553 =head3 get_daysmode_effective_value
555 Return the value for daysmode defined in the circulation rules.
556 If not defined (or empty string), the value of the system preference useDaysMode is returned
560 sub get_effective_daysmode {
561 my ( $class, $params ) = @_;
563 my $categorycode = $params->{categorycode};
564 my $itemtype = $params->{itemtype};
565 my $branchcode = $params->{branchcode};
567 my $daysmode_rule = $class->get_effective_rule(
569 categorycode => $categorycode,
570 itemtype => $itemtype,
571 branchcode => $branchcode,
572 rule_name => 'daysmode',
576 return ( defined($daysmode_rule)
577 and $daysmode_rule->rule_value ne '' )
578 ? $daysmode_rule->rule_value
579 : C4::Context->preference('useDaysMode');
589 return 'CirculationRule';
597 return 'Koha::CirculationRule';