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