Bug 23091: (QA follow-up) POD fixes
[koha.git] / Koha / CirculationRules.pm
1 package Koha::CirculationRules;
2
3 # Copyright ByWater Solutions 2017
4 #
5 # This file is part of Koha.
6 #
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.
11 #
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.
16 #
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>.
19
20 use Modern::Perl;
21 use Carp qw(croak);
22
23 use Koha::Exceptions;
24 use Koha::CirculationRule;
25
26 use base qw(Koha::Objects);
27
28 use constant GUESSED_ITEMTYPES_KEY => 'Koha_IssuingRules_last_guess';
29
30 =head1 NAME
31
32 Koha::CirculationRules - Koha CirculationRule Object set class
33
34 =head1 API
35
36 =head2 Class Methods
37
38 =cut
39
40 =head3 rule_kinds
41
42 This structure describes the possible rules that may be set, and what scopes they can be set at.
43
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.
45
46 =cut
47
48 our $RULE_KINDS = {
49     lostreturn => {
50         scope => [ 'branchcode' ],
51     },
52
53     patron_maxissueqty => {
54         scope => [ 'branchcode', 'categorycode' ],
55     },
56     patron_maxonsiteissueqty => {
57         scope => [ 'branchcode', 'categorycode' ],
58     },
59     max_holds => {
60         scope => [ 'branchcode', 'categorycode' ],
61     },
62
63     holdallowed => {
64         scope => [ 'branchcode', 'itemtype' ],
65         can_be_blank => 0,
66     },
67     hold_fulfillment_policy => {
68         scope => [ 'branchcode', 'itemtype' ],
69         can_be_blank => 0,
70     },
71     returnbranch => {
72         scope => [ 'branchcode', 'itemtype' ],
73         can_be_blank => 0,
74     },
75
76     article_requests => {
77         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
78     },
79     auto_renew => {
80         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
81     },
82     cap_fine_to_replacement_price => {
83         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
84     },
85     chargeperiod => {
86         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
87     },
88     chargeperiod_charge_at => {
89         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
90     },
91     fine => {
92         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
93     },
94     finedays => {
95         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
96     },
97     firstremind => {
98         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
99     },
100     hardduedate => {
101         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
102     },
103     hardduedatecompare => {
104         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
105     },
106     holds_per_day => {
107         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
108     },
109     holds_per_record => {
110         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
111     },
112     issuelength => {
113         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
114     },
115     daysmode => {
116         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
117     },
118     lengthunit => {
119         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
120     },
121     maxissueqty => {
122         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
123     },
124     maxonsiteissueqty => {
125         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
126     },
127     maxsuspensiondays => {
128         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
129     },
130     no_auto_renewal_after => {
131         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
132     },
133     no_auto_renewal_after_hard_limit => {
134         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
135     },
136     norenewalbefore => {
137         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
138     },
139     onshelfholds => {
140         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
141     },
142     opacitemholds => {
143         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
144     },
145     overduefinescap => {
146         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
147     },
148     renewalperiod => {
149         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
150     },
151     renewalsallowed => {
152         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
153     },
154     rentaldiscount => {
155         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
156     },
157     reservesallowed => {
158         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
159     },
160     suspension_chargeperiod => {
161         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
162     },
163     note => { # This is not really a rule. Maybe we will want to separate this later.
164         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
165     },
166     # Not included (deprecated?):
167     #   * accountsent
168     #   * reservecharge
169     #   * restrictedtype
170 };
171
172 sub rule_kinds {
173     return $RULE_KINDS;
174 }
175
176 =head3 get_effective_rule
177
178 =cut
179
180 sub get_effective_rule {
181     my ( $self, $params ) = @_;
182
183     $params->{categorycode} //= undef;
184     $params->{branchcode}   //= undef;
185     $params->{itemtype}     //= undef;
186
187     my $rule_name    = $params->{rule_name};
188     my $categorycode = $params->{categorycode};
189     my $itemtype     = $params->{itemtype};
190     my $branchcode   = $params->{branchcode};
191
192     Koha::Exceptions::MissingParameter->throw(
193         "Required parameter 'rule_name' missing")
194       unless $rule_name;
195
196     for my $v ( $branchcode, $categorycode, $itemtype ) {
197         $v = undef if $v and $v eq '*';
198     }
199
200     my $order_by = $params->{order_by}
201       // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
202
203     my $search_params;
204     $search_params->{rule_name} = $rule_name;
205
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;
209
210     my $rule = $self->search(
211         $search_params,
212         {
213             order_by => $order_by,
214             rows => 1,
215         }
216     )->single;
217
218     return $rule;
219 }
220
221 =head3 get_effective_rules
222
223 =cut
224
225 sub get_effective_rules {
226     my ( $self, $params ) = @_;
227
228     my $rules        = $params->{rules};
229     my $categorycode = $params->{categorycode};
230     my $itemtype     = $params->{itemtype};
231     my $branchcode   = $params->{branchcode};
232
233     my $r;
234     foreach my $rule (@$rules) {
235         my $effective_rule = $self->get_effective_rule(
236             {
237                 rule_name    => $rule,
238                 categorycode => $categorycode,
239                 itemtype     => $itemtype,
240                 branchcode   => $branchcode,
241             }
242         );
243
244         $r->{$rule} = $effective_rule->rule_value if $effective_rule;
245     }
246
247     return $r;
248 }
249
250 =head3 set_rule
251
252 =cut
253
254 sub set_rule {
255     my ( $self, $params ) = @_;
256
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};
261     }
262
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;
267
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};
276         }
277     }
278
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 defined $rule_value && $rule_value eq "" && !$can_be_blank;
286
287     for my $v ( $branchcode, $categorycode, $itemtype ) {
288         $v = undef if $v and $v eq '*';
289     }
290     my $rule = $self->search(
291         {
292             rule_name    => $rule_name,
293             branchcode   => $branchcode,
294             categorycode => $categorycode,
295             itemtype     => $itemtype,
296         }
297     )->next();
298
299     if ($rule) {
300         if ( defined $rule_value ) {
301             $rule->rule_value($rule_value);
302             $rule->update();
303         }
304         else {
305             $rule->delete();
306         }
307     }
308     else {
309         if ( defined $rule_value ) {
310             $rule = Koha::CirculationRule->new(
311                 {
312                     branchcode   => $branchcode,
313                     categorycode => $categorycode,
314                     itemtype     => $itemtype,
315                     rule_name    => $rule_name,
316                     rule_value   => $rule_value,
317                 }
318             );
319             $rule->store();
320         }
321     }
322
323     return $rule;
324 }
325
326 =head3 set_rules
327
328 =cut
329
330 sub set_rules {
331     my ( $self, $params ) = @_;
332
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};
338
339     my $rule_objects = [];
340     while ( my ( $rule_name, $rule_value ) = each %$rules ) {
341         my $rule_object = Koha::CirculationRules->set_rule(
342             {
343                 %set_params,
344                 rule_name    => $rule_name,
345                 rule_value   => $rule_value,
346             }
347         );
348         push( @$rule_objects, $rule_object );
349     }
350
351     return $rule_objects;
352 }
353
354 =head3 delete
355
356 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
357
358 =cut
359
360 sub delete {
361     my ( $self ) = @_;
362
363     while ( my $rule = $self->next ){
364         $rule->delete;
365     }
366 }
367
368 =head3 clone
369
370 Clone a set of circulation rules to another branch
371
372 =cut
373
374 sub clone {
375     my ( $self, $to_branch ) = @_;
376
377     while ( my $rule = $self->next ){
378         $rule->clone($to_branch);
379     }
380 }
381
382 =head3 get_opacitemholds_policy
383
384 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
385
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
389
390 =cut
391
392 sub get_opacitemholds_policy {
393     my ( $class, $params ) = @_;
394
395     my $item   = $params->{item};
396     my $patron = $params->{patron};
397
398     return unless $item or $patron;
399
400     my $rule = Koha::CirculationRules->get_effective_rule(
401         {
402             categorycode => $patron->categorycode,
403             itemtype     => $item->effective_itemtype,
404             branchcode   => $item->homebranch,
405             rule_name    => 'opacitemholds',
406         }
407     );
408
409     return $rule ? $rule->rule_value : undef;
410 }
411
412 =head3 get_onshelfholds_policy
413
414     my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
415
416 =cut
417
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(
424         {
425             categorycode => ( $patron ? $patron->categorycode : undef ),
426             itemtype     => $itemtype,
427             branchcode   => $item->holdingbranch,
428             rule_name    => 'onshelfholds',
429         }
430     );
431     return $rule ? $rule->rule_value : 0;
432 }
433
434 =head3 get_lostreturn_policy
435
436   my $lostrefund_policy = Koha::CirculationRules->get_lostreturn_policy( { return_branch => $return_branch, item => $item } );
437
438 Return values are:
439
440 =over 2
441
442 =item '0' - Do not refund
443
444 =item 'refund' - Refund the lost item charge
445
446 =item 'restore' - Refund the lost item charge and restore the original overdue fine
447
448 =item 'charge' - Refund the lost item charge and charge a new overdue fine
449
450 =back
451
452 =cut
453
454 sub get_lostreturn_policy {
455     my ( $class, $params ) = @_;
456
457     my $item   = $params->{item};
458
459     my $behaviour = C4::Context->preference( 'RefundLostOnReturnControl' ) // 'CheckinLibrary';
460     my $behaviour_mapping = {
461         CheckinLibrary    => $params->{'return_branch'} // $item->homebranch,
462         ItemHomeBranch    => $item->homebranch,
463         ItemHoldingBranch => $item->holdingbranch
464     };
465
466     my $branch = $behaviour_mapping->{ $behaviour };
467
468     my $rule = Koha::CirculationRules->get_effective_rule(
469         {
470             branchcode => $branch,
471             rule_name  => 'lostreturn',
472         }
473     );
474
475     return $rule ? $rule->rule_value : 'refund';
476 }
477
478 =head3 article_requestable_rules
479
480     Return rules that allow article requests, optionally filtered by
481     patron categorycode.
482
483     Use with care; see guess_article_requestable_itemtypes.
484
485 =cut
486
487 sub article_requestable_rules {
488     my ( $class, $params ) = @_;
489     my $category = $params->{categorycode};
490
491     return if !C4::Context->preference('ArticleRequests');
492     return $class->search({
493         $category ? ( categorycode => [ $category, undef ] ) : (),
494         rule_name => 'article_requests',
495         rule_value => { '!=' => 'no' },
496     });
497 }
498
499 =head3 guess_article_requestable_itemtypes
500
501     Return item types in a hashref that are likely possible to be
502     'article requested'. Constructed by an intelligent guess in the
503     issuing rules (see article_requestable_rules).
504
505     Note: pref ArticleRequestsLinkControl overrides the algorithm.
506
507     Optional parameters: categorycode.
508
509     Note: the routine is used in opac-search to obtain a reasonable
510     estimate within performance borders (not looking at all items but
511     just using default itemtype). Also we are not looking at the
512     branchcode here, since home or holding branch of the item is
513     leading and branch may be unknown too (anonymous opac session).
514
515 =cut
516
517 sub guess_article_requestable_itemtypes {
518     my ( $class, $params ) = @_;
519     my $category = $params->{categorycode};
520     return {} if !C4::Context->preference('ArticleRequests');
521     return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
522
523     my $cache = Koha::Caches->get_instance;
524     my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
525     my $key = $category || '*';
526     return $last_article_requestable_guesses->{$key}
527         if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
528
529     my $res = {};
530     my $rules = $class->article_requestable_rules({
531         $category ? ( categorycode => $category ) : (),
532     });
533     return $res if !$rules;
534     foreach my $rule ( $rules->as_list ) {
535         $res->{ $rule->itemtype // '*' } = 1;
536     }
537     $last_article_requestable_guesses->{$key} = $res;
538     $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
539     return $res;
540 }
541
542 =head3 get_daysmode_effective_value
543
544 Return the value for daysmode defined in the circulation rules.
545 If not defined (or empty string), the value of the system preference useDaysMode is returned
546
547 =cut
548
549 sub get_effective_daysmode {
550     my ( $class, $params ) = @_;
551
552     my $categorycode     = $params->{categorycode};
553     my $itemtype         = $params->{itemtype};
554     my $branchcode       = $params->{branchcode};
555
556     my $daysmode_rule = $class->get_effective_rule(
557         {
558             categorycode => $categorycode,
559             itemtype     => $itemtype,
560             branchcode   => $branchcode,
561             rule_name    => 'daysmode',
562         }
563     );
564
565     return ( defined($daysmode_rule)
566           and $daysmode_rule->rule_value ne '' )
567       ? $daysmode_rule->rule_value
568       : C4::Context->preference('useDaysMode');
569
570 }
571
572
573 =head3 type
574
575 =cut
576
577 sub _type {
578     return 'CirculationRule';
579 }
580
581 =head3 object_class
582
583 =cut
584
585 sub object_class {
586     return 'Koha::CirculationRule';
587 }
588
589 1;