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 article_request_fee => {
80 scope => [ 'branchcode', 'categorycode' ],
82 open_article_requests_limit => {
83 scope => [ 'branchcode', 'categorycode' ],
87 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
89 cap_fine_to_replacement_price => {
90 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
93 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
95 chargeperiod_charge_at => {
96 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
99 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
102 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
105 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
108 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
110 hardduedatecompare => {
111 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
114 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
116 holds_per_record => {
117 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
120 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
123 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
126 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
129 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
131 maxonsiteissueqty => {
132 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
134 maxsuspensiondays => {
135 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
137 no_auto_renewal_after => {
138 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
140 no_auto_renewal_after_hard_limit => {
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' ],
159 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
161 unseen_renewals_allowed => {
162 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
165 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
169 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
171 suspension_chargeperiod => {
172 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
174 note => { # This is not really a rule. Maybe we will want to separate this later.
175 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
177 decreaseloanholds => {
178 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
180 # Not included (deprecated?):
190 =head3 get_effective_rule
194 sub get_effective_rule {
195 my ( $self, $params ) = @_;
197 $params->{categorycode} //= undef;
198 $params->{branchcode} //= undef;
199 $params->{itemtype} //= undef;
201 my $rule_name = $params->{rule_name};
202 my $categorycode = $params->{categorycode};
203 my $itemtype = $params->{itemtype};
204 my $branchcode = $params->{branchcode};
206 Koha::Exceptions::MissingParameter->throw(
207 "Required parameter 'rule_name' missing")
210 for my $v ( $branchcode, $categorycode, $itemtype ) {
211 $v = undef if $v and $v eq '*';
214 my $order_by = $params->{order_by}
215 // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
218 $search_params->{rule_name} = $rule_name;
220 $search_params->{categorycode} = defined $categorycode ? [ $categorycode, undef ] : undef;
221 $search_params->{itemtype} = defined $itemtype ? [ $itemtype, undef ] : undef;
222 $search_params->{branchcode} = defined $branchcode ? [ $branchcode, undef ] : undef;
224 my $rule = $self->search(
227 order_by => $order_by,
235 =head3 get_effective_rules
239 sub get_effective_rules {
240 my ( $self, $params ) = @_;
242 my $rules = $params->{rules};
243 my $categorycode = $params->{categorycode};
244 my $itemtype = $params->{itemtype};
245 my $branchcode = $params->{branchcode};
248 foreach my $rule (@$rules) {
249 my $effective_rule = $self->get_effective_rule(
252 categorycode => $categorycode,
253 itemtype => $itemtype,
254 branchcode => $branchcode,
258 $r->{$rule} = $effective_rule->rule_value if $effective_rule;
269 my ( $self, $params ) = @_;
271 for my $mandatory_parameter (qw( rule_name rule_value ) ) {
272 Koha::Exceptions::MissingParameter->throw(
273 "Required parameter '$mandatory_parameter' missing")
274 unless exists $params->{$mandatory_parameter};
277 my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
278 Koha::Exceptions::MissingParameter->throw(
279 "set_rule given unknown rule '$params->{rule_name}'!")
280 unless defined $kind_info;
282 # Enforce scope; a rule should be set for its defined scope, no more, no less.
283 foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
284 if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
285 croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
286 unless exists $params->{$scope_level};
288 croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
289 if exists $params->{$scope_level};
293 my $branchcode = $params->{branchcode};
294 my $categorycode = $params->{categorycode};
295 my $itemtype = $params->{itemtype};
296 my $rule_name = $params->{rule_name};
297 my $rule_value = $params->{rule_value};
298 my $can_be_blank = defined $kind_info->{can_be_blank} ? $kind_info->{can_be_blank} : 1;
299 $rule_value = undef if defined $rule_value && $rule_value eq "" && !$can_be_blank;
301 for my $v ( $branchcode, $categorycode, $itemtype ) {
302 $v = undef if $v and $v eq '*';
304 my $rule = $self->search(
306 rule_name => $rule_name,
307 branchcode => $branchcode,
308 categorycode => $categorycode,
309 itemtype => $itemtype,
314 if ( defined $rule_value ) {
315 $rule->rule_value($rule_value);
323 if ( defined $rule_value ) {
324 $rule = Koha::CirculationRule->new(
326 branchcode => $branchcode,
327 categorycode => $categorycode,
328 itemtype => $itemtype,
329 rule_name => $rule_name,
330 rule_value => $rule_value,
345 my ( $self, $params ) = @_;
348 $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
349 $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
350 $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
351 my $rules = $params->{rules};
353 my $rule_objects = [];
354 while ( my ( $rule_name, $rule_value ) = each %$rules ) {
355 my $rule_object = Koha::CirculationRules->set_rule(
358 rule_name => $rule_name,
359 rule_value => $rule_value,
362 push( @$rule_objects, $rule_object );
365 return $rule_objects;
370 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
377 while ( my $rule = $self->next ){
384 Clone a set of circulation rules to another branch
389 my ( $self, $to_branch ) = @_;
391 while ( my $rule = $self->next ){
392 $rule->clone($to_branch);
396 =head3 get_opacitemholds_policy
398 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
400 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
401 and the "Item level holds" (opacitemholds).
402 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
406 sub get_opacitemholds_policy {
407 my ( $class, $params ) = @_;
409 my $item = $params->{item};
410 my $patron = $params->{patron};
412 return unless $item or $patron;
414 my $rule = Koha::CirculationRules->get_effective_rule(
416 categorycode => $patron->categorycode,
417 itemtype => $item->effective_itemtype,
418 branchcode => $item->homebranch,
419 rule_name => 'opacitemholds',
423 return $rule ? $rule->rule_value : undef;
426 =head3 get_onshelfholds_policy
428 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
432 sub get_onshelfholds_policy {
433 my ( $class, $params ) = @_;
434 my $item = $params->{item};
435 my $itemtype = $item->effective_itemtype;
436 my $patron = $params->{patron};
437 my $rule = Koha::CirculationRules->get_effective_rule(
439 categorycode => ( $patron ? $patron->categorycode : undef ),
440 itemtype => $itemtype,
441 branchcode => $item->holdingbranch,
442 rule_name => 'onshelfholds',
445 return $rule ? $rule->rule_value : 0;
448 =head3 get_lostreturn_policy
450 my $lostrefund_policy = Koha::CirculationRules->get_lostreturn_policy( { return_branch => $return_branch, item => $item } );
456 =item '0' - Do not refund
458 =item 'refund' - Refund the lost item charge
460 =item 'restore' - Refund the lost item charge and restore the original overdue fine
462 =item 'charge' - Refund the lost item charge and charge a new overdue fine
468 sub get_lostreturn_policy {
469 my ( $class, $params ) = @_;
471 my $item = $params->{item};
473 my $behaviour = C4::Context->preference( 'RefundLostOnReturnControl' ) // 'CheckinLibrary';
474 my $behaviour_mapping = {
475 CheckinLibrary => $params->{'return_branch'} // $item->homebranch,
476 ItemHomeBranch => $item->homebranch,
477 ItemHoldingBranch => $item->holdingbranch
480 my $branch = $behaviour_mapping->{ $behaviour };
482 my $rule = Koha::CirculationRules->get_effective_rule(
484 branchcode => $branch,
485 rule_name => 'lostreturn',
489 return $rule ? $rule->rule_value : 'refund';
492 =head3 article_requestable_rules
494 Return rules that allow article requests, optionally filtered by
497 Use with care; see guess_article_requestable_itemtypes.
501 sub article_requestable_rules {
502 my ( $class, $params ) = @_;
503 my $category = $params->{categorycode};
505 return if !C4::Context->preference('ArticleRequests');
506 return $class->search({
507 $category ? ( categorycode => [ $category, undef ] ) : (),
508 rule_name => 'article_requests',
509 rule_value => { '!=' => 'no' },
513 =head3 guess_article_requestable_itemtypes
515 Return item types in a hashref that are likely possible to be
516 'article requested'. Constructed by an intelligent guess in the
517 issuing rules (see article_requestable_rules).
519 Note: pref ArticleRequestsLinkControl overrides the algorithm.
521 Optional parameters: categorycode.
523 Note: the routine is used in opac-search to obtain a reasonable
524 estimate within performance borders (not looking at all items but
525 just using default itemtype). Also we are not looking at the
526 branchcode here, since home or holding branch of the item is
527 leading and branch may be unknown too (anonymous opac session).
531 sub guess_article_requestable_itemtypes {
532 my ( $class, $params ) = @_;
533 my $category = $params->{categorycode};
534 return {} if !C4::Context->preference('ArticleRequests');
535 return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
537 my $cache = Koha::Caches->get_instance;
538 my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
539 my $key = $category || '*';
540 return $last_article_requestable_guesses->{$key}
541 if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
544 my $rules = $class->article_requestable_rules({
545 $category ? ( categorycode => $category ) : (),
547 return $res if !$rules;
548 foreach my $rule ( $rules->as_list ) {
549 $res->{ $rule->itemtype // '*' } = 1;
551 $last_article_requestable_guesses->{$key} = $res;
552 $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
556 =head3 get_daysmode_effective_value
558 Return the value for daysmode defined in the circulation rules.
559 If not defined (or empty string), the value of the system preference useDaysMode is returned
563 sub get_effective_daysmode {
564 my ( $class, $params ) = @_;
566 my $categorycode = $params->{categorycode};
567 my $itemtype = $params->{itemtype};
568 my $branchcode = $params->{branchcode};
570 my $daysmode_rule = $class->get_effective_rule(
572 categorycode => $categorycode,
573 itemtype => $itemtype,
574 branchcode => $branchcode,
575 rule_name => 'daysmode',
579 return ( defined($daysmode_rule)
580 and $daysmode_rule->rule_value ne '' )
581 ? $daysmode_rule->rule_value
582 : C4::Context->preference('useDaysMode');
592 return 'CirculationRule';
600 return 'Koha::CirculationRule';