Bug 17583: Add the Koha::Patron->is_going_to_expired method
[koha.git] / Koha / Patron.pm
1 package Koha::Patron;
2
3 # Copyright ByWater Solutions 2014
4 # Copyright PTFS Europe 2016
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it under the
9 # terms of the GNU General Public License as published by the Free Software
10 # Foundation; either version 3 of the License, or (at your option) any later
11 # version.
12 #
13 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
14 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License along
18 # with Koha; if not, write to the Free Software Foundation, Inc.,
19 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21 use Modern::Perl;
22
23 use Carp;
24
25 use C4::Context;
26 use C4::Log;
27 use Koha::Database;
28 use Koha::DateUtils;
29 use Koha::Holds;
30 use Koha::Issues;
31 use Koha::OldIssues;
32 use Koha::Patron::Categories;
33 use Koha::Patron::HouseboundProfile;
34 use Koha::Patron::HouseboundRole;
35 use Koha::Patron::Images;
36 use Koha::Patrons;
37 use Koha::Virtualshelves;
38
39 use base qw(Koha::Object);
40
41 =head1 NAME
42
43 Koha::Patron - Koha Patron Object class
44
45 =head1 API
46
47 =head2 Class Methods
48
49 =cut
50
51 =head3 delete
52
53 $patron->delete
54
55 Delete patron's holds, lists and finally the patron.
56
57 Lists owned by the borrower are deleted, but entries from the borrower to
58 other lists are kept.
59
60 =cut
61
62 sub delete {
63     my ($self) = @_;
64
65     my $deleted;
66     $self->_result->result_source->schema->txn_do(
67         sub {
68             # Delete Patron's holds
69             # FIXME Should be $patron->get_holds
70             $_->delete for Koha::Holds->search( { borrowernumber => $self->borrowernumber } );
71
72             # Delete all lists and all shares of this borrower
73             # Consistent with the approach Koha uses on deleting individual lists
74             # Note that entries in virtualshelfcontents added by this borrower to
75             # lists of others will be handled by a table constraint: the borrower
76             # is set to NULL in those entries.
77             # NOTE:
78             # We could handle the above deletes via a constraint too.
79             # But a new BZ report 11889 has been opened to discuss another approach.
80             # Instead of deleting we could also disown lists (based on a pref).
81             # In that way we could save shared and public lists.
82             # The current table constraints support that idea now.
83             # This pref should then govern the results of other routines/methods such as
84             # Koha::Virtualshelf->new->delete too.
85             # FIXME Could be $patron->get_lists
86             $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
87
88             $deleted = $self->SUPER::delete;
89
90             logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
91         }
92     );
93     return $deleted;
94 }
95
96
97 =head3 category
98
99 my $patron_category = $patron->category
100
101 Return the patron category for this patron
102
103 =cut
104
105 sub category {
106     my ( $self ) = @_;
107     return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
108 }
109
110 =head3 guarantor
111
112 Returns a Koha::Patron object for this patron's guarantor
113
114 =cut
115
116 sub guarantor {
117     my ( $self ) = @_;
118
119     return unless $self->guarantorid();
120
121     return Koha::Patrons->find( $self->guarantorid() );
122 }
123
124 sub image {
125     my ( $self ) = @_;
126
127     return Koha::Patron::Images->find( $self->borrowernumber )
128 }
129
130 =head3 guarantees
131
132 Returns the guarantees (list of Koha::Patron) of this patron
133
134 =cut
135
136 sub guarantees {
137     my ( $self ) = @_;
138
139     return Koha::Patrons->search( { guarantorid => $self->borrowernumber } );
140 }
141
142 =head3 housebound_profile
143
144 Returns the HouseboundProfile associated with this patron.
145
146 =cut
147
148 sub housebound_profile {
149     my ( $self ) = @_;
150     my $profile = $self->_result->housebound_profile;
151     return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
152         if ( $profile );
153     return;
154 }
155
156 =head3 housebound_role
157
158 Returns the HouseboundRole associated with this patron.
159
160 =cut
161
162 sub housebound_role {
163     my ( $self ) = @_;
164
165     my $role = $self->_result->housebound_role;
166     return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
167     return;
168 }
169
170 =head3 siblings
171
172 Returns the siblings of this patron.
173
174 =cut
175
176 sub siblings {
177     my ( $self ) = @_;
178
179     my $guarantor = $self->guarantor;
180
181     return unless $guarantor;
182
183     return Koha::Patrons->search(
184         {
185             guarantorid => {
186                 '!=' => undef,
187                 '=' => $guarantor->id,
188             },
189             borrowernumber => {
190                 '!=' => $self->borrowernumber,
191             }
192         }
193     );
194 }
195
196 =head3 wants_check_for_previous_checkout
197
198     $wants_check = $patron->wants_check_for_previous_checkout;
199
200 Return 1 if Koha needs to perform PrevIssue checking, else 0.
201
202 =cut
203
204 sub wants_check_for_previous_checkout {
205     my ( $self ) = @_;
206     my $syspref = C4::Context->preference("checkPrevCheckout");
207
208     # Simple cases
209     ## Hard syspref trumps all
210     return 1 if ($syspref eq 'hardyes');
211     return 0 if ($syspref eq 'hardno');
212     ## Now, patron pref trumps all
213     return 1 if ($self->checkprevcheckout eq 'yes');
214     return 0 if ($self->checkprevcheckout eq 'no');
215
216     # More complex: patron inherits -> determine category preference
217     my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
218     return 1 if ($checkPrevCheckoutByCat eq 'yes');
219     return 0 if ($checkPrevCheckoutByCat eq 'no');
220
221     # Finally: category preference is inherit, default to 0
222     if ($syspref eq 'softyes') {
223         return 1;
224     } else {
225         return 0;
226     }
227 }
228
229 =head3 do_check_for_previous_checkout
230
231     $do_check = $patron->do_check_for_previous_checkout($item);
232
233 Return 1 if the bib associated with $ITEM has previously been checked out to
234 $PATRON, 0 otherwise.
235
236 =cut
237
238 sub do_check_for_previous_checkout {
239     my ( $self, $item ) = @_;
240
241     # Find all items for bib and extract item numbers.
242     my @items = Koha::Items->search({biblionumber => $item->{biblionumber}});
243     my @item_nos;
244     foreach my $item (@items) {
245         push @item_nos, $item->itemnumber;
246     }
247
248     # Create (old)issues search criteria
249     my $criteria = {
250         borrowernumber => $self->borrowernumber,
251         itemnumber => \@item_nos,
252     };
253
254     # Check current issues table
255     my $issues = Koha::Issues->search($criteria);
256     return 1 if $issues->count; # 0 || N
257
258     # Check old issues table
259     my $old_issues = Koha::OldIssues->search($criteria);
260     return $old_issues->count;  # 0 || N
261 }
262
263 =head2 is_debarred
264
265 my $debarment_expiration = $patron->is_debarred;
266
267 Returns the date a patron debarment will expire, or undef if the patron is not
268 debarred
269
270 =cut
271
272 sub is_debarred {
273     my ($self) = @_;
274
275     return unless $self->debarred;
276     return $self->debarred
277       if $self->debarred =~ '^9999'
278       or dt_from_string( $self->debarred ) > dt_from_string;
279     return;
280 }
281
282 =head2 is_expired
283
284 my $is_expired = $patron->is_expired;
285
286 Returns 1 if the patron is expired or 0;
287
288 =cut
289
290 sub is_expired {
291     my ($self) = @_;
292     return 0 unless $self->dateexpiry;
293     return 0 if $self->dateexpiry eq '0000-00-00';
294     return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
295     return 0;
296 }
297
298 =head2 is_going_to_expired
299
300 my $is_going_to_expired = $patron->is_going_to_expired;
301
302 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
303
304 =cut
305
306 sub is_going_to_expired {
307     my ($self) = @_;
308
309     my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
310
311     return 0 unless $self->dateexpiry;
312     return 0 if $self->dateexpiry eq '0000-00-00';
313     return 1 if dt_from_string( $self->dateexpiry )->add( days => $delay ) < dt_from_string;
314     return 0;
315 }
316
317 =head2 update_password
318
319 my $updated = $patron->update_password( $userid, $password );
320
321 Update the userid and the password of a patron.
322 If the userid already exists, returns and let DBIx::Class warns
323 This will add an entry to action_logs if BorrowersLog is set.
324
325 =cut
326
327 sub update_password {
328     my ( $self, $userid, $password ) = @_;
329     eval { $self->userid($userid)->store; };
330     return if $@; # Make sure the userid is not already in used by another patron
331     $self->password($password)->store;
332     logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
333     return 1;
334 }
335
336 =head3 renew_account
337
338 my $new_expiry_date = $patron->renew_account
339
340 Extending the subscription to the expiry date.
341
342 =cut
343
344 sub renew_account {
345     my ($self) = @_;
346     my $date;
347     if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
348         $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
349     } else {
350         $date =
351             C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
352             ? dt_from_string( $self->dateexpiry )
353             : dt_from_string;
354     }
355     my $expiry_date = $self->category->get_expiry_date($date);
356
357     $self->dateexpiry($expiry_date)->store;
358
359     $self->add_enrolment_fee_if_needed;
360
361     logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
362     return dt_from_string( $expiry_date )->truncate( to => 'day' );
363 }
364
365 =head2 has_overdues
366
367 my $has_overdues = $patron->has_overdues;
368
369 Returns the number of patron's overdues
370
371 =cut
372
373 sub has_overdues {
374     my ($self) = @_;
375     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
376     return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
377 }
378
379 =head2 track_login
380
381     $patron->track_login;
382     $patron->track_login({ force => 1 });
383
384     Tracks a (successful) login attempt.
385     The preference TrackLastPatronActivity must be enabled. Or you
386     should pass the force parameter.
387
388 =cut
389
390 sub track_login {
391     my ( $self, $params ) = @_;
392     return if
393         !$params->{force} &&
394         !C4::Context->preference('TrackLastPatronActivity');
395     $self->lastseen( dt_from_string() )->store;
396 }
397
398 =head2 move_to_deleted
399
400 my $is_moved = $patron->move_to_deleted;
401
402 Move a patron to the deletedborrowers table.
403 This can be done before deleting a patron, to make sure the data are not completely deleted.
404
405 =cut
406
407 sub move_to_deleted {
408     my ($self) = @_;
409     my $patron_infos = $self->unblessed;
410     return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
411 }
412
413 =head3 article_requests
414
415 my @requests = $borrower->article_requests();
416 my $requests = $borrower->article_requests();
417
418 Returns either a list of ArticleRequests objects,
419 or an ArtitleRequests object, depending on the
420 calling context.
421
422 =cut
423
424 sub article_requests {
425     my ( $self ) = @_;
426
427     $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
428
429     return $self->{_article_requests};
430 }
431
432 =head3 article_requests_current
433
434 my @requests = $patron->article_requests_current
435
436 Returns the article requests associated with this patron that are incomplete
437
438 =cut
439
440 sub article_requests_current {
441     my ( $self ) = @_;
442
443     $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
444         {
445             borrowernumber => $self->id(),
446             -or          => [
447                 { status => Koha::ArticleRequest::Status::Pending },
448                 { status => Koha::ArticleRequest::Status::Processing }
449             ]
450         }
451     );
452
453     return $self->{_article_requests_current};
454 }
455
456 =head3 article_requests_finished
457
458 my @requests = $biblio->article_requests_finished
459
460 Returns the article requests associated with this patron that are completed
461
462 =cut
463
464 sub article_requests_finished {
465     my ( $self, $borrower ) = @_;
466
467     $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
468         {
469             borrowernumber => $self->id(),
470             -or          => [
471                 { status => Koha::ArticleRequest::Status::Completed },
472                 { status => Koha::ArticleRequest::Status::Canceled }
473             ]
474         }
475     );
476
477     return $self->{_article_requests_finished};
478 }
479
480 =head3 add_enrolment_fee_if_needed
481
482 my $enrolment_fee = $patron->add_enrolment_fee_if_needed;
483
484 Add enrolment fee for a patron if needed.
485
486 =cut
487
488 sub add_enrolment_fee_if_needed {
489     my ($self) = @_;
490     my $enrolment_fee = $self->category->enrolmentfee;
491     if ( $enrolment_fee && $enrolment_fee > 0 ) {
492         # insert fee in patron debts
493         C4::Accounts::manualinvoice( $self->borrowernumber, '', '', 'A', $enrolment_fee );
494     }
495     return $enrolment_fee || 0;
496 }
497
498 =head3 type
499
500 =cut
501
502 sub _type {
503     return 'Borrower';
504 }
505
506 =head1 AUTHOR
507
508 Kyle M Hall <kyle@bywatersolutions.com>
509 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
510
511 =cut
512
513 1;