Bug 29886: Add Koha::Suggestions->search_limited
[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     article_request_fee => {
80         scope => [ 'branchcode', 'categorycode' ],
81     },
82     open_article_requests_limit => {
83         scope => [ 'branchcode', 'categorycode' ],
84     },
85
86     auto_renew => {
87         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
88     },
89     cap_fine_to_replacement_price => {
90         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
91     },
92     chargeperiod => {
93         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
94     },
95     chargeperiod_charge_at => {
96         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
97     },
98     fine => {
99         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
100     },
101     finedays => {
102         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
103     },
104     firstremind => {
105         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
106     },
107     hardduedate => {
108         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
109     },
110     hardduedatecompare => {
111         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
112     },
113     holds_per_day => {
114         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
115     },
116     holds_per_record => {
117         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
118     },
119     issuelength => {
120         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
121     },
122     daysmode => {
123         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
124     },
125     lengthunit => {
126         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
127     },
128     maxissueqty => {
129         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
130     },
131     maxonsiteissueqty => {
132         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
133     },
134     maxsuspensiondays => {
135         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
136     },
137     no_auto_renewal_after => {
138         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
139     },
140     no_auto_renewal_after_hard_limit => {
141         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
142     },
143     norenewalbefore => {
144         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
145     },
146     onshelfholds => {
147         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
148     },
149     opacitemholds => {
150         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
151     },
152     overduefinescap => {
153         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
154     },
155     renewalperiod => {
156         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
157     },
158     renewalsallowed => {
159         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
160     },
161     unseen_renewals_allowed => {
162         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
163     },
164     rentaldiscount => {
165         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
166         can_be_blank => 0,
167     },
168     reservesallowed => {
169         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
170     },
171     suspension_chargeperiod => {
172         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
173     },
174     note => { # This is not really a rule. Maybe we will want to separate this later.
175         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
176     },
177     decreaseloanholds => {
178         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
179     },
180     # Not included (deprecated?):
181     #   * accountsent
182     #   * reservecharge
183     #   * restrictedtype
184 };
185
186 sub rule_kinds {
187     return $RULE_KINDS;
188 }
189
190 =head3 get_effective_rule
191
192 =cut
193
194 sub get_effective_rule {
195     my ( $self, $params ) = @_;
196
197     $params->{categorycode} //= undef;
198     $params->{branchcode}   //= undef;
199     $params->{itemtype}     //= undef;
200
201     my $rule_name    = $params->{rule_name};
202     my $categorycode = $params->{categorycode};
203     my $itemtype     = $params->{itemtype};
204     my $branchcode   = $params->{branchcode};
205
206     Koha::Exceptions::MissingParameter->throw(
207         "Required parameter 'rule_name' missing")
208       unless $rule_name;
209
210     for my $v ( $branchcode, $categorycode, $itemtype ) {
211         $v = undef if $v and $v eq '*';
212     }
213
214     my $order_by = $params->{order_by}
215       // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
216
217     my $search_params;
218     $search_params->{rule_name} = $rule_name;
219
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;
223
224     my $rule = $self->search(
225         $search_params,
226         {
227             order_by => $order_by,
228             rows => 1,
229         }
230     )->single;
231
232     return $rule;
233 }
234
235 =head3 get_effective_rules
236
237 =cut
238
239 sub get_effective_rules {
240     my ( $self, $params ) = @_;
241
242     my $rules        = $params->{rules};
243     my $categorycode = $params->{categorycode};
244     my $itemtype     = $params->{itemtype};
245     my $branchcode   = $params->{branchcode};
246
247     my $r;
248     foreach my $rule (@$rules) {
249         my $effective_rule = $self->get_effective_rule(
250             {
251                 rule_name    => $rule,
252                 categorycode => $categorycode,
253                 itemtype     => $itemtype,
254                 branchcode   => $branchcode,
255             }
256         );
257
258         $r->{$rule} = $effective_rule->rule_value if $effective_rule;
259     }
260
261     return $r;
262 }
263
264 =head3 set_rule
265
266 =cut
267
268 sub set_rule {
269     my ( $self, $params ) = @_;
270
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};
275     }
276
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;
281
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};
287         } else {
288             croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
289                 if exists $params->{$scope_level};
290         }
291     }
292
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;
300
301     for my $v ( $branchcode, $categorycode, $itemtype ) {
302         $v = undef if $v and $v eq '*';
303     }
304     my $rule = $self->search(
305         {
306             rule_name    => $rule_name,
307             branchcode   => $branchcode,
308             categorycode => $categorycode,
309             itemtype     => $itemtype,
310         }
311     )->next();
312
313     if ($rule) {
314         if ( defined $rule_value ) {
315             $rule->rule_value($rule_value);
316             $rule->update();
317         }
318         else {
319             $rule->delete();
320         }
321     }
322     else {
323         if ( defined $rule_value ) {
324             $rule = Koha::CirculationRule->new(
325                 {
326                     branchcode   => $branchcode,
327                     categorycode => $categorycode,
328                     itemtype     => $itemtype,
329                     rule_name    => $rule_name,
330                     rule_value   => $rule_value,
331                 }
332             );
333             $rule->store();
334         }
335     }
336
337     return $rule;
338 }
339
340 =head3 set_rules
341
342 =cut
343
344 sub set_rules {
345     my ( $self, $params ) = @_;
346
347     my %set_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};
352
353     my $rule_objects = [];
354     while ( my ( $rule_name, $rule_value ) = each %$rules ) {
355         my $rule_object = Koha::CirculationRules->set_rule(
356             {
357                 %set_params,
358                 rule_name    => $rule_name,
359                 rule_value   => $rule_value,
360             }
361         );
362         push( @$rule_objects, $rule_object );
363     }
364
365     return $rule_objects;
366 }
367
368 =head3 delete
369
370 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
371
372 =cut
373
374 sub delete {
375     my ( $self ) = @_;
376
377     while ( my $rule = $self->next ){
378         $rule->delete;
379     }
380 }
381
382 =head3 clone
383
384 Clone a set of circulation rules to another branch
385
386 =cut
387
388 sub clone {
389     my ( $self, $to_branch ) = @_;
390
391     while ( my $rule = $self->next ){
392         $rule->clone($to_branch);
393     }
394 }
395
396 =head3 get_opacitemholds_policy
397
398 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
399
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
403
404 =cut
405
406 sub get_opacitemholds_policy {
407     my ( $class, $params ) = @_;
408
409     my $item   = $params->{item};
410     my $patron = $params->{patron};
411
412     return unless $item or $patron;
413
414     my $rule = Koha::CirculationRules->get_effective_rule(
415         {
416             categorycode => $patron->categorycode,
417             itemtype     => $item->effective_itemtype,
418             branchcode   => $item->homebranch,
419             rule_name    => 'opacitemholds',
420         }
421     );
422
423     return $rule ? $rule->rule_value : undef;
424 }
425
426 =head3 get_onshelfholds_policy
427
428     my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
429
430 =cut
431
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(
438         {
439             categorycode => ( $patron ? $patron->categorycode : undef ),
440             itemtype     => $itemtype,
441             branchcode   => $item->holdingbranch,
442             rule_name    => 'onshelfholds',
443         }
444     );
445     return $rule ? $rule->rule_value : 0;
446 }
447
448 =head3 get_lostreturn_policy
449
450   my $lostrefund_policy = Koha::CirculationRules->get_lostreturn_policy( { return_branch => $return_branch, item => $item } );
451
452 Return values are:
453
454 =over 2
455
456 =item '0' - Do not refund
457
458 =item 'refund' - Refund the lost item charge
459
460 =item 'restore' - Refund the lost item charge and restore the original overdue fine
461
462 =item 'charge' - Refund the lost item charge and charge a new overdue fine
463
464 =back
465
466 =cut
467
468 sub get_lostreturn_policy {
469     my ( $class, $params ) = @_;
470
471     my $item   = $params->{item};
472
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
478     };
479
480     my $branch = $behaviour_mapping->{ $behaviour };
481
482     my $rule = Koha::CirculationRules->get_effective_rule(
483         {
484             branchcode => $branch,
485             rule_name  => 'lostreturn',
486         }
487     );
488
489     return $rule ? $rule->rule_value : 'refund';
490 }
491
492 =head3 article_requestable_rules
493
494     Return rules that allow article requests, optionally filtered by
495     patron categorycode.
496
497     Use with care; see guess_article_requestable_itemtypes.
498
499 =cut
500
501 sub article_requestable_rules {
502     my ( $class, $params ) = @_;
503     my $category = $params->{categorycode};
504
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' },
510     });
511 }
512
513 =head3 guess_article_requestable_itemtypes
514
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).
518
519     Note: pref ArticleRequestsLinkControl overrides the algorithm.
520
521     Optional parameters: categorycode.
522
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).
528
529 =cut
530
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';
536
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};
542
543     my $res = {};
544     my $rules = $class->article_requestable_rules({
545         $category ? ( categorycode => $category ) : (),
546     });
547     return $res if !$rules;
548     foreach my $rule ( $rules->as_list ) {
549         $res->{ $rule->itemtype // '*' } = 1;
550     }
551     $last_article_requestable_guesses->{$key} = $res;
552     $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
553     return $res;
554 }
555
556 =head3 get_daysmode_effective_value
557
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
560
561 =cut
562
563 sub get_effective_daysmode {
564     my ( $class, $params ) = @_;
565
566     my $categorycode     = $params->{categorycode};
567     my $itemtype         = $params->{itemtype};
568     my $branchcode       = $params->{branchcode};
569
570     my $daysmode_rule = $class->get_effective_rule(
571         {
572             categorycode => $categorycode,
573             itemtype     => $itemtype,
574             branchcode   => $branchcode,
575             rule_name    => 'daysmode',
576         }
577     );
578
579     return ( defined($daysmode_rule)
580           and $daysmode_rule->rule_value ne '' )
581       ? $daysmode_rule->rule_value
582       : C4::Context->preference('useDaysMode');
583
584 }
585
586
587 =head3 type
588
589 =cut
590
591 sub _type {
592     return 'CirculationRule';
593 }
594
595 =head3 object_class
596
597 =cut
598
599 sub object_class {
600     return 'Koha::CirculationRule';
601 }
602
603 1;