Bug 22144: (QA follow-up) Prepare the ground for other formats
[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 use List::MoreUtils qw( uniq );
25 use JSON qw( to_json );
26 use Text::Unaccent qw( unac_string );
27
28 use C4::Accounts;
29 use C4::Context;
30 use C4::Log;
31 use Koha::AuthUtils;
32 use Koha::Checkouts;
33 use Koha::Database;
34 use Koha::DateUtils;
35 use Koha::Exceptions::Password;
36 use Koha::Holds;
37 use Koha::Old::Checkouts;
38 use Koha::Patron::Categories;
39 use Koha::Patron::HouseboundProfile;
40 use Koha::Patron::HouseboundRole;
41 use Koha::Patron::Images;
42 use Koha::Patrons;
43 use Koha::Virtualshelves;
44 use Koha::Club::Enrollments;
45 use Koha::Account;
46 use Koha::Subscription::Routinglists;
47
48 use base qw(Koha::Object);
49
50 our $RESULTSET_PATRON_ID_MAPPING = {
51     Accountline          => 'borrowernumber',
52     Aqbasketuser         => 'borrowernumber',
53     Aqbudget             => 'budget_owner_id',
54     Aqbudgetborrower     => 'borrowernumber',
55     ArticleRequest       => 'borrowernumber',
56     BorrowerAttribute    => 'borrowernumber',
57     BorrowerDebarment    => 'borrowernumber',
58     BorrowerFile         => 'borrowernumber',
59     BorrowerModification => 'borrowernumber',
60     ClubEnrollment       => 'borrowernumber',
61     Issue                => 'borrowernumber',
62     ItemsLastBorrower    => 'borrowernumber',
63     Linktracker          => 'borrowernumber',
64     Message              => 'borrowernumber',
65     MessageQueue         => 'borrowernumber',
66     OldIssue             => 'borrowernumber',
67     OldReserve           => 'borrowernumber',
68     Rating               => 'borrowernumber',
69     Reserve              => 'borrowernumber',
70     Review               => 'borrowernumber',
71     SearchHistory        => 'userid',
72     Statistic            => 'borrowernumber',
73     Suggestion           => 'suggestedby',
74     TagAll               => 'borrowernumber',
75     Virtualshelfcontent  => 'borrowernumber',
76     Virtualshelfshare    => 'borrowernumber',
77     Virtualshelve        => 'owner',
78 };
79
80 =head1 NAME
81
82 Koha::Patron - Koha Patron Object class
83
84 =head1 API
85
86 =head2 Class Methods
87
88 =cut
89
90 =head3 new
91
92 =cut
93
94 sub new {
95     my ( $class, $params ) = @_;
96
97     return $class->SUPER::new($params);
98 }
99
100 =head3 fixup_cardnumber
101
102 Autogenerate next cardnumber from highest value found in database
103
104 =cut
105
106 sub fixup_cardnumber {
107     my ( $self ) = @_;
108     my $max = Koha::Patrons->search({
109         cardnumber => {-regexp => '^-?[0-9]+$'}
110     }, {
111         select => \'CAST(cardnumber AS SIGNED)',
112         as => ['cast_cardnumber']
113     })->_resultset->get_column('cast_cardnumber')->max;
114     $self->cardnumber(($max || 0) +1);
115 }
116
117 =head3 trim_whitespace
118
119 trim whitespace from data which has some non-whitespace in it.
120 Could be moved to Koha::Object if need to be reused
121
122 =cut
123
124 sub trim_whitespaces {
125     my( $self ) = @_;
126
127     my $schema  = Koha::Database->new->schema;
128     my @columns = $schema->source($self->_type)->columns;
129
130     for my $column( @columns ) {
131         my $value = $self->$column;
132         if ( defined $value ) {
133             $value =~ s/^\s*|\s*$//g;
134             $self->$column($value);
135         }
136     }
137     return $self;
138 }
139
140 =head3 plain_text_password
141
142 $patron->plain_text_password( $password );
143
144 stores a copy of the unencrypted password in the object
145 for use in code before encrypting for db
146
147 =cut
148
149 sub plain_text_password {
150     my ( $self, $password ) = @_;
151     if ( $password ) {
152         $self->{_plain_text_password} = $password;
153         return $self;
154     }
155     return $self->{_plain_text_password}
156         if $self->{_plain_text_password};
157
158     return;
159 }
160
161 =head3 store
162
163 Patron specific store method to cleanup record
164 and do other necessary things before saving
165 to db
166
167 =cut
168
169 sub store {
170     my ($self) = @_;
171
172     $self->_result->result_source->schema->txn_do(
173         sub {
174             if (
175                 C4::Context->preference("autoMemberNum")
176                 and ( not defined $self->cardnumber
177                     or $self->cardnumber eq '' )
178               )
179             {
180                 # Warning: The caller is responsible for locking the members table in write
181                 # mode, to avoid database corruption.
182                 # We are in a transaction but the table is not locked
183                 $self->fixup_cardnumber;
184             }
185
186             unless( $self->category->in_storage ) {
187                 Koha::Exceptions::Object::FKConstraint->throw(
188                     broken_fk => 'categorycode',
189                     value     => $self->categorycode,
190                 );
191             }
192
193             $self->trim_whitespaces;
194
195             # We don't want invalid dates in the db (mysql has a bad habit of inserting 0000-00-00)
196             $self->dateofbirth(undef)  unless $self->dateofbirth;
197             $self->debarred(undef)     unless $self->debarred;
198             $self->date_renewed(undef) unless $self->date_renewed;
199             $self->lastseen(undef)     unless $self->lastseen;
200
201             if ( defined $self->updated_on and not $self->updated_on ) {
202                 $self->updated_on(undef);
203             }
204
205             # Set default values if not set
206             $self->sms_provider_id(undef) unless $self->sms_provider_id;
207             $self->guarantorid(undef)     unless $self->guarantorid;
208
209             # If flags == 0 or flags == '' => no permission
210             $self->flags(undef) unless $self->flags;
211
212             # tinyint or int
213             $self->gonenoaddress(0)  unless $self->gonenoaddress;
214             $self->login_attempts(0) unless $self->login_attempts;
215             $self->privacy_guarantor_checkouts(0) unless $self->privacy_guarantor_checkouts;
216             $self->lost(0)           unless $self->lost;
217
218             unless ( $self->in_storage ) {    #AddMember
219
220                 # Generate a valid userid/login if needed
221                 $self->generate_userid
222                   if not $self->userid or not $self->has_valid_userid;
223
224                 # Add expiration date if it isn't already there
225                 unless ( $self->dateexpiry ) {
226                     $self->dateexpiry( $self->category->get_expiry_date );
227                 }
228
229                 # Add enrollment date if it isn't already there
230                 unless ( $self->dateenrolled ) {
231                     $self->dateenrolled(dt_from_string);
232                 }
233
234                 # Set the privacy depending on the patron's category
235                 my $default_privacy = $self->category->default_privacy || q{};
236                 $default_privacy =
237                     $default_privacy eq 'default' ? 1
238                   : $default_privacy eq 'never'   ? 2
239                   : $default_privacy eq 'forever' ? 0
240                   :                                                   undef;
241                 $self->privacy($default_privacy);
242
243                 unless ( defined $self->privacy_guarantor_checkouts ) {
244                     $self->privacy_guarantor_checkouts(0);
245                 }
246
247                 # Make a copy of the plain text password for later use
248                 $self->plain_text_password( $self->password );
249
250                 # Create a disabled account if no password provided
251                 $self->password( $self->password
252                     ? Koha::AuthUtils::hash_password( $self->password )
253                     : '!' );
254
255                 $self->borrowernumber(undef);
256
257                 $self = $self->SUPER::store;
258
259                 $self->add_enrolment_fee_if_needed;
260
261                 logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
262                   if C4::Context->preference("BorrowersLog");
263             }
264             else {    #ModMember
265
266                 # Come from ModMember, but should not be possible (?)
267                 $self->dateenrolled(undef) unless $self->dateenrolled;
268                 $self->dateexpiry(undef)   unless $self->dateexpiry;
269
270
271                 my $self_from_storage = $self->get_from_storage;
272                 # FIXME We should not deal with that here, callers have to do this job
273                 # Moved from ModMember to prevent regressions
274                 unless ( $self->userid ) {
275                     my $stored_userid = $self_from_storage->userid;
276                     $self->userid($stored_userid);
277                 }
278
279                 # Password must be updated using $self->set_password
280                 $self->password($self_from_storage->password);
281
282                 if ( C4::Context->preference('FeeOnChangePatronCategory')
283                     and $self->category->categorycode ne
284                     $self_from_storage->category->categorycode )
285                 {
286                     $self->add_enrolment_fee_if_needed;
287                 }
288
289                 my $borrowers_log = C4::Context->preference("BorrowersLog");
290                 my $previous_cardnumber = $self_from_storage->cardnumber;
291                 if ($borrowers_log
292                     && ( !defined $previous_cardnumber
293                         || $previous_cardnumber ne $self->cardnumber )
294                     )
295                 {
296                     logaction(
297                         "MEMBERS",
298                         "MODIFY",
299                         $self->borrowernumber,
300                         to_json(
301                             {
302                                 cardnumber_replaced => {
303                                     previous_cardnumber => $previous_cardnumber,
304                                     new_cardnumber      => $self->cardnumber,
305                                 }
306                             },
307                             { utf8 => 1, pretty => 1 }
308                         )
309                     );
310                 }
311
312                 logaction( "MEMBERS", "MODIFY", $self->borrowernumber,
313                     "UPDATE (executed w/ arg: " . $self->borrowernumber . ")" )
314                   if $borrowers_log;
315
316                 $self = $self->SUPER::store;
317             }
318         }
319     );
320     return $self;
321 }
322
323 =head3 delete
324
325 $patron->delete
326
327 Delete patron's holds, lists and finally the patron.
328
329 Lists owned by the borrower are deleted, but entries from the borrower to
330 other lists are kept.
331
332 =cut
333
334 sub delete {
335     my ($self) = @_;
336
337     my $deleted;
338     $self->_result->result_source->schema->txn_do(
339         sub {
340             # Delete Patron's holds
341             $self->holds->delete;
342
343             # Delete all lists and all shares of this borrower
344             # Consistent with the approach Koha uses on deleting individual lists
345             # Note that entries in virtualshelfcontents added by this borrower to
346             # lists of others will be handled by a table constraint: the borrower
347             # is set to NULL in those entries.
348             # NOTE:
349             # We could handle the above deletes via a constraint too.
350             # But a new BZ report 11889 has been opened to discuss another approach.
351             # Instead of deleting we could also disown lists (based on a pref).
352             # In that way we could save shared and public lists.
353             # The current table constraints support that idea now.
354             # This pref should then govern the results of other routines/methods such as
355             # Koha::Virtualshelf->new->delete too.
356             # FIXME Could be $patron->get_lists
357             $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
358
359             $deleted = $self->SUPER::delete;
360
361             logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
362         }
363     );
364     return $deleted;
365 }
366
367
368 =head3 category
369
370 my $patron_category = $patron->category
371
372 Return the patron category for this patron
373
374 =cut
375
376 sub category {
377     my ( $self ) = @_;
378     return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
379 }
380
381 =head3 guarantor
382
383 Returns a Koha::Patron object for this patron's guarantor
384
385 =cut
386
387 sub guarantor {
388     my ( $self ) = @_;
389
390     return unless $self->guarantorid();
391
392     return Koha::Patrons->find( $self->guarantorid() );
393 }
394
395 sub image {
396     my ( $self ) = @_;
397
398     return scalar Koha::Patron::Images->find( $self->borrowernumber );
399 }
400
401 sub library {
402     my ( $self ) = @_;
403     return Koha::Library->_new_from_dbic($self->_result->branchcode);
404 }
405
406 =head3 guarantees
407
408 Returns the guarantees (list of Koha::Patron) of this patron
409
410 =cut
411
412 sub guarantees {
413     my ( $self ) = @_;
414
415     return Koha::Patrons->search( { guarantorid => $self->borrowernumber }, { order_by => { -asc => ['surname','firstname'] } } );
416 }
417
418 =head3 housebound_profile
419
420 Returns the HouseboundProfile associated with this patron.
421
422 =cut
423
424 sub housebound_profile {
425     my ( $self ) = @_;
426     my $profile = $self->_result->housebound_profile;
427     return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
428         if ( $profile );
429     return;
430 }
431
432 =head3 housebound_role
433
434 Returns the HouseboundRole associated with this patron.
435
436 =cut
437
438 sub housebound_role {
439     my ( $self ) = @_;
440
441     my $role = $self->_result->housebound_role;
442     return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
443     return;
444 }
445
446 =head3 siblings
447
448 Returns the siblings of this patron.
449
450 =cut
451
452 sub siblings {
453     my ( $self ) = @_;
454
455     my $guarantor = $self->guarantor;
456
457     return unless $guarantor;
458
459     return Koha::Patrons->search(
460         {
461             guarantorid => {
462                 '!=' => undef,
463                 '=' => $guarantor->id,
464             },
465             borrowernumber => {
466                 '!=' => $self->borrowernumber,
467             }
468         }
469     );
470 }
471
472 =head3 merge_with
473
474     my $patron = Koha::Patrons->find($id);
475     $patron->merge_with( \@patron_ids );
476
477     This subroutine merges a list of patrons into the patron record. This is accomplished by finding
478     all related patron ids for the patrons to be merged in other tables and changing the ids to be that
479     of the keeper patron.
480
481 =cut
482
483 sub merge_with {
484     my ( $self, $patron_ids ) = @_;
485
486     my @patron_ids = @{ $patron_ids };
487
488     # Ensure the keeper isn't in the list of patrons to merge
489     @patron_ids = grep { $_ ne $self->id } @patron_ids;
490
491     my $schema = Koha::Database->new()->schema();
492
493     my $results;
494
495     $self->_result->result_source->schema->txn_do( sub {
496         foreach my $patron_id (@patron_ids) {
497             my $patron = Koha::Patrons->find( $patron_id );
498
499             next unless $patron;
500
501             # Unbless for safety, the patron will end up being deleted
502             $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
503
504             while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
505                 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
506                 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
507                 $rs->update({ $field => $self->id });
508             }
509
510             $patron->move_to_deleted();
511             $patron->delete();
512         }
513     });
514
515     return $results;
516 }
517
518
519
520 =head3 wants_check_for_previous_checkout
521
522     $wants_check = $patron->wants_check_for_previous_checkout;
523
524 Return 1 if Koha needs to perform PrevIssue checking, else 0.
525
526 =cut
527
528 sub wants_check_for_previous_checkout {
529     my ( $self ) = @_;
530     my $syspref = C4::Context->preference("checkPrevCheckout");
531
532     # Simple cases
533     ## Hard syspref trumps all
534     return 1 if ($syspref eq 'hardyes');
535     return 0 if ($syspref eq 'hardno');
536     ## Now, patron pref trumps all
537     return 1 if ($self->checkprevcheckout eq 'yes');
538     return 0 if ($self->checkprevcheckout eq 'no');
539
540     # More complex: patron inherits -> determine category preference
541     my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
542     return 1 if ($checkPrevCheckoutByCat eq 'yes');
543     return 0 if ($checkPrevCheckoutByCat eq 'no');
544
545     # Finally: category preference is inherit, default to 0
546     if ($syspref eq 'softyes') {
547         return 1;
548     } else {
549         return 0;
550     }
551 }
552
553 =head3 do_check_for_previous_checkout
554
555     $do_check = $patron->do_check_for_previous_checkout($item);
556
557 Return 1 if the bib associated with $ITEM has previously been checked out to
558 $PATRON, 0 otherwise.
559
560 =cut
561
562 sub do_check_for_previous_checkout {
563     my ( $self, $item ) = @_;
564
565     # Find all items for bib and extract item numbers.
566     my @items = Koha::Items->search({biblionumber => $item->{biblionumber}});
567     my @item_nos;
568     foreach my $item (@items) {
569         push @item_nos, $item->itemnumber;
570     }
571
572     # Create (old)issues search criteria
573     my $criteria = {
574         borrowernumber => $self->borrowernumber,
575         itemnumber => \@item_nos,
576     };
577
578     # Check current issues table
579     my $issues = Koha::Checkouts->search($criteria);
580     return 1 if $issues->count; # 0 || N
581
582     # Check old issues table
583     my $old_issues = Koha::Old::Checkouts->search($criteria);
584     return $old_issues->count;  # 0 || N
585 }
586
587 =head3 is_debarred
588
589 my $debarment_expiration = $patron->is_debarred;
590
591 Returns the date a patron debarment will expire, or undef if the patron is not
592 debarred
593
594 =cut
595
596 sub is_debarred {
597     my ($self) = @_;
598
599     return unless $self->debarred;
600     return $self->debarred
601       if $self->debarred =~ '^9999'
602       or dt_from_string( $self->debarred ) > dt_from_string;
603     return;
604 }
605
606 =head3 is_expired
607
608 my $is_expired = $patron->is_expired;
609
610 Returns 1 if the patron is expired or 0;
611
612 =cut
613
614 sub is_expired {
615     my ($self) = @_;
616     return 0 unless $self->dateexpiry;
617     return 0 if $self->dateexpiry =~ '^9999';
618     return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
619     return 0;
620 }
621
622 =head3 is_going_to_expire
623
624 my $is_going_to_expire = $patron->is_going_to_expire;
625
626 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
627
628 =cut
629
630 sub is_going_to_expire {
631     my ($self) = @_;
632
633     my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
634
635     return 0 unless $delay;
636     return 0 unless $self->dateexpiry;
637     return 0 if $self->dateexpiry =~ '^9999';
638     return 1 if dt_from_string( $self->dateexpiry )->subtract( days => $delay ) < dt_from_string->truncate( to => 'day' );
639     return 0;
640 }
641
642 =head3 set_password
643
644     $patron->set_password({ password => $plain_text_password [, skip_validation => 1 ] });
645
646 Set the patron's password.
647
648 =head4 Exceptions
649
650 The passed string is validated against the current password enforcement policy.
651 Validation can be skipped by passing the I<skip_validation> parameter.
652
653 Exceptions are thrown if the password is not good enough.
654
655 =over 4
656
657 =item Koha::Exceptions::Password::TooShort
658
659 =item Koha::Exceptions::Password::WhitespaceCharacters
660
661 =item Koha::Exceptions::Password::TooWeak
662
663 =back
664
665 =cut
666
667 sub set_password {
668     my ( $self, $args ) = @_;
669
670     my $password = $args->{password};
671
672     unless ( $args->{skip_validation} ) {
673         my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password );
674
675         if ( !$is_valid ) {
676             if ( $error eq 'too_short' ) {
677                 my $min_length = C4::Context->preference('minPasswordLength');
678                 $min_length = 3 if not $min_length or $min_length < 3;
679
680                 my $password_length = length($password);
681                 Koha::Exceptions::Password::TooShort->throw(
682                     length => $password_length, min_length => $min_length );
683             }
684             elsif ( $error eq 'has_whitespaces' ) {
685                 Koha::Exceptions::Password::WhitespaceCharacters->throw();
686             }
687             elsif ( $error eq 'too_weak' ) {
688                 Koha::Exceptions::Password::TooWeak->throw();
689             }
690         }
691     }
692
693     my $digest = Koha::AuthUtils::hash_password($password);
694     $self->update(
695         {   password       => $digest,
696             login_attempts => 0,
697         }
698     );
699
700     logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" )
701         if C4::Context->preference("BorrowersLog");
702
703     return $self;
704 }
705
706
707 =head3 renew_account
708
709 my $new_expiry_date = $patron->renew_account
710
711 Extending the subscription to the expiry date.
712
713 =cut
714
715 sub renew_account {
716     my ($self) = @_;
717     my $date;
718     if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
719         $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
720     } else {
721         $date =
722             C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
723             ? dt_from_string( $self->dateexpiry )
724             : dt_from_string;
725     }
726     my $expiry_date = $self->category->get_expiry_date($date);
727
728     $self->dateexpiry($expiry_date);
729     $self->date_renewed( dt_from_string() );
730     $self->store();
731
732     $self->add_enrolment_fee_if_needed;
733
734     logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
735     return dt_from_string( $expiry_date )->truncate( to => 'day' );
736 }
737
738 =head3 has_overdues
739
740 my $has_overdues = $patron->has_overdues;
741
742 Returns the number of patron's overdues
743
744 =cut
745
746 sub has_overdues {
747     my ($self) = @_;
748     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
749     return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
750 }
751
752 =head3 track_login
753
754     $patron->track_login;
755     $patron->track_login({ force => 1 });
756
757     Tracks a (successful) login attempt.
758     The preference TrackLastPatronActivity must be enabled. Or you
759     should pass the force parameter.
760
761 =cut
762
763 sub track_login {
764     my ( $self, $params ) = @_;
765     return if
766         !$params->{force} &&
767         !C4::Context->preference('TrackLastPatronActivity');
768     $self->lastseen( dt_from_string() )->store;
769 }
770
771 =head3 move_to_deleted
772
773 my $is_moved = $patron->move_to_deleted;
774
775 Move a patron to the deletedborrowers table.
776 This can be done before deleting a patron, to make sure the data are not completely deleted.
777
778 =cut
779
780 sub move_to_deleted {
781     my ($self) = @_;
782     my $patron_infos = $self->unblessed;
783     delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
784     return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
785 }
786
787 =head3 article_requests
788
789 my @requests = $borrower->article_requests();
790 my $requests = $borrower->article_requests();
791
792 Returns either a list of ArticleRequests objects,
793 or an ArtitleRequests object, depending on the
794 calling context.
795
796 =cut
797
798 sub article_requests {
799     my ( $self ) = @_;
800
801     $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
802
803     return $self->{_article_requests};
804 }
805
806 =head3 article_requests_current
807
808 my @requests = $patron->article_requests_current
809
810 Returns the article requests associated with this patron that are incomplete
811
812 =cut
813
814 sub article_requests_current {
815     my ( $self ) = @_;
816
817     $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
818         {
819             borrowernumber => $self->id(),
820             -or          => [
821                 { status => Koha::ArticleRequest::Status::Pending },
822                 { status => Koha::ArticleRequest::Status::Processing }
823             ]
824         }
825     );
826
827     return $self->{_article_requests_current};
828 }
829
830 =head3 article_requests_finished
831
832 my @requests = $biblio->article_requests_finished
833
834 Returns the article requests associated with this patron that are completed
835
836 =cut
837
838 sub article_requests_finished {
839     my ( $self, $borrower ) = @_;
840
841     $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
842         {
843             borrowernumber => $self->id(),
844             -or          => [
845                 { status => Koha::ArticleRequest::Status::Completed },
846                 { status => Koha::ArticleRequest::Status::Canceled }
847             ]
848         }
849     );
850
851     return $self->{_article_requests_finished};
852 }
853
854 =head3 add_enrolment_fee_if_needed
855
856 my $enrolment_fee = $patron->add_enrolment_fee_if_needed;
857
858 Add enrolment fee for a patron if needed.
859
860 =cut
861
862 sub add_enrolment_fee_if_needed {
863     my ($self) = @_;
864     my $enrolment_fee = $self->category->enrolmentfee;
865     if ( $enrolment_fee && $enrolment_fee > 0 ) {
866         # insert fee in patron debts
867         C4::Accounts::manualinvoice( $self->borrowernumber, '', '', 'A', $enrolment_fee );
868     }
869     return $enrolment_fee || 0;
870 }
871
872 =head3 checkouts
873
874 my $checkouts = $patron->checkouts
875
876 =cut
877
878 sub checkouts {
879     my ($self) = @_;
880     my $checkouts = $self->_result->issues;
881     return Koha::Checkouts->_new_from_dbic( $checkouts );
882 }
883
884 =head3 pending_checkouts
885
886 my $pending_checkouts = $patron->pending_checkouts
887
888 This method will return the same as $self->checkouts, but with a prefetch on
889 items, biblio and biblioitems.
890
891 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
892
893 It should not be used directly, prefer to access fields you need instead of
894 retrieving all these fields in one go.
895
896
897 =cut
898
899 sub pending_checkouts {
900     my( $self ) = @_;
901     my $checkouts = $self->_result->issues->search(
902         {},
903         {
904             order_by => [
905                 { -desc => 'me.timestamp' },
906                 { -desc => 'issuedate' },
907                 { -desc => 'issue_id' }, # Sort by issue_id should be enough
908             ],
909             prefetch => { item => { biblio => 'biblioitems' } },
910         }
911     );
912     return Koha::Checkouts->_new_from_dbic( $checkouts );
913 }
914
915 =head3 old_checkouts
916
917 my $old_checkouts = $patron->old_checkouts
918
919 =cut
920
921 sub old_checkouts {
922     my ($self) = @_;
923     my $old_checkouts = $self->_result->old_issues;
924     return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
925 }
926
927 =head3 get_overdues
928
929 my $overdue_items = $patron->get_overdues
930
931 Return the overdue items
932
933 =cut
934
935 sub get_overdues {
936     my ($self) = @_;
937     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
938     return $self->checkouts->search(
939         {
940             'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
941         },
942         {
943             prefetch => { item => { biblio => 'biblioitems' } },
944         }
945     );
946 }
947
948 =head3 get_routing_lists
949
950 my @routinglists = $patron->get_routing_lists
951
952 Returns the routing lists a patron is subscribed to.
953
954 =cut
955
956 sub get_routing_lists {
957     my ($self) = @_;
958     my $routing_list_rs = $self->_result->subscriptionroutinglists;
959     return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
960 }
961
962 =head3 get_age
963
964 my $age = $patron->get_age
965
966 Return the age of the patron
967
968 =cut
969
970 sub get_age {
971     my ($self)    = @_;
972     my $today_str = dt_from_string->strftime("%Y-%m-%d");
973     return unless $self->dateofbirth;
974     my $dob_str   = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
975
976     my ( $dob_y,   $dob_m,   $dob_d )   = split /-/, $dob_str;
977     my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
978
979     my $age = $today_y - $dob_y;
980     if ( $dob_m . $dob_d > $today_m . $today_d ) {
981         $age--;
982     }
983
984     return $age;
985 }
986
987 =head3 account
988
989 my $account = $patron->account
990
991 =cut
992
993 sub account {
994     my ($self) = @_;
995     return Koha::Account->new( { patron_id => $self->borrowernumber } );
996 }
997
998 =head3 holds
999
1000 my $holds = $patron->holds
1001
1002 Return all the holds placed by this patron
1003
1004 =cut
1005
1006 sub holds {
1007     my ($self) = @_;
1008     my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1009     return Koha::Holds->_new_from_dbic($holds_rs);
1010 }
1011
1012 =head3 old_holds
1013
1014 my $old_holds = $patron->old_holds
1015
1016 Return all the historical holds for this patron
1017
1018 =cut
1019
1020 sub old_holds {
1021     my ($self) = @_;
1022     my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1023     return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1024 }
1025
1026 =head3 notice_email_address
1027
1028   my $email = $patron->notice_email_address;
1029
1030 Return the email address of patron used for notices.
1031 Returns the empty string if no email address.
1032
1033 =cut
1034
1035 sub notice_email_address{
1036     my ( $self ) = @_;
1037
1038     my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1039     # if syspref is set to 'first valid' (value == OFF), look up email address
1040     if ( $which_address eq 'OFF' ) {
1041         return $self->first_valid_email_address;
1042     }
1043
1044     return $self->$which_address || '';
1045 }
1046
1047 =head3 first_valid_email_address
1048
1049 my $first_valid_email_address = $patron->first_valid_email_address
1050
1051 Return the first valid email address for a patron.
1052 For now, the order  is defined as email, emailpro, B_email.
1053 Returns the empty string if the borrower has no email addresses.
1054
1055 =cut
1056
1057 sub first_valid_email_address {
1058     my ($self) = @_;
1059
1060     return $self->email() || $self->emailpro() || $self->B_email() || q{};
1061 }
1062
1063 =head3 get_club_enrollments
1064
1065 =cut
1066
1067 sub get_club_enrollments {
1068     my ( $self, $return_scalar ) = @_;
1069
1070     my $e = Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1071
1072     return $e if $return_scalar;
1073
1074     return wantarray ? $e->as_list : $e;
1075 }
1076
1077 =head3 get_enrollable_clubs
1078
1079 =cut
1080
1081 sub get_enrollable_clubs {
1082     my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
1083
1084     my $params;
1085     $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1086       if $is_enrollable_from_opac;
1087     $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1088
1089     $params->{borrower} = $self;
1090
1091     my $e = Koha::Clubs->get_enrollable($params);
1092
1093     return $e if $return_scalar;
1094
1095     return wantarray ? $e->as_list : $e;
1096 }
1097
1098 =head3 account_locked
1099
1100 my $is_locked = $patron->account_locked
1101
1102 Return true if the patron has reach the maximum number of login attempts (see pref FailedLoginAttempts).
1103 Otherwise return false.
1104 If the pref is not set (empty string, null or 0), the feature is considered as disabled.
1105
1106 =cut
1107
1108 sub account_locked {
1109     my ($self) = @_;
1110     my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1111     return ( $FailedLoginAttempts
1112           and $self->login_attempts
1113           and $self->login_attempts >= $FailedLoginAttempts )? 1 : 0;
1114 }
1115
1116 =head3 can_see_patron_infos
1117
1118 my $can_see = $patron->can_see_patron_infos( $patron );
1119
1120 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1121
1122 =cut
1123
1124 sub can_see_patron_infos {
1125     my ( $self, $patron ) = @_;
1126     return $self->can_see_patrons_from( $patron->library->branchcode );
1127 }
1128
1129 =head3 can_see_patrons_from
1130
1131 my $can_see = $patron->can_see_patrons_from( $branchcode );
1132
1133 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1134
1135 =cut
1136
1137 sub can_see_patrons_from {
1138     my ( $self, $branchcode ) = @_;
1139     my $can = 0;
1140     if ( $self->branchcode eq $branchcode ) {
1141         $can = 1;
1142     } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1143         $can = 1;
1144     } elsif ( my $library_groups = $self->library->library_groups ) {
1145         while ( my $library_group = $library_groups->next ) {
1146             if ( $library_group->parent->has_child( $branchcode ) ) {
1147                 $can = 1;
1148                 last;
1149             }
1150         }
1151     }
1152     return $can;
1153 }
1154
1155 =head3 libraries_where_can_see_patrons
1156
1157 my $libraries = $patron-libraries_where_can_see_patrons;
1158
1159 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1160 The branchcodes are arbitrarily returned sorted.
1161 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1162
1163 An empty array means no restriction, the patron can see patron's infos from any libraries.
1164
1165 =cut
1166
1167 sub libraries_where_can_see_patrons {
1168     my ( $self ) = @_;
1169     my $userenv = C4::Context->userenv;
1170
1171     return () unless $userenv; # For tests, but userenv should be defined in tests...
1172
1173     my @restricted_branchcodes;
1174     if (C4::Context::only_my_library) {
1175         push @restricted_branchcodes, $self->branchcode;
1176     }
1177     else {
1178         unless (
1179             $self->has_permission(
1180                 { borrowers => 'view_borrower_infos_from_any_libraries' }
1181             )
1182           )
1183         {
1184             my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1185             if ( $library_groups->count )
1186             {
1187                 while ( my $library_group = $library_groups->next ) {
1188                     my $parent = $library_group->parent;
1189                     if ( $parent->has_child( $self->branchcode ) ) {
1190                         push @restricted_branchcodes, $parent->children->get_column('branchcode');
1191                     }
1192                 }
1193             }
1194
1195             @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1196         }
1197     }
1198
1199     @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1200     @restricted_branchcodes = uniq(@restricted_branchcodes);
1201     @restricted_branchcodes = sort(@restricted_branchcodes);
1202     return @restricted_branchcodes;
1203 }
1204
1205 sub has_permission {
1206     my ( $self, $flagsrequired ) = @_;
1207     return unless $self->userid;
1208     # TODO code from haspermission needs to be moved here!
1209     return C4::Auth::haspermission( $self->userid, $flagsrequired );
1210 }
1211
1212 =head3 is_adult
1213
1214 my $is_adult = $patron->is_adult
1215
1216 Return true if the patron has a category with a type Adult (A) or Organization (I)
1217
1218 =cut
1219
1220 sub is_adult {
1221     my ( $self ) = @_;
1222     return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1223 }
1224
1225 =head3 is_child
1226
1227 my $is_child = $patron->is_child
1228
1229 Return true if the patron has a category with a type Child (C)
1230
1231 =cut
1232 sub is_child {
1233     my( $self ) = @_;
1234     return $self->category->category_type eq 'C' ? 1 : 0;
1235 }
1236
1237 =head3 has_valid_userid
1238
1239 my $patron = Koha::Patrons->find(42);
1240 $patron->userid( $new_userid );
1241 my $has_a_valid_userid = $patron->has_valid_userid
1242
1243 my $patron = Koha::Patron->new( $params );
1244 my $has_a_valid_userid = $patron->has_valid_userid
1245
1246 Return true if the current userid of this patron is valid/unique, otherwise false.
1247
1248 Note that this should be done in $self->store instead and raise an exception if needed.
1249
1250 =cut
1251
1252 sub has_valid_userid {
1253     my ($self) = @_;
1254
1255     return 0 unless $self->userid;
1256
1257     return 0 if ( $self->userid eq C4::Context->config('user') );    # DB user
1258
1259     my $already_exists = Koha::Patrons->search(
1260         {
1261             userid => $self->userid,
1262             (
1263                 $self->in_storage
1264                 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1265                 : ()
1266             ),
1267         }
1268     )->count;
1269     return $already_exists ? 0 : 1;
1270 }
1271
1272 =head3 generate_userid
1273
1274 my $patron = Koha::Patron->new( $params );
1275 $patron->generate_userid
1276
1277 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1278
1279 Set a generated userid ($firstname.$surname if there is a $firstname, or $surname if there is no value in $firstname) plus offset (0 if the $userid is unique, or a higher numeric value if not unique).
1280
1281 =cut
1282
1283 sub generate_userid {
1284     my ($self) = @_;
1285     my $offset = 0;
1286     my $firstname = $self->firstname // q{};
1287     my $surname = $self->surname // q{};
1288     #The script will "do" the following code and increment the $offset until the generated userid is unique
1289     do {
1290       $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1291       $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1292       my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1293       $userid = unac_string('utf-8',$userid);
1294       $userid .= $offset unless $offset == 0;
1295       $self->userid( $userid );
1296       $offset++;
1297      } while (! $self->has_valid_userid );
1298
1299      return $self;
1300
1301 }
1302
1303 =head2 Internal methods
1304
1305 =head3 _type
1306
1307 =cut
1308
1309 sub _type {
1310     return 'Borrower';
1311 }
1312
1313 =head1 AUTHOR
1314
1315 Kyle M Hall <kyle@bywatersolutions.com>
1316 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1317
1318 =cut
1319
1320 1;