Bug 28489: Modify sessions.a_session from longtext to longblob
[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     unseen_renewals_allowed => {
155         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
156     },
157     rentaldiscount => {
158         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
159     },
160     reservesallowed => {
161         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
162     },
163     suspension_chargeperiod => {
164         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
165     },
166     note => { # This is not really a rule. Maybe we will want to separate this later.
167         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
168     },
169     decreaseloanholds => {
170         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
171     },
172     # Not included (deprecated?):
173     #   * accountsent
174     #   * reservecharge
175     #   * restrictedtype
176 };
177
178 sub rule_kinds {
179     return $RULE_KINDS;
180 }
181
182 =head3 get_effective_rule
183
184 =cut
185
186 sub get_effective_rule {
187     my ( $self, $params ) = @_;
188
189     $params->{categorycode} //= undef;
190     $params->{branchcode}   //= undef;
191     $params->{itemtype}     //= undef;
192
193     my $rule_name    = $params->{rule_name};
194     my $categorycode = $params->{categorycode};
195     my $itemtype     = $params->{itemtype};
196     my $branchcode   = $params->{branchcode};
197
198     Koha::Exceptions::MissingParameter->throw(
199         "Required parameter 'rule_name' missing")
200       unless $rule_name;
201
202     for my $v ( $branchcode, $categorycode, $itemtype ) {
203         $v = undef if $v and $v eq '*';
204     }
205
206     my $order_by = $params->{order_by}
207       // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
208
209     my $search_params;
210     $search_params->{rule_name} = $rule_name;
211
212     $search_params->{categorycode} = defined $categorycode ? [ $categorycode, undef ] : undef;
213     $search_params->{itemtype}     = defined $itemtype     ? [ $itemtype, undef ] : undef;
214     $search_params->{branchcode}   = defined $branchcode   ? [ $branchcode,   undef ] : undef;
215
216     my $rule = $self->search(
217         $search_params,
218         {
219             order_by => $order_by,
220             rows => 1,
221         }
222     )->single;
223
224     return $rule;
225 }
226
227 =head3 get_effective_rules
228
229 =cut
230
231 sub get_effective_rules {
232     my ( $self, $params ) = @_;
233
234     my $rules        = $params->{rules};
235     my $categorycode = $params->{categorycode};
236     my $itemtype     = $params->{itemtype};
237     my $branchcode   = $params->{branchcode};
238
239     my $r;
240     foreach my $rule (@$rules) {
241         my $effective_rule = $self->get_effective_rule(
242             {
243                 rule_name    => $rule,
244                 categorycode => $categorycode,
245                 itemtype     => $itemtype,
246                 branchcode   => $branchcode,
247             }
248         );
249
250         $r->{$rule} = $effective_rule->rule_value if $effective_rule;
251     }
252
253     return $r;
254 }
255
256 =head3 set_rule
257
258 =cut
259
260 sub set_rule {
261     my ( $self, $params ) = @_;
262
263     for my $mandatory_parameter (qw( rule_name rule_value ) ) {
264         Koha::Exceptions::MissingParameter->throw(
265             "Required parameter '$mandatory_parameter' missing")
266           unless exists $params->{$mandatory_parameter};
267     }
268
269     my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
270     Koha::Exceptions::MissingParameter->throw(
271         "set_rule given unknown rule '$params->{rule_name}'!")
272         unless defined $kind_info;
273
274     # Enforce scope; a rule should be set for its defined scope, no more, no less.
275     foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
276         if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
277             croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
278                 unless exists $params->{$scope_level};
279         } else {
280             croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
281                 if exists $params->{$scope_level};
282         }
283     }
284
285     my $branchcode   = $params->{branchcode};
286     my $categorycode = $params->{categorycode};
287     my $itemtype     = $params->{itemtype};
288     my $rule_name    = $params->{rule_name};
289     my $rule_value   = $params->{rule_value};
290     my $can_be_blank = defined $kind_info->{can_be_blank} ? $kind_info->{can_be_blank} : 1;
291     $rule_value = undef if defined $rule_value && $rule_value eq "" && !$can_be_blank;
292
293     for my $v ( $branchcode, $categorycode, $itemtype ) {
294         $v = undef if $v and $v eq '*';
295     }
296     my $rule = $self->search(
297         {
298             rule_name    => $rule_name,
299             branchcode   => $branchcode,
300             categorycode => $categorycode,
301             itemtype     => $itemtype,
302         }
303     )->next();
304
305     if ($rule) {
306         if ( defined $rule_value ) {
307             $rule->rule_value($rule_value);
308             $rule->update();
309         }
310         else {
311             $rule->delete();
312         }
313     }
314     else {
315         if ( defined $rule_value ) {
316             $rule = Koha::CirculationRule->new(
317                 {
318                     branchcode   => $branchcode,
319                     categorycode => $categorycode,
320                     itemtype     => $itemtype,
321                     rule_name    => $rule_name,
322                     rule_value   => $rule_value,
323                 }
324             );
325             $rule->store();
326         }
327     }
328
329     return $rule;
330 }
331
332 =head3 set_rules
333
334 =cut
335
336 sub set_rules {
337     my ( $self, $params ) = @_;
338
339     my %set_params;
340     $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
341     $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
342     $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
343     my $rules        = $params->{rules};
344
345     my $rule_objects = [];
346     while ( my ( $rule_name, $rule_value ) = each %$rules ) {
347         my $rule_object = Koha::CirculationRules->set_rule(
348             {
349                 %set_params,
350                 rule_name    => $rule_name,
351                 rule_value   => $rule_value,
352             }
353         );
354         push( @$rule_objects, $rule_object );
355     }
356
357     return $rule_objects;
358 }
359
360 =head3 delete
361
362 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
363
364 =cut
365
366 sub delete {
367     my ( $self ) = @_;
368
369     while ( my $rule = $self->next ){
370         $rule->delete;
371     }
372 }
373
374 =head3 clone
375
376 Clone a set of circulation rules to another branch
377
378 =cut
379
380 sub clone {
381     my ( $self, $to_branch ) = @_;
382
383     while ( my $rule = $self->next ){
384         $rule->clone($to_branch);
385     }
386 }
387
388 =head3 get_opacitemholds_policy
389
390 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
391
392 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
393 and the "Item level holds" (opacitemholds).
394 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
395
396 =cut
397
398 sub get_opacitemholds_policy {
399     my ( $class, $params ) = @_;
400
401     my $item   = $params->{item};
402     my $patron = $params->{patron};
403
404     return unless $item or $patron;
405
406     my $rule = Koha::CirculationRules->get_effective_rule(
407         {
408             categorycode => $patron->categorycode,
409             itemtype     => $item->effective_itemtype,
410             branchcode   => $item->homebranch,
411             rule_name    => 'opacitemholds',
412         }
413     );
414
415     return $rule ? $rule->rule_value : undef;
416 }
417
418 =head3 get_onshelfholds_policy
419
420     my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
421
422 =cut
423
424 sub get_onshelfholds_policy {
425     my ( $class, $params ) = @_;
426     my $item = $params->{item};
427     my $itemtype = $item->effective_itemtype;
428     my $patron = $params->{patron};
429     my $rule = Koha::CirculationRules->get_effective_rule(
430         {
431             categorycode => ( $patron ? $patron->categorycode : undef ),
432             itemtype     => $itemtype,
433             branchcode   => $item->holdingbranch,
434             rule_name    => 'onshelfholds',
435         }
436     );
437     return $rule ? $rule->rule_value : 0;
438 }
439
440 =head3 get_lostreturn_policy
441
442   my $lostrefund_policy = Koha::CirculationRules->get_lostreturn_policy( { return_branch => $return_branch, item => $item } );
443
444 Return values are:
445
446 =over 2
447
448 =item '0' - Do not refund
449
450 =item 'refund' - Refund the lost item charge
451
452 =item 'restore' - Refund the lost item charge and restore the original overdue fine
453
454 =item 'charge' - Refund the lost item charge and charge a new overdue fine
455
456 =back
457
458 =cut
459
460 sub get_lostreturn_policy {
461     my ( $class, $params ) = @_;
462
463     my $item   = $params->{item};
464
465     my $behaviour = C4::Context->preference( 'RefundLostOnReturnControl' ) // 'CheckinLibrary';
466     my $behaviour_mapping = {
467         CheckinLibrary    => $params->{'return_branch'} // $item->homebranch,
468         ItemHomeBranch    => $item->homebranch,
469         ItemHoldingBranch => $item->holdingbranch
470     };
471
472     my $branch = $behaviour_mapping->{ $behaviour };
473
474     my $rule = Koha::CirculationRules->get_effective_rule(
475         {
476             branchcode => $branch,
477             rule_name  => 'lostreturn',
478         }
479     );
480
481     return $rule ? $rule->rule_value : 'refund';
482 }
483
484 =head3 article_requestable_rules
485
486     Return rules that allow article requests, optionally filtered by
487     patron categorycode.
488
489     Use with care; see guess_article_requestable_itemtypes.
490
491 =cut
492
493 sub article_requestable_rules {
494     my ( $class, $params ) = @_;
495     my $category = $params->{categorycode};
496
497     return if !C4::Context->preference('ArticleRequests');
498     return $class->search({
499         $category ? ( categorycode => [ $category, undef ] ) : (),
500         rule_name => 'article_requests',
501         rule_value => { '!=' => 'no' },
502     });
503 }
504
505 =head3 guess_article_requestable_itemtypes
506
507     Return item types in a hashref that are likely possible to be
508     'article requested'. Constructed by an intelligent guess in the
509     issuing rules (see article_requestable_rules).
510
511     Note: pref ArticleRequestsLinkControl overrides the algorithm.
512
513     Optional parameters: categorycode.
514
515     Note: the routine is used in opac-search to obtain a reasonable
516     estimate within performance borders (not looking at all items but
517     just using default itemtype). Also we are not looking at the
518     branchcode here, since home or holding branch of the item is
519     leading and branch may be unknown too (anonymous opac session).
520
521 =cut
522
523 sub guess_article_requestable_itemtypes {
524     my ( $class, $params ) = @_;
525     my $category = $params->{categorycode};
526     return {} if !C4::Context->preference('ArticleRequests');
527     return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
528
529     my $cache = Koha::Caches->get_instance;
530     my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
531     my $key = $category || '*';
532     return $last_article_requestable_guesses->{$key}
533         if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
534
535     my $res = {};
536     my $rules = $class->article_requestable_rules({
537         $category ? ( categorycode => $category ) : (),
538     });
539     return $res if !$rules;
540     foreach my $rule ( $rules->as_list ) {
541         $res->{ $rule->itemtype // '*' } = 1;
542     }
543     $last_article_requestable_guesses->{$key} = $res;
544     $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
545     return $res;
546 }
547
548 =head3 get_daysmode_effective_value
549
550 Return the value for daysmode defined in the circulation rules.
551 If not defined (or empty string), the value of the system preference useDaysMode is returned
552
553 =cut
554
555 sub get_effective_daysmode {
556     my ( $class, $params ) = @_;
557
558     my $categorycode     = $params->{categorycode};
559     my $itemtype         = $params->{itemtype};
560     my $branchcode       = $params->{branchcode};
561
562     my $daysmode_rule = $class->get_effective_rule(
563         {
564             categorycode => $categorycode,
565             itemtype     => $itemtype,
566             branchcode   => $branchcode,
567             rule_name    => 'daysmode',
568         }
569     );
570
571     return ( defined($daysmode_rule)
572           and $daysmode_rule->rule_value ne '' )
573       ? $daysmode_rule->rule_value
574       : C4::Context->preference('useDaysMode');
575
576 }
577
578
579 =head3 type
580
581 =cut
582
583 sub _type {
584     return 'CirculationRule';
585 }
586
587 =head3 object_class
588
589 =cut
590
591 sub object_class {
592     return 'Koha::CirculationRule';
593 }
594
595 1;