Bug 26529: Remove warning
[koha.git] / Koha / CirculationRules.pm
blob743d8f1ab37c1218de94100af081e2289c9da22c
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>.
20 use Modern::Perl;
21 use Carp qw(croak);
23 use Koha::Exceptions;
24 use Koha::CirculationRule;
26 use base qw(Koha::Objects);
28 use constant GUESSED_ITEMTYPES_KEY => 'Koha_IssuingRules_last_guess';
30 =head1 NAME
32 Koha::CirculationRules - Koha CirculationRule Object set class
34 =head1 API
36 =head2 Class Methods
38 =cut
40 =head3 rule_kinds
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.
46 =cut
48 our $RULE_KINDS = {
49 refund => {
50 scope => [ 'branchcode' ],
53 patron_maxissueqty => {
54 scope => [ 'branchcode', 'categorycode' ],
56 patron_maxonsiteissueqty => {
57 scope => [ 'branchcode', 'categorycode' ],
59 max_holds => {
60 scope => [ 'branchcode', 'categorycode' ],
63 holdallowed => {
64 scope => [ 'branchcode', 'itemtype' ],
65 can_be_blank => 0,
67 hold_fulfillment_policy => {
68 scope => [ 'branchcode', 'itemtype' ],
69 can_be_blank => 0,
71 returnbranch => {
72 scope => [ 'branchcode', 'itemtype' ],
73 can_be_blank => 0,
76 article_requests => {
77 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
79 auto_renew => {
80 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
82 cap_fine_to_replacement_price => {
83 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
85 chargeperiod => {
86 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
88 chargeperiod_charge_at => {
89 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
91 fine => {
92 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
94 finedays => {
95 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
97 firstremind => {
98 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
100 hardduedate => {
101 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
103 hardduedatecompare => {
104 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
106 holds_per_day => {
107 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
109 holds_per_record => {
110 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
112 issuelength => {
113 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
115 daysmode => {
116 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
118 lengthunit => {
119 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
121 maxissueqty => {
122 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
124 maxonsiteissueqty => {
125 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
127 maxsuspensiondays => {
128 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
130 no_auto_renewal_after => {
131 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
133 no_auto_renewal_after_hard_limit => {
134 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
136 norenewalbefore => {
137 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
139 onshelfholds => {
140 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
142 opacitemholds => {
143 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
145 overduefinescap => {
146 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
148 renewalperiod => {
149 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
151 renewalsallowed => {
152 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
154 rentaldiscount => {
155 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
157 reservesallowed => {
158 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
160 suspension_chargeperiod => {
161 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
163 note => { # This is not really a rule. Maybe we will want to separate this later.
164 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
166 # Not included (deprecated?):
167 # * accountsent
168 # * reservecharge
169 # * restrictedtype
172 sub rule_kinds {
173 return $RULE_KINDS;
176 =head3 get_effective_rule
178 =cut
180 sub get_effective_rule {
181 my ( $self, $params ) = @_;
183 $params->{categorycode} //= undef;
184 $params->{branchcode} //= undef;
185 $params->{itemtype} //= undef;
187 my $rule_name = $params->{rule_name};
188 my $categorycode = $params->{categorycode};
189 my $itemtype = $params->{itemtype};
190 my $branchcode = $params->{branchcode};
192 Koha::Exceptions::MissingParameter->throw(
193 "Required parameter 'rule_name' missing")
194 unless $rule_name;
196 for my $v ( $branchcode, $categorycode, $itemtype ) {
197 $v = undef if $v and $v eq '*';
200 my $order_by = $params->{order_by}
201 // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
203 my $search_params;
204 $search_params->{rule_name} = $rule_name;
206 $search_params->{categorycode} = defined $categorycode ? [ $categorycode, undef ] : undef;
207 $search_params->{itemtype} = defined $itemtype ? [ $itemtype, undef ] : undef;
208 $search_params->{branchcode} = defined $branchcode ? [ $branchcode, undef ] : undef;
210 my $rule = $self->search(
211 $search_params,
213 order_by => $order_by,
214 rows => 1,
216 )->single;
218 return $rule;
221 =head3 get_effective_rules
223 =cut
225 sub get_effective_rules {
226 my ( $self, $params ) = @_;
228 my $rules = $params->{rules};
229 my $categorycode = $params->{categorycode};
230 my $itemtype = $params->{itemtype};
231 my $branchcode = $params->{branchcode};
233 my $r;
234 foreach my $rule (@$rules) {
235 my $effective_rule = $self->get_effective_rule(
237 rule_name => $rule,
238 categorycode => $categorycode,
239 itemtype => $itemtype,
240 branchcode => $branchcode,
244 $r->{$rule} = $effective_rule->rule_value if $effective_rule;
247 return $r;
250 =head3 set_rule
252 =cut
254 sub set_rule {
255 my ( $self, $params ) = @_;
257 for my $mandatory_parameter (qw( rule_name rule_value ) ) {
258 Koha::Exceptions::MissingParameter->throw(
259 "Required parameter '$mandatory_parameter' missing")
260 unless exists $params->{$mandatory_parameter};
263 my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
264 Koha::Exceptions::MissingParameter->throw(
265 "set_rule given unknown rule '$params->{rule_name}'!")
266 unless defined $kind_info;
268 # Enforce scope; a rule should be set for its defined scope, no more, no less.
269 foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
270 if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
271 croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
272 unless exists $params->{$scope_level};
273 } else {
274 croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
275 if exists $params->{$scope_level};
279 my $branchcode = $params->{branchcode};
280 my $categorycode = $params->{categorycode};
281 my $itemtype = $params->{itemtype};
282 my $rule_name = $params->{rule_name};
283 my $rule_value = $params->{rule_value};
284 my $can_be_blank = defined $kind_info->{can_be_blank} ? $kind_info->{can_be_blank} : 1;
285 $rule_value = undef if $rule_value && $rule_value eq "" && !$can_be_blank;
287 for my $v ( $branchcode, $categorycode, $itemtype ) {
288 $v = undef if $v and $v eq '*';
290 my $rule = $self->search(
292 rule_name => $rule_name,
293 branchcode => $branchcode,
294 categorycode => $categorycode,
295 itemtype => $itemtype,
297 )->next();
299 if ($rule) {
300 if ( defined $rule_value ) {
301 $rule->rule_value($rule_value);
302 $rule->update();
304 else {
305 $rule->delete();
308 else {
309 if ( defined $rule_value ) {
310 $rule = Koha::CirculationRule->new(
312 branchcode => $branchcode,
313 categorycode => $categorycode,
314 itemtype => $itemtype,
315 rule_name => $rule_name,
316 rule_value => $rule_value,
319 $rule->store();
323 return $rule;
326 =head3 set_rules
328 =cut
330 sub set_rules {
331 my ( $self, $params ) = @_;
333 my %set_params;
334 $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
335 $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
336 $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
337 my $rules = $params->{rules};
339 my $rule_objects = [];
340 while ( my ( $rule_name, $rule_value ) = each %$rules ) {
341 my $rule_object = Koha::CirculationRules->set_rule(
343 %set_params,
344 rule_name => $rule_name,
345 rule_value => $rule_value,
348 push( @$rule_objects, $rule_object );
351 return $rule_objects;
354 =head3 delete
356 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
358 =cut
360 sub delete {
361 my ( $self ) = @_;
363 while ( my $rule = $self->next ){
364 $rule->delete;
368 =head3 clone
370 Clone a set of circulation rules to another branch
372 =cut
374 sub clone {
375 my ( $self, $to_branch ) = @_;
377 while ( my $rule = $self->next ){
378 $rule->clone($to_branch);
382 =head3 get_opacitemholds_policy
384 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
386 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
387 and the "Item level holds" (opacitemholds).
388 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
390 =cut
392 sub get_opacitemholds_policy {
393 my ( $class, $params ) = @_;
395 my $item = $params->{item};
396 my $patron = $params->{patron};
398 return unless $item or $patron;
400 my $rule = Koha::CirculationRules->get_effective_rule(
402 categorycode => $patron->categorycode,
403 itemtype => $item->effective_itemtype,
404 branchcode => $item->homebranch,
405 rule_name => 'opacitemholds',
409 return $rule ? $rule->rule_value : undef;
412 =head3 get_onshelfholds_policy
414 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
416 =cut
418 sub get_onshelfholds_policy {
419 my ( $class, $params ) = @_;
420 my $item = $params->{item};
421 my $itemtype = $item->effective_itemtype;
422 my $patron = $params->{patron};
423 my $rule = Koha::CirculationRules->get_effective_rule(
425 categorycode => ( $patron ? $patron->categorycode : undef ),
426 itemtype => $itemtype,
427 branchcode => $item->holdingbranch,
428 rule_name => 'onshelfholds',
431 return $rule ? $rule->rule_value : 0;
434 =head3 get_lostreturn_policy
436 my $refund = Koha::CirculationRules->get_lostreturn_policy( { return_branch => $return_branch, item => $item } );
438 =cut
440 sub get_lostreturn_policy {
441 my ( $class, $params ) = @_;
443 my $item = $params->{item};
445 my $behaviour = C4::Context->preference( 'RefundLostOnReturnControl' ) // 'CheckinLibrary';
446 my $behaviour_mapping = {
447 CheckinLibrary => $params->{'return_branch'} // $item->homebranch,
448 ItemHomeBranch => $item->homebranch,
449 ItemHoldingBranch => $item->holdingbranch
452 my $branch = $behaviour_mapping->{ $behaviour };
454 my $rule = Koha::CirculationRules->get_effective_rule(
456 branchcode => $branch,
457 rule_name => 'refund',
461 return $rule ? $rule->rule_value : 1;
464 =head3 article_requestable_rules
466 Return rules that allow article requests, optionally filtered by
467 patron categorycode.
469 Use with care; see guess_article_requestable_itemtypes.
471 =cut
473 sub article_requestable_rules {
474 my ( $class, $params ) = @_;
475 my $category = $params->{categorycode};
477 return if !C4::Context->preference('ArticleRequests');
478 return $class->search({
479 $category ? ( categorycode => [ $category, undef ] ) : (),
480 rule_name => 'article_requests',
481 rule_value => { '!=' => 'no' },
485 =head3 guess_article_requestable_itemtypes
487 Return item types in a hashref that are likely possible to be
488 'article requested'. Constructed by an intelligent guess in the
489 issuing rules (see article_requestable_rules).
491 Note: pref ArticleRequestsLinkControl overrides the algorithm.
493 Optional parameters: categorycode.
495 Note: the routine is used in opac-search to obtain a reasonable
496 estimate within performance borders (not looking at all items but
497 just using default itemtype). Also we are not looking at the
498 branchcode here, since home or holding branch of the item is
499 leading and branch may be unknown too (anonymous opac session).
501 =cut
503 sub guess_article_requestable_itemtypes {
504 my ( $class, $params ) = @_;
505 my $category = $params->{categorycode};
506 return {} if !C4::Context->preference('ArticleRequests');
507 return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
509 my $cache = Koha::Caches->get_instance;
510 my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
511 my $key = $category || '*';
512 return $last_article_requestable_guesses->{$key}
513 if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
515 my $res = {};
516 my $rules = $class->article_requestable_rules({
517 $category ? ( categorycode => $category ) : (),
519 return $res if !$rules;
520 foreach my $rule ( $rules->as_list ) {
521 $res->{ $rule->itemtype // '*' } = 1;
523 $last_article_requestable_guesses->{$key} = $res;
524 $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
525 return $res;
528 =head3 get_daysmode_effective_value
530 Return the value for daysmode defined in the circulation rules.
531 If not defined (or empty string), the value of the system preference useDaysMode is returned
533 =cut
535 sub get_effective_daysmode {
536 my ( $class, $params ) = @_;
538 my $categorycode = $params->{categorycode};
539 my $itemtype = $params->{itemtype};
540 my $branchcode = $params->{branchcode};
542 my $daysmode_rule = $class->get_effective_rule(
544 categorycode => $categorycode,
545 itemtype => $itemtype,
546 branchcode => $branchcode,
547 rule_name => 'daysmode',
551 return ( defined($daysmode_rule)
552 and $daysmode_rule->rule_value ne '' )
553 ? $daysmode_rule->rule_value
554 : C4::Context->preference('useDaysMode');
559 =head3 type
561 =cut
563 sub _type {
564 return 'CirculationRule';
567 =head3 object_class
569 =cut
571 sub object_class {
572 return 'Koha::CirculationRule';