Bug 17583: Make sure we are comparing 2 dates
[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 $delay;
312     return 0 unless $self->dateexpiry;
313     return 0 if $self->dateexpiry eq '0000-00-00';
314     return 1 if dt_from_string( $self->dateexpiry )->add( days => -$delay ) < dt_from_string->truncate( to => 'day' );
315     return 0;
316 }
317
318 =head2 update_password
319
320 my $updated = $patron->update_password( $userid, $password );
321
322 Update the userid and the password of a patron.
323 If the userid already exists, returns and let DBIx::Class warns
324 This will add an entry to action_logs if BorrowersLog is set.
325
326 =cut
327
328 sub update_password {
329     my ( $self, $userid, $password ) = @_;
330     eval { $self->userid($userid)->store; };
331     return if $@; # Make sure the userid is not already in used by another patron
332     $self->password($password)->store;
333     logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
334     return 1;
335 }
336
337 =head3 renew_account
338
339 my $new_expiry_date = $patron->renew_account
340
341 Extending the subscription to the expiry date.
342
343 =cut
344
345 sub renew_account {
346     my ($self) = @_;
347     my $date;
348     if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
349         $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
350     } else {
351         $date =
352             C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
353             ? dt_from_string( $self->dateexpiry )
354             : dt_from_string;
355     }
356     my $expiry_date = $self->category->get_expiry_date($date);
357
358     $self->dateexpiry($expiry_date)->store;
359
360     $self->add_enrolment_fee_if_needed;
361
362     logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
363     return dt_from_string( $expiry_date )->truncate( to => 'day' );
364 }
365
366 =head2 has_overdues
367
368 my $has_overdues = $patron->has_overdues;
369
370 Returns the number of patron's overdues
371
372 =cut
373
374 sub has_overdues {
375     my ($self) = @_;
376     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
377     return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
378 }
379
380 =head2 track_login
381
382     $patron->track_login;
383     $patron->track_login({ force => 1 });
384
385     Tracks a (successful) login attempt.
386     The preference TrackLastPatronActivity must be enabled. Or you
387     should pass the force parameter.
388
389 =cut
390
391 sub track_login {
392     my ( $self, $params ) = @_;
393     return if
394         !$params->{force} &&
395         !C4::Context->preference('TrackLastPatronActivity');
396     $self->lastseen( dt_from_string() )->store;
397 }
398
399 =head2 move_to_deleted
400
401 my $is_moved = $patron->move_to_deleted;
402
403 Move a patron to the deletedborrowers table.
404 This can be done before deleting a patron, to make sure the data are not completely deleted.
405
406 =cut
407
408 sub move_to_deleted {
409     my ($self) = @_;
410     my $patron_infos = $self->unblessed;
411     return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
412 }
413
414 =head3 article_requests
415
416 my @requests = $borrower->article_requests();
417 my $requests = $borrower->article_requests();
418
419 Returns either a list of ArticleRequests objects,
420 or an ArtitleRequests object, depending on the
421 calling context.
422
423 =cut
424
425 sub article_requests {
426     my ( $self ) = @_;
427
428     $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
429
430     return $self->{_article_requests};
431 }
432
433 =head3 article_requests_current
434
435 my @requests = $patron->article_requests_current
436
437 Returns the article requests associated with this patron that are incomplete
438
439 =cut
440
441 sub article_requests_current {
442     my ( $self ) = @_;
443
444     $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
445         {
446             borrowernumber => $self->id(),
447             -or          => [
448                 { status => Koha::ArticleRequest::Status::Pending },
449                 { status => Koha::ArticleRequest::Status::Processing }
450             ]
451         }
452     );
453
454     return $self->{_article_requests_current};
455 }
456
457 =head3 article_requests_finished
458
459 my @requests = $biblio->article_requests_finished
460
461 Returns the article requests associated with this patron that are completed
462
463 =cut
464
465 sub article_requests_finished {
466     my ( $self, $borrower ) = @_;
467
468     $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
469         {
470             borrowernumber => $self->id(),
471             -or          => [
472                 { status => Koha::ArticleRequest::Status::Completed },
473                 { status => Koha::ArticleRequest::Status::Canceled }
474             ]
475         }
476     );
477
478     return $self->{_article_requests_finished};
479 }
480
481 =head3 add_enrolment_fee_if_needed
482
483 my $enrolment_fee = $patron->add_enrolment_fee_if_needed;
484
485 Add enrolment fee for a patron if needed.
486
487 =cut
488
489 sub add_enrolment_fee_if_needed {
490     my ($self) = @_;
491     my $enrolment_fee = $self->category->enrolmentfee;
492     if ( $enrolment_fee && $enrolment_fee > 0 ) {
493         # insert fee in patron debts
494         C4::Accounts::manualinvoice( $self->borrowernumber, '', '', 'A', $enrolment_fee );
495     }
496     return $enrolment_fee || 0;
497 }
498
499 =head3 type
500
501 =cut
502
503 sub _type {
504     return 'Borrower';
505 }
506
507 =head1 AUTHOR
508
509 Kyle M Hall <kyle@bywatersolutions.com>
510 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
511
512 =cut
513
514 1;