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