Bug 18936: new suspension_chargeperiod
[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 under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 3 of the License, or (at your option) any later
10 # version.
11 #
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
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     refund => {
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     },
66     hold_fulfillment_policy => {
67         scope => [ 'branchcode', 'itemtype' ],
68     },
69     returnbranch => {
70         scope => [ 'branchcode', 'itemtype' ],
71     },
72
73     article_requests => {
74         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
75     },
76     auto_renew => {
77         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
78     },
79     cap_fine_to_replacement_price => {
80         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
81     },
82     chargeperiod => {
83         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
84     },
85     chargeperiod_charge_at => {
86         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
87     },
88     fine => {
89         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
90     },
91     finedays => {
92         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
93     },
94     firstremind => {
95         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
96     },
97     hardduedate => {
98         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
99     },
100     hardduedatecompare => {
101         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
102     },
103     holds_per_record => {
104         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
105     },
106     issuelength => {
107         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
108     },
109     lengthunit => {
110         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
111     },
112     maxissueqty => {
113         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
114     },
115     maxonsiteissueqty => {
116         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
117     },
118     maxsuspensiondays => {
119         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
120     },
121     no_auto_renewal_after => {
122         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
123     },
124     no_auto_renewal_after_hard_limit => {
125         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
126     },
127     norenewalbefore => {
128         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
129     },
130     onshelfholds => {
131         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
132     },
133     opacitemholds => {
134         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
135     },
136     overduefinescap => {
137         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
138     },
139     renewalperiod => {
140         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
141     },
142     renewalsallowed => {
143         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
144     },
145     rentaldiscount => {
146         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
147     },
148     reservesallowed => {
149         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
150     },
151     suspension_chargeperiod => {
152         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
153     },
154     # Not included (deprecated?):
155     #   * accountsent
156     #   * reservecharge
157     #   * restrictedtype
158 };
159
160 sub rule_kinds {
161     return $RULE_KINDS;
162 }
163
164 =head3 get_effective_rule
165
166 =cut
167
168 sub get_effective_rule {
169     my ( $self, $params ) = @_;
170
171     $params->{categorycode} //= undef;
172     $params->{branchcode}   //= undef;
173     $params->{itemtype}     //= undef;
174
175     my $rule_name    = $params->{rule_name};
176     my $categorycode = $params->{categorycode};
177     my $itemtype     = $params->{itemtype};
178     my $branchcode   = $params->{branchcode};
179
180     Koha::Exceptions::MissingParameter->throw(
181         "Required parameter 'rule_name' missing")
182       unless $rule_name;
183
184     for my $v ( $branchcode, $categorycode, $itemtype ) {
185         $v = undef if $v and $v eq '*';
186     }
187
188     my $order_by = $params->{order_by}
189       // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
190
191     my $search_params;
192     $search_params->{rule_name} = $rule_name;
193
194     $search_params->{categorycode} = defined $categorycode ? [ $categorycode, undef ] : undef;
195     $search_params->{itemtype}     = defined $itemtype     ? [ $itemtype, undef ] : undef;
196     $search_params->{branchcode}   = defined $branchcode   ? [ $branchcode,   undef ] : undef;
197
198     my $rule = $self->search(
199         $search_params,
200         {
201             order_by => $order_by,
202             rows => 1,
203         }
204     )->single;
205
206     return $rule;
207 }
208
209 =head3 get_effective_rule
210
211 =cut
212
213 sub get_effective_rules {
214     my ( $self, $params ) = @_;
215
216     my $rules        = $params->{rules};
217     my $categorycode = $params->{categorycode};
218     my $itemtype     = $params->{itemtype};
219     my $branchcode   = $params->{branchcode};
220
221     my $r;
222     foreach my $rule (@$rules) {
223         my $effective_rule = $self->get_effective_rule(
224             {
225                 rule_name    => $rule,
226                 categorycode => $categorycode,
227                 itemtype     => $itemtype,
228                 branchcode   => $branchcode,
229             }
230         );
231
232         $r->{$rule} = $effective_rule->rule_value if $effective_rule;
233     }
234
235     return $r;
236 }
237
238 =head3 set_rule
239
240 =cut
241
242 sub set_rule {
243     my ( $self, $params ) = @_;
244
245     for my $mandatory_parameter (qw( rule_name rule_value ) ) {
246         Koha::Exceptions::MissingParameter->throw(
247             "Required parameter '$mandatory_parameter' missing")
248           unless exists $params->{$mandatory_parameter};
249     }
250
251     my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
252     Koha::Exceptions::MissingParameter->throw(
253         "set_rule given unknown rule '$params->{rule_name}'!")
254         unless defined $kind_info;
255
256     # Enforce scope; a rule should be set for its defined scope, no more, no less.
257     foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
258         if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
259             croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
260                 unless exists $params->{$scope_level};
261         } else {
262             croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
263                 if exists $params->{$scope_level};
264         }
265     }
266
267     my $branchcode   = $params->{branchcode};
268     my $categorycode = $params->{categorycode};
269     my $itemtype     = $params->{itemtype};
270     my $rule_name    = $params->{rule_name};
271     my $rule_value   = $params->{rule_value};
272
273     for my $v ( $branchcode, $categorycode, $itemtype ) {
274         $v = undef if $v and $v eq '*';
275     }
276     my $rule = $self->search(
277         {
278             rule_name    => $rule_name,
279             branchcode   => $branchcode,
280             categorycode => $categorycode,
281             itemtype     => $itemtype,
282         }
283     )->next();
284
285     if ($rule) {
286         if ( defined $rule_value ) {
287             $rule->rule_value($rule_value);
288             $rule->update();
289         }
290         else {
291             $rule->delete();
292         }
293     }
294     else {
295         if ( defined $rule_value ) {
296             $rule = Koha::CirculationRule->new(
297                 {
298                     branchcode   => $branchcode,
299                     categorycode => $categorycode,
300                     itemtype     => $itemtype,
301                     rule_name    => $rule_name,
302                     rule_value   => $rule_value,
303                 }
304             );
305             $rule->store();
306         }
307     }
308
309     return $rule;
310 }
311
312 =head3 set_rules
313
314 =cut
315
316 sub set_rules {
317     my ( $self, $params ) = @_;
318
319     my %set_params;
320     $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
321     $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
322     $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
323     my $rules        = $params->{rules};
324
325     my $rule_objects = [];
326     while ( my ( $rule_name, $rule_value ) = each %$rules ) {
327         my $rule_object = Koha::CirculationRules->set_rule(
328             {
329                 %set_params,
330                 rule_name    => $rule_name,
331                 rule_value   => $rule_value,
332             }
333         );
334         push( @$rule_objects, $rule_object );
335     }
336
337     return $rule_objects;
338 }
339
340 =head3 delete
341
342 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
343
344 =cut
345
346 sub delete {
347     my ( $self ) = @_;
348
349     while ( my $rule = $self->next ){
350         $rule->delete;
351     }
352 }
353
354 =head3 get_opacitemholds_policy
355
356 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
357
358 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
359 and the "Item level holds" (opacitemholds).
360 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
361
362 =cut
363
364 sub get_opacitemholds_policy {
365     my ( $class, $params ) = @_;
366
367     my $item   = $params->{item};
368     my $patron = $params->{patron};
369
370     return unless $item or $patron;
371
372     my $rule = Koha::CirculationRules->get_effective_issuing_rule(
373         {
374             categorycode => $patron->categorycode,
375             itemtype     => $item->effective_itemtype,
376             branchcode   => $item->homebranch,
377             rule_name    => 'opacitemholds',
378         }
379     );
380
381     return $rule ? $rule->rule_value : undef;
382 }
383
384 =head3 get_onshelfholds_policy
385
386     my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
387
388 =cut
389
390 sub get_onshelfholds_policy {
391     my ( $class, $params ) = @_;
392     my $item = $params->{item};
393     my $itemtype = $item->effective_itemtype;
394     my $patron = $params->{patron};
395     my $rule = Koha::CirculationRules->get_effective_rule(
396         {
397             categorycode => ( $patron ? $patron->categorycode : undef ),
398             itemtype     => $itemtype,
399             branchcode   => $item->holdingbranch,
400             rule_name    => 'onshelfholds',
401         }
402     );
403     return $rule ? $rule->rule_value : undef;
404 }
405
406 =head3 article_requestable_rules
407
408     Return rules that allow article requests, optionally filtered by
409     patron categorycode.
410
411     Use with care; see guess_article_requestable_itemtypes.
412
413 =cut
414
415 sub article_requestable_rules {
416     my ( $class, $params ) = @_;
417     my $category = $params->{categorycode};
418
419     return if !C4::Context->preference('ArticleRequests');
420     return $class->search({
421         $category ? ( categorycode => [ $category, '*' ] ) : (),
422         rule_name => 'article_requests',
423         rule_value => { '!=' => 'no' },
424     });
425 }
426
427 =head3 guess_article_requestable_itemtypes
428
429     Return item types in a hashref that are likely possible to be
430     'article requested'. Constructed by an intelligent guess in the
431     issuing rules (see article_requestable_rules).
432
433     Note: pref ArticleRequestsLinkControl overrides the algorithm.
434
435     Optional parameters: categorycode.
436
437     Note: the routine is used in opac-search to obtain a reasonable
438     estimate within performance borders (not looking at all items but
439     just using default itemtype). Also we are not looking at the
440     branchcode here, since home or holding branch of the item is
441     leading and branch may be unknown too (anonymous opac session).
442
443 =cut
444
445 sub guess_article_requestable_itemtypes {
446     my ( $class, $params ) = @_;
447     my $category = $params->{categorycode};
448     return {} if !C4::Context->preference('ArticleRequests');
449     return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
450
451     my $cache = Koha::Caches->get_instance;
452     my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
453     my $key = $category || '*';
454     return $last_article_requestable_guesses->{$key}
455         if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
456
457     my $res = {};
458     my $rules = $class->article_requestable_rules({
459         $category ? ( categorycode => $category ) : (),
460     });
461     return $res if !$rules;
462     foreach my $rule ( $rules->as_list ) {
463         $res->{ $rule->itemtype } = 1;
464     }
465     $last_article_requestable_guesses->{$key} = $res;
466     $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
467     return $res;
468 }
469
470
471 =head3 type
472
473 =cut
474
475 sub _type {
476     return 'CirculationRule';
477 }
478
479 =head3 object_class
480
481 =cut
482
483 sub object_class {
484     return 'Koha::CirculationRule';
485 }
486
487 1;