Bug 18260: Koha::Biblio - Remove GetBiblio
[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::Checkouts;
28 use Koha::Database;
29 use Koha::DateUtils;
30 use Koha::Holds;
31 use Koha::Old::Checkouts;
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 use Koha::Club::Enrollments;
39
40 use base qw(Koha::Object);
41
42 =head1 NAME
43
44 Koha::Patron - Koha Patron Object class
45
46 =head1 API
47
48 =head2 Class Methods
49
50 =cut
51
52 =head3 delete
53
54 $patron->delete
55
56 Delete patron's holds, lists and finally the patron.
57
58 Lists owned by the borrower are deleted, but entries from the borrower to
59 other lists are kept.
60
61 =cut
62
63 sub delete {
64     my ($self) = @_;
65
66     my $deleted;
67     $self->_result->result_source->schema->txn_do(
68         sub {
69             # Delete Patron's holds
70             $self->holds->delete;
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 sub library {
131     my ( $self ) = @_;
132     return Koha::Library->_new_from_dbic($self->_result->branchcode);
133 }
134
135 =head3 guarantees
136
137 Returns the guarantees (list of Koha::Patron) of this patron
138
139 =cut
140
141 sub guarantees {
142     my ( $self ) = @_;
143
144     return Koha::Patrons->search( { guarantorid => $self->borrowernumber } );
145 }
146
147 =head3 housebound_profile
148
149 Returns the HouseboundProfile associated with this patron.
150
151 =cut
152
153 sub housebound_profile {
154     my ( $self ) = @_;
155     my $profile = $self->_result->housebound_profile;
156     return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
157         if ( $profile );
158     return;
159 }
160
161 =head3 housebound_role
162
163 Returns the HouseboundRole associated with this patron.
164
165 =cut
166
167 sub housebound_role {
168     my ( $self ) = @_;
169
170     my $role = $self->_result->housebound_role;
171     return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
172     return;
173 }
174
175 =head3 siblings
176
177 Returns the siblings of this patron.
178
179 =cut
180
181 sub siblings {
182     my ( $self ) = @_;
183
184     my $guarantor = $self->guarantor;
185
186     return unless $guarantor;
187
188     return Koha::Patrons->search(
189         {
190             guarantorid => {
191                 '!=' => undef,
192                 '=' => $guarantor->id,
193             },
194             borrowernumber => {
195                 '!=' => $self->borrowernumber,
196             }
197         }
198     );
199 }
200
201 =head3 wants_check_for_previous_checkout
202
203     $wants_check = $patron->wants_check_for_previous_checkout;
204
205 Return 1 if Koha needs to perform PrevIssue checking, else 0.
206
207 =cut
208
209 sub wants_check_for_previous_checkout {
210     my ( $self ) = @_;
211     my $syspref = C4::Context->preference("checkPrevCheckout");
212
213     # Simple cases
214     ## Hard syspref trumps all
215     return 1 if ($syspref eq 'hardyes');
216     return 0 if ($syspref eq 'hardno');
217     ## Now, patron pref trumps all
218     return 1 if ($self->checkprevcheckout eq 'yes');
219     return 0 if ($self->checkprevcheckout eq 'no');
220
221     # More complex: patron inherits -> determine category preference
222     my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
223     return 1 if ($checkPrevCheckoutByCat eq 'yes');
224     return 0 if ($checkPrevCheckoutByCat eq 'no');
225
226     # Finally: category preference is inherit, default to 0
227     if ($syspref eq 'softyes') {
228         return 1;
229     } else {
230         return 0;
231     }
232 }
233
234 =head3 do_check_for_previous_checkout
235
236     $do_check = $patron->do_check_for_previous_checkout($item);
237
238 Return 1 if the bib associated with $ITEM has previously been checked out to
239 $PATRON, 0 otherwise.
240
241 =cut
242
243 sub do_check_for_previous_checkout {
244     my ( $self, $item ) = @_;
245
246     # Find all items for bib and extract item numbers.
247     my @items = Koha::Items->search({biblionumber => $item->{biblionumber}});
248     my @item_nos;
249     foreach my $item (@items) {
250         push @item_nos, $item->itemnumber;
251     }
252
253     # Create (old)issues search criteria
254     my $criteria = {
255         borrowernumber => $self->borrowernumber,
256         itemnumber => \@item_nos,
257     };
258
259     # Check current issues table
260     my $issues = Koha::Checkouts->search($criteria);
261     return 1 if $issues->count; # 0 || N
262
263     # Check old issues table
264     my $old_issues = Koha::Old::Checkouts->search($criteria);
265     return $old_issues->count;  # 0 || N
266 }
267
268 =head3 is_debarred
269
270 my $debarment_expiration = $patron->is_debarred;
271
272 Returns the date a patron debarment will expire, or undef if the patron is not
273 debarred
274
275 =cut
276
277 sub is_debarred {
278     my ($self) = @_;
279
280     return unless $self->debarred;
281     return $self->debarred
282       if $self->debarred =~ '^9999'
283       or dt_from_string( $self->debarred ) > dt_from_string;
284     return;
285 }
286
287 =head3 is_expired
288
289 my $is_expired = $patron->is_expired;
290
291 Returns 1 if the patron is expired or 0;
292
293 =cut
294
295 sub is_expired {
296     my ($self) = @_;
297     return 0 unless $self->dateexpiry;
298     return 0 if $self->dateexpiry eq '0000-00-00';
299     return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
300     return 0;
301 }
302
303 =head3 is_going_to_expire
304
305 my $is_going_to_expire = $patron->is_going_to_expire;
306
307 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
308
309 =cut
310
311 sub is_going_to_expire {
312     my ($self) = @_;
313
314     my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
315
316     return 0 unless $delay;
317     return 0 unless $self->dateexpiry;
318     return 0 if $self->dateexpiry eq '0000-00-00';
319     return 1 if dt_from_string( $self->dateexpiry )->subtract( days => $delay ) < dt_from_string->truncate( to => 'day' );
320     return 0;
321 }
322
323 =head3 update_password
324
325 my $updated = $patron->update_password( $userid, $password );
326
327 Update the userid and the password of a patron.
328 If the userid already exists, returns and let DBIx::Class warns
329 This will add an entry to action_logs if BorrowersLog is set.
330
331 =cut
332
333 sub update_password {
334     my ( $self, $userid, $password ) = @_;
335     eval { $self->userid($userid)->store; };
336     return if $@; # Make sure the userid is not already in used by another patron
337     $self->update(
338         {
339             password       => $password,
340             login_attempts => 0,
341         }
342     );
343     logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
344     return 1;
345 }
346
347 =head3 renew_account
348
349 my $new_expiry_date = $patron->renew_account
350
351 Extending the subscription to the expiry date.
352
353 =cut
354
355 sub renew_account {
356     my ($self) = @_;
357     my $date;
358     if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
359         $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
360     } else {
361         $date =
362             C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
363             ? dt_from_string( $self->dateexpiry )
364             : dt_from_string;
365     }
366     my $expiry_date = $self->category->get_expiry_date($date);
367
368     $self->dateexpiry($expiry_date)->store;
369
370     $self->add_enrolment_fee_if_needed;
371
372     logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
373     return dt_from_string( $expiry_date )->truncate( to => 'day' );
374 }
375
376 =head3 has_overdues
377
378 my $has_overdues = $patron->has_overdues;
379
380 Returns the number of patron's overdues
381
382 =cut
383
384 sub has_overdues {
385     my ($self) = @_;
386     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
387     return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
388 }
389
390 =head3 track_login
391
392     $patron->track_login;
393     $patron->track_login({ force => 1 });
394
395     Tracks a (successful) login attempt.
396     The preference TrackLastPatronActivity must be enabled. Or you
397     should pass the force parameter.
398
399 =cut
400
401 sub track_login {
402     my ( $self, $params ) = @_;
403     return if
404         !$params->{force} &&
405         !C4::Context->preference('TrackLastPatronActivity');
406     $self->lastseen( dt_from_string() )->store;
407 }
408
409 =head3 move_to_deleted
410
411 my $is_moved = $patron->move_to_deleted;
412
413 Move a patron to the deletedborrowers table.
414 This can be done before deleting a patron, to make sure the data are not completely deleted.
415
416 =cut
417
418 sub move_to_deleted {
419     my ($self) = @_;
420     my $patron_infos = $self->unblessed;
421     delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
422     return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
423 }
424
425 =head3 article_requests
426
427 my @requests = $borrower->article_requests();
428 my $requests = $borrower->article_requests();
429
430 Returns either a list of ArticleRequests objects,
431 or an ArtitleRequests object, depending on the
432 calling context.
433
434 =cut
435
436 sub article_requests {
437     my ( $self ) = @_;
438
439     $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
440
441     return $self->{_article_requests};
442 }
443
444 =head3 article_requests_current
445
446 my @requests = $patron->article_requests_current
447
448 Returns the article requests associated with this patron that are incomplete
449
450 =cut
451
452 sub article_requests_current {
453     my ( $self ) = @_;
454
455     $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
456         {
457             borrowernumber => $self->id(),
458             -or          => [
459                 { status => Koha::ArticleRequest::Status::Pending },
460                 { status => Koha::ArticleRequest::Status::Processing }
461             ]
462         }
463     );
464
465     return $self->{_article_requests_current};
466 }
467
468 =head3 article_requests_finished
469
470 my @requests = $biblio->article_requests_finished
471
472 Returns the article requests associated with this patron that are completed
473
474 =cut
475
476 sub article_requests_finished {
477     my ( $self, $borrower ) = @_;
478
479     $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
480         {
481             borrowernumber => $self->id(),
482             -or          => [
483                 { status => Koha::ArticleRequest::Status::Completed },
484                 { status => Koha::ArticleRequest::Status::Canceled }
485             ]
486         }
487     );
488
489     return $self->{_article_requests_finished};
490 }
491
492 =head3 add_enrolment_fee_if_needed
493
494 my $enrolment_fee = $patron->add_enrolment_fee_if_needed;
495
496 Add enrolment fee for a patron if needed.
497
498 =cut
499
500 sub add_enrolment_fee_if_needed {
501     my ($self) = @_;
502     my $enrolment_fee = $self->category->enrolmentfee;
503     if ( $enrolment_fee && $enrolment_fee > 0 ) {
504         # insert fee in patron debts
505         C4::Accounts::manualinvoice( $self->borrowernumber, '', '', 'A', $enrolment_fee );
506     }
507     return $enrolment_fee || 0;
508 }
509
510 =head3 checkouts
511
512 my $issues = $patron->checkouts
513
514 =cut
515
516 sub checkouts {
517     my ($self) = @_;
518     my $issues = $self->_result->issues;
519     return Koha::Checkouts->_new_from_dbic( $issues );
520 }
521
522 =head3 get_overdues
523
524 my $overdue_items = $patron->get_overdues
525
526 Return the overdued items
527
528 =cut
529
530 sub get_overdues {
531     my ($self) = @_;
532     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
533     return $self->checkouts->search(
534         {
535             'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
536         },
537         {
538             prefetch => { item => { biblio => 'biblioitems' } },
539         }
540     );
541 }
542
543 =head3 get_age
544
545 my $age = $patron->get_age
546
547 Return the age of the patron
548
549 =cut
550
551 sub get_age {
552     my ($self)    = @_;
553     my $today_str = dt_from_string->strftime("%Y-%m-%d");
554     return unless $self->dateofbirth;
555     my $dob_str   = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
556
557     my ( $dob_y,   $dob_m,   $dob_d )   = split /-/, $dob_str;
558     my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
559
560     my $age = $today_y - $dob_y;
561     if ( $dob_m . $dob_d > $today_m . $today_d ) {
562         $age--;
563     }
564
565     return $age;
566 }
567
568 =head3 account
569
570 my $account = $patron->account
571
572 =cut
573
574 sub account {
575     my ($self) = @_;
576     return Koha::Account->new( { patron_id => $self->borrowernumber } );
577 }
578
579 =head3 holds
580
581 my $holds = $patron->holds
582
583 Return all the holds placed by this patron
584
585 =cut
586
587 sub holds {
588     my ($self) = @_;
589     my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
590     return Koha::Holds->_new_from_dbic($holds_rs);
591 }
592
593 =head3 first_valid_email_address
594
595 =cut
596
597 sub first_valid_email_address {
598     my ($self) = @_;
599
600     return $self->email() || $self->emailpro() || $self->B_email() || q{};
601 }
602
603 =head3 get_club_enrollments
604
605 =cut
606
607 sub get_club_enrollments {
608     my ( $self, $return_scalar ) = @_;
609
610     my $e = Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
611
612     return $e if $return_scalar;
613
614     return wantarray ? $e->as_list : $e;
615 }
616
617 =head3 get_enrollable_clubs
618
619 =cut
620
621 sub get_enrollable_clubs {
622     my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
623
624     my $params;
625     $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
626       if $is_enrollable_from_opac;
627     $params->{is_email_required} = 0 unless $self->first_valid_email_address();
628
629     $params->{borrower} = $self;
630
631     my $e = Koha::Clubs->get_enrollable($params);
632
633     return $e if $return_scalar;
634
635     return wantarray ? $e->as_list : $e;
636 }
637
638 =head3 account_locked
639
640 my $is_locked = $patron->account_locked
641
642 Return true if the patron has reach the maximum number of login attempts (see pref FailedLoginAttempts).
643 Otherwise return false.
644 If the pref is not set (empty string, null or 0), the feature is considered as disabled.
645
646 =cut
647
648 sub account_locked {
649     my ($self) = @_;
650     my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
651     return ( $FailedLoginAttempts
652           and $self->login_attempts
653           and $self->login_attempts >= $FailedLoginAttempts )? 1 : 0;
654 }
655
656 =head3 type
657
658 =cut
659
660 sub _type {
661     return 'Borrower';
662 }
663
664 =head1 AUTHOR
665
666 Kyle M Hall <kyle@bywatersolutions.com>
667 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
668
669 =cut
670
671 1;