e202fecdb70ed61f09d67680745bdc9a2c0e4b6f
[srvgit] / 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
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20
21 use Modern::Perl;
22
23 use Carp;
24 use List::MoreUtils qw( any uniq );
25 use JSON qw( to_json );
26 use Unicode::Normalize;
27
28 use C4::Context;
29 use C4::Log;
30 use Koha::Account;
31 use Koha::ArticleRequests;
32 use C4::Letters qw( GetPreparedLetter EnqueueLetter );
33 use Koha::AuthUtils;
34 use Koha::Checkouts;
35 use Koha::Club::Enrollments;
36 use Koha::Database;
37 use Koha::DateUtils;
38 use Koha::Exceptions::Password;
39 use Koha::Holds;
40 use Koha::Old::Checkouts;
41 use Koha::Patron::Attributes;
42 use Koha::Patron::Categories;
43 use Koha::Patron::Debarments;
44 use Koha::Patron::HouseboundProfile;
45 use Koha::Patron::HouseboundRole;
46 use Koha::Patron::Images;
47 use Koha::Patron::Modifications;
48 use Koha::Patron::Relationships;
49 use Koha::Patrons;
50 use Koha::Plugins;
51 use Koha::Subscription::Routinglists;
52 use Koha::Token;
53 use Koha::Virtualshelves;
54
55 use base qw(Koha::Object);
56
57 use constant ADMINISTRATIVE_LOCKOUT => -1;
58
59 our $RESULTSET_PATRON_ID_MAPPING = {
60     Accountline          => 'borrowernumber',
61     Aqbasketuser         => 'borrowernumber',
62     Aqbudget             => 'budget_owner_id',
63     Aqbudgetborrower     => 'borrowernumber',
64     ArticleRequest       => 'borrowernumber',
65     BorrowerDebarment    => 'borrowernumber',
66     BorrowerFile         => 'borrowernumber',
67     BorrowerModification => 'borrowernumber',
68     ClubEnrollment       => 'borrowernumber',
69     Issue                => 'borrowernumber',
70     ItemsLastBorrower    => 'borrowernumber',
71     Linktracker          => 'borrowernumber',
72     Message              => 'borrowernumber',
73     MessageQueue         => 'borrowernumber',
74     OldIssue             => 'borrowernumber',
75     OldReserve           => 'borrowernumber',
76     Rating               => 'borrowernumber',
77     Reserve              => 'borrowernumber',
78     Review               => 'borrowernumber',
79     SearchHistory        => 'userid',
80     Statistic            => 'borrowernumber',
81     Suggestion           => 'suggestedby',
82     TagAll               => 'borrowernumber',
83     Virtualshelfcontent  => 'borrowernumber',
84     Virtualshelfshare    => 'borrowernumber',
85     Virtualshelve        => 'owner',
86 };
87
88 =head1 NAME
89
90 Koha::Patron - Koha Patron Object class
91
92 =head1 API
93
94 =head2 Class Methods
95
96 =head3 new
97
98 =cut
99
100 sub new {
101     my ( $class, $params ) = @_;
102
103     return $class->SUPER::new($params);
104 }
105
106 =head3 fixup_cardnumber
107
108 Autogenerate next cardnumber from highest value found in database
109
110 =cut
111
112 sub fixup_cardnumber {
113     my ( $self ) = @_;
114     my $max = Koha::Patrons->search({
115         cardnumber => {-regexp => '^-?[0-9]+$'}
116     }, {
117         select => \'CAST(cardnumber AS SIGNED)',
118         as => ['cast_cardnumber']
119     })->_resultset->get_column('cast_cardnumber')->max;
120     $self->cardnumber(($max || 0) +1);
121 }
122
123 =head3 trim_whitespace
124
125 trim whitespace from data which has some non-whitespace in it.
126 Could be moved to Koha::Object if need to be reused
127
128 =cut
129
130 sub trim_whitespaces {
131     my( $self ) = @_;
132
133     my $schema  = Koha::Database->new->schema;
134     my @columns = $schema->source($self->_type)->columns;
135
136     for my $column( @columns ) {
137         my $value = $self->$column;
138         if ( defined $value ) {
139             $value =~ s/^\s*|\s*$//g;
140             $self->$column($value);
141         }
142     }
143     return $self;
144 }
145
146 =head3 plain_text_password
147
148 $patron->plain_text_password( $password );
149
150 stores a copy of the unencrypted password in the object
151 for use in code before encrypting for db
152
153 =cut
154
155 sub plain_text_password {
156     my ( $self, $password ) = @_;
157     if ( $password ) {
158         $self->{_plain_text_password} = $password;
159         return $self;
160     }
161     return $self->{_plain_text_password}
162         if $self->{_plain_text_password};
163
164     return;
165 }
166
167 =head3 store
168
169 Patron specific store method to cleanup record
170 and do other necessary things before saving
171 to db
172
173 =cut
174
175 sub store {
176     my ($self) = @_;
177
178     $self->_result->result_source->schema->txn_do(
179         sub {
180             if (
181                 C4::Context->preference("autoMemberNum")
182                 and ( not defined $self->cardnumber
183                     or $self->cardnumber eq '' )
184               )
185             {
186                 # Warning: The caller is responsible for locking the members table in write
187                 # mode, to avoid database corruption.
188                 # We are in a transaction but the table is not locked
189                 $self->fixup_cardnumber;
190             }
191
192             unless( $self->category->in_storage ) {
193                 Koha::Exceptions::Object::FKConstraint->throw(
194                     broken_fk => 'categorycode',
195                     value     => $self->categorycode,
196                 );
197             }
198
199             $self->trim_whitespaces;
200
201             # Set surname to uppercase if uppercasesurname is true
202             $self->surname( uc($self->surname) )
203                 if C4::Context->preference("uppercasesurnames");
204
205             $self->relationship(undef) # We do not want to store an empty string in this field
206               if defined $self->relationship
207                      and $self->relationship eq "";
208
209             unless ( $self->in_storage ) {    #AddMember
210
211                 # Generate a valid userid/login if needed
212                 $self->generate_userid
213                   if not $self->userid or not $self->has_valid_userid;
214
215                 # Add expiration date if it isn't already there
216                 unless ( $self->dateexpiry ) {
217                     $self->dateexpiry( $self->category->get_expiry_date );
218                 }
219
220                 # Add enrollment date if it isn't already there
221                 unless ( $self->dateenrolled ) {
222                     $self->dateenrolled(dt_from_string);
223                 }
224
225                 # Set the privacy depending on the patron's category
226                 my $default_privacy = $self->category->default_privacy || q{};
227                 $default_privacy =
228                     $default_privacy eq 'default' ? 1
229                   : $default_privacy eq 'never'   ? 2
230                   : $default_privacy eq 'forever' ? 0
231                   :                                                   undef;
232                 $self->privacy($default_privacy);
233
234                 # Call any check_password plugins if password is passed
235                 if ( C4::Context->config("enable_plugins") && $self->password ) {
236                     my @plugins = Koha::Plugins->new()->GetPlugins({
237                         method => 'check_password',
238                     });
239                     foreach my $plugin ( @plugins ) {
240                         # This plugin hook will also be used by a plugin for the Norwegian national
241                         # patron database. This is why we need to pass both the password and the
242                         # borrowernumber to the plugin.
243                         my $ret = $plugin->check_password(
244                             {
245                                 password       => $self->password,
246                                 borrowernumber => $self->borrowernumber
247                             }
248                         );
249                         if ( $ret->{'error'} == 1 ) {
250                             Koha::Exceptions::Password::Plugin->throw();
251                         }
252                     }
253                 }
254
255                 # Make a copy of the plain text password for later use
256                 $self->plain_text_password( $self->password );
257
258                 # Create a disabled account if no password provided
259                 $self->password( $self->password
260                     ? Koha::AuthUtils::hash_password( $self->password )
261                     : '!' );
262
263                 $self->borrowernumber(undef);
264
265                 $self = $self->SUPER::store;
266
267                 $self->add_enrolment_fee_if_needed(0);
268
269                 logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
270                   if C4::Context->preference("BorrowersLog");
271             }
272             else {    #ModMember
273
274                 my $self_from_storage = $self->get_from_storage;
275                 # FIXME We should not deal with that here, callers have to do this job
276                 # Moved from ModMember to prevent regressions
277                 unless ( $self->userid ) {
278                     my $stored_userid = $self_from_storage->userid;
279                     $self->userid($stored_userid);
280                 }
281
282                 # Password must be updated using $self->set_password
283                 $self->password($self_from_storage->password);
284
285                 if ( $self->category->categorycode ne
286                     $self_from_storage->category->categorycode )
287                 {
288                     # Add enrolement fee on category change if required
289                     $self->add_enrolment_fee_if_needed(1)
290                       if C4::Context->preference('FeeOnChangePatronCategory');
291
292                     # Clean up guarantors on category change if required
293                     $self->guarantor_relationships->delete
294                       if ( $self->category->category_type ne 'C'
295                         && $self->category->category_type ne 'P' );
296
297                 }
298
299                 # Actionlogs
300                 if ( C4::Context->preference("BorrowersLog") ) {
301                     my $info;
302                     my $from_storage = $self_from_storage->unblessed;
303                     my $from_object  = $self->unblessed;
304                     my @skip_fields  = (qw/lastseen updated_on/);
305                     for my $key ( keys %{$from_storage} ) {
306                         next if any { /$key/ } @skip_fields;
307                         if (
308                             (
309                                   !defined( $from_storage->{$key} )
310                                 && defined( $from_object->{$key} )
311                             )
312                             || ( defined( $from_storage->{$key} )
313                                 && !defined( $from_object->{$key} ) )
314                             || (
315                                    defined( $from_storage->{$key} )
316                                 && defined( $from_object->{$key} )
317                                 && ( $from_storage->{$key} ne
318                                     $from_object->{$key} )
319                             )
320                           )
321                         {
322                             $info->{$key} = {
323                                 before => $from_storage->{$key},
324                                 after  => $from_object->{$key}
325                             };
326                         }
327                     }
328
329                     if ( defined($info) ) {
330                         logaction(
331                             "MEMBERS",
332                             "MODIFY",
333                             $self->borrowernumber,
334                             to_json(
335                                 $info,
336                                 { utf8 => 1, pretty => 1, canonical => 1 }
337                             )
338                         );
339                     }
340                 }
341
342                 # Final store
343                 $self = $self->SUPER::store;
344             }
345         }
346     );
347     return $self;
348 }
349
350 =head3 delete
351
352 $patron->delete
353
354 Delete patron's holds, lists and finally the patron.
355
356 Lists owned by the borrower are deleted, but entries from the borrower to
357 other lists are kept.
358
359 =cut
360
361 sub delete {
362     my ($self) = @_;
363
364     my $anonymous_patron = C4::Context->preference("AnonymousPatron");
365     Koha::Exceptions::Patron::FailedDeleteAnonymousPatron->throw() if $anonymous_patron && $self->id eq $anonymous_patron;
366
367     $self->_result->result_source->schema->txn_do(
368         sub {
369             # Cancel Patron's holds
370             my $holds = $self->holds;
371             while( my $hold = $holds->next ){
372                 $hold->cancel;
373             }
374
375             # Delete all lists and all shares of this borrower
376             # Consistent with the approach Koha uses on deleting individual lists
377             # Note that entries in virtualshelfcontents added by this borrower to
378             # lists of others will be handled by a table constraint: the borrower
379             # is set to NULL in those entries.
380             # NOTE:
381             # We could handle the above deletes via a constraint too.
382             # But a new BZ report 11889 has been opened to discuss another approach.
383             # Instead of deleting we could also disown lists (based on a pref).
384             # In that way we could save shared and public lists.
385             # The current table constraints support that idea now.
386             # This pref should then govern the results of other routines/methods such as
387             # Koha::Virtualshelf->new->delete too.
388             # FIXME Could be $patron->get_lists
389             $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
390
391             # We cannot have a FK on borrower_modifications.borrowernumber, the table is also used
392             # for patron selfreg
393             $_->delete for Koha::Patron::Modifications->search( { borrowernumber => $self->borrowernumber } );
394
395             $self->SUPER::delete;
396
397             logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
398         }
399     );
400     return $self;
401 }
402
403
404 =head3 category
405
406 my $patron_category = $patron->category
407
408 Return the patron category for this patron
409
410 =cut
411
412 sub category {
413     my ( $self ) = @_;
414     return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
415 }
416
417 =head3 image
418
419 =cut
420
421 sub image {
422     my ( $self ) = @_;
423
424     return Koha::Patron::Images->find( $self->borrowernumber );
425 }
426
427 =head3 library
428
429 Returns a Koha::Library object representing the patron's home library.
430
431 =cut
432
433 sub library {
434     my ( $self ) = @_;
435     return Koha::Library->_new_from_dbic($self->_result->branchcode);
436 }
437
438 =head3 sms_provider
439
440 Returns a Koha::SMS::Provider object representing the patron's SMS provider.
441
442 =cut
443
444 sub sms_provider {
445     my ( $self ) = @_;
446     my $sms_provider_rs = $self->_result->sms_provider;
447     return unless $sms_provider_rs;
448     return Koha::SMS::Provider->_new_from_dbic($sms_provider_rs);
449 }
450
451 =head3 guarantor_relationships
452
453 Returns Koha::Patron::Relationships object for this patron's guarantors
454
455 Returns the set of relationships for the patrons that are guarantors for this patron.
456
457 This is returned instead of a Koha::Patron object because the guarantor
458 may not exist as a patron in Koha. If this is true, the guarantors name
459 exists in the Koha::Patron::Relationship object and will have no guarantor_id.
460
461 =cut
462
463 sub guarantor_relationships {
464     my ($self) = @_;
465
466     return Koha::Patron::Relationships->search( { guarantee_id => $self->id } );
467 }
468
469 =head3 guarantee_relationships
470
471 Returns Koha::Patron::Relationships object for this patron's guarantors
472
473 Returns the set of relationships for the patrons that are guarantees for this patron.
474
475 The method returns Koha::Patron::Relationship objects for the sake
476 of consistency with the guantors method.
477 A guarantee by definition must exist as a patron in Koha.
478
479 =cut
480
481 sub guarantee_relationships {
482     my ($self) = @_;
483
484     return Koha::Patron::Relationships->search(
485         { guarantor_id => $self->id },
486         {
487             prefetch => 'guarantee',
488             order_by => { -asc => [ 'guarantee.surname', 'guarantee.firstname' ] },
489         }
490     );
491 }
492
493 =head3 relationships_debt
494
495 Returns the amount owed by the patron's guarantors *and* the other guarantees of those guarantors
496
497 =cut
498
499 sub relationships_debt {
500     my ($self, $params) = @_;
501
502     my $include_guarantors  = $params->{include_guarantors};
503     my $only_this_guarantor = $params->{only_this_guarantor};
504     my $include_this_patron = $params->{include_this_patron};
505
506     my @guarantors;
507     if ( $only_this_guarantor ) {
508         @guarantors = $self->guarantee_relationships->count ? ( $self ) : ();
509         Koha::Exceptions::BadParameter->throw( { parameter => 'only_this_guarantor' } ) unless @guarantors;
510     } elsif ( $self->guarantor_relationships->count ) {
511         # I am a guarantee, just get all my guarantors
512         @guarantors = $self->guarantor_relationships->guarantors;
513     } else {
514         # I am a guarantor, I need to get all the guarantors of all my guarantees
515         @guarantors = map { $_->guarantor_relationships->guarantors } $self->guarantee_relationships->guarantees;
516     }
517
518     my $non_issues_charges = 0;
519     my $seen = $include_this_patron ? {} : { $self->id => 1 }; # For tracking members already added to the total
520     foreach my $guarantor (@guarantors) {
521         $non_issues_charges += $guarantor->account->non_issues_charges if $include_guarantors && !$seen->{ $guarantor->id };
522
523         # We've added what the guarantor owes, not added in that guarantor's guarantees as well
524         my @guarantees = map { $_->guarantee } $guarantor->guarantee_relationships();
525         my $guarantees_non_issues_charges = 0;
526         foreach my $guarantee (@guarantees) {
527             next if $seen->{ $guarantee->id };
528             $guarantees_non_issues_charges += $guarantee->account->non_issues_charges;
529             # Mark this guarantee as seen so we don't double count a guarantee linked to multiple guarantors
530             $seen->{ $guarantee->id } = 1;
531         }
532
533         $non_issues_charges += $guarantees_non_issues_charges;
534         $seen->{ $guarantor->id } = 1;
535     }
536
537     return $non_issues_charges;
538 }
539
540 =head3 housebound_profile
541
542 Returns the HouseboundProfile associated with this patron.
543
544 =cut
545
546 sub housebound_profile {
547     my ( $self ) = @_;
548     my $profile = $self->_result->housebound_profile;
549     return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
550         if ( $profile );
551     return;
552 }
553
554 =head3 housebound_role
555
556 Returns the HouseboundRole associated with this patron.
557
558 =cut
559
560 sub housebound_role {
561     my ( $self ) = @_;
562
563     my $role = $self->_result->housebound_role;
564     return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
565     return;
566 }
567
568 =head3 siblings
569
570 Returns the siblings of this patron.
571
572 =cut
573
574 sub siblings {
575     my ($self) = @_;
576
577     my @guarantors = $self->guarantor_relationships()->guarantors();
578
579     return unless @guarantors;
580
581     my @siblings =
582       map { $_->guarantee_relationships()->guarantees() } @guarantors;
583
584     return unless @siblings;
585
586     my %seen;
587     @siblings =
588       grep { !$seen{ $_->id }++ && ( $_->id != $self->id ) } @siblings;
589
590     return wantarray ? @siblings : Koha::Patrons->search( { borrowernumber => { -in => [ map { $_->id } @siblings ] } } );
591 }
592
593 =head3 merge_with
594
595     my $patron = Koha::Patrons->find($id);
596     $patron->merge_with( \@patron_ids );
597
598     This subroutine merges a list of patrons into the patron record. This is accomplished by finding
599     all related patron ids for the patrons to be merged in other tables and changing the ids to be that
600     of the keeper patron.
601
602 =cut
603
604 sub merge_with {
605     my ( $self, $patron_ids ) = @_;
606
607     my $anonymous_patron = C4::Context->preference("AnonymousPatron");
608     return if $anonymous_patron && $self->id eq $anonymous_patron;
609
610     my @patron_ids = @{ $patron_ids };
611
612     # Ensure the keeper isn't in the list of patrons to merge
613     @patron_ids = grep { $_ ne $self->id } @patron_ids;
614
615     my $schema = Koha::Database->new()->schema();
616
617     my $results;
618
619     $self->_result->result_source->schema->txn_do( sub {
620         foreach my $patron_id (@patron_ids) {
621
622             next if $patron_id eq $anonymous_patron;
623
624             my $patron = Koha::Patrons->find( $patron_id );
625
626             next unless $patron;
627
628             # Unbless for safety, the patron will end up being deleted
629             $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
630
631             my $attributes = $patron->extended_attributes;
632             my $new_attributes = [
633                 map { { code => $_->code, attribute => $_->attribute } }
634                     $attributes->as_list
635             ];
636             $attributes->delete; # We need to delete before trying to merge them to prevent exception on unique and repeatable
637             for my $attribute ( @$new_attributes ) {
638                 $self->add_extended_attribute($attribute);
639             }
640
641             while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
642                 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
643                 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
644                 $rs->update({ $field => $self->id });
645                 if ( $r eq 'BorrowerDebarment' ) {
646                     Koha::Patron::Debarments::UpdateBorrowerDebarmentFlags($self->id);
647                 }
648             }
649
650             $patron->move_to_deleted();
651             $patron->delete();
652         }
653     });
654
655     return $results;
656 }
657
658
659
660 =head3 wants_check_for_previous_checkout
661
662     $wants_check = $patron->wants_check_for_previous_checkout;
663
664 Return 1 if Koha needs to perform PrevIssue checking, else 0.
665
666 =cut
667
668 sub wants_check_for_previous_checkout {
669     my ( $self ) = @_;
670     my $syspref = C4::Context->preference("checkPrevCheckout");
671
672     # Simple cases
673     ## Hard syspref trumps all
674     return 1 if ($syspref eq 'hardyes');
675     return 0 if ($syspref eq 'hardno');
676     ## Now, patron pref trumps all
677     return 1 if ($self->checkprevcheckout eq 'yes');
678     return 0 if ($self->checkprevcheckout eq 'no');
679
680     # More complex: patron inherits -> determine category preference
681     my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
682     return 1 if ($checkPrevCheckoutByCat eq 'yes');
683     return 0 if ($checkPrevCheckoutByCat eq 'no');
684
685     # Finally: category preference is inherit, default to 0
686     if ($syspref eq 'softyes') {
687         return 1;
688     } else {
689         return 0;
690     }
691 }
692
693 =head3 do_check_for_previous_checkout
694
695     $do_check = $patron->do_check_for_previous_checkout($item);
696
697 Return 1 if the bib associated with $ITEM has previously been checked out to
698 $PATRON, 0 otherwise.
699
700 =cut
701
702 sub do_check_for_previous_checkout {
703     my ( $self, $item ) = @_;
704
705     my @item_nos;
706     my $biblio = Koha::Biblios->find( $item->{biblionumber} );
707     if ( $biblio->is_serial ) {
708         push @item_nos, $item->{itemnumber};
709     } else {
710         # Get all itemnumbers for given bibliographic record.
711         @item_nos = $biblio->items->get_column( 'itemnumber' );
712     }
713
714     # Create (old)issues search criteria
715     my $criteria = {
716         borrowernumber => $self->borrowernumber,
717         itemnumber => \@item_nos,
718     };
719
720     my $delay = C4::Context->preference('CheckPrevCheckoutDelay') || 0;
721     if ($delay) {
722         my $dtf = Koha::Database->new->schema->storage->datetime_parser;
723         my $newer_than = dt_from_string()->subtract( days => $delay );
724         $criteria->{'returndate'} = { '>'   =>  $dtf->format_datetime($newer_than), };
725     }
726
727     # Check current issues table
728     my $issues = Koha::Checkouts->search($criteria);
729     return 1 if $issues->count; # 0 || N
730
731     # Check old issues table
732     my $old_issues = Koha::Old::Checkouts->search($criteria);
733     return $old_issues->count;  # 0 || N
734 }
735
736 =head3 is_debarred
737
738 my $debarment_expiration = $patron->is_debarred;
739
740 Returns the date a patron debarment will expire, or undef if the patron is not
741 debarred
742
743 =cut
744
745 sub is_debarred {
746     my ($self) = @_;
747
748     return unless $self->debarred;
749     return $self->debarred
750       if $self->debarred =~ '^9999'
751       or dt_from_string( $self->debarred ) > dt_from_string;
752     return;
753 }
754
755 =head3 is_expired
756
757 my $is_expired = $patron->is_expired;
758
759 Returns 1 if the patron is expired or 0;
760
761 =cut
762
763 sub is_expired {
764     my ($self) = @_;
765     return 0 unless $self->dateexpiry;
766     return 0 if $self->dateexpiry =~ '^9999';
767     return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
768     return 0;
769 }
770
771 =head3 is_going_to_expire
772
773 my $is_going_to_expire = $patron->is_going_to_expire;
774
775 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
776
777 =cut
778
779 sub is_going_to_expire {
780     my ($self) = @_;
781
782     my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
783
784     return 0 unless $delay;
785     return 0 unless $self->dateexpiry;
786     return 0 if $self->dateexpiry =~ '^9999';
787     return 1 if dt_from_string( $self->dateexpiry, undef, 'floating' )->subtract( days => $delay ) < dt_from_string(undef, undef, 'floating')->truncate( to => 'day' );
788     return 0;
789 }
790
791 =head3 set_password
792
793     $patron->set_password({ password => $plain_text_password [, skip_validation => 1 ] });
794
795 Set the patron's password.
796
797 =head4 Exceptions
798
799 The passed string is validated against the current password enforcement policy.
800 Validation can be skipped by passing the I<skip_validation> parameter.
801
802 Exceptions are thrown if the password is not good enough.
803
804 =over 4
805
806 =item Koha::Exceptions::Password::TooShort
807
808 =item Koha::Exceptions::Password::WhitespaceCharacters
809
810 =item Koha::Exceptions::Password::TooWeak
811
812 =item Koha::Exceptions::Password::Plugin (if a "check password" plugin is enabled)
813
814 =back
815
816 =cut
817
818 sub set_password {
819     my ( $self, $args ) = @_;
820
821     my $password = $args->{password};
822
823     unless ( $args->{skip_validation} ) {
824         my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password, $self->category );
825
826         if ( !$is_valid ) {
827             if ( $error eq 'too_short' ) {
828                 my $min_length = $self->category->effective_min_password_length;
829                 $min_length = 3 if not $min_length or $min_length < 3;
830
831                 my $password_length = length($password);
832                 Koha::Exceptions::Password::TooShort->throw(
833                     length => $password_length, min_length => $min_length );
834             }
835             elsif ( $error eq 'has_whitespaces' ) {
836                 Koha::Exceptions::Password::WhitespaceCharacters->throw();
837             }
838             elsif ( $error eq 'too_weak' ) {
839                 Koha::Exceptions::Password::TooWeak->throw();
840             }
841         }
842     }
843
844     if ( C4::Context->config("enable_plugins") ) {
845         # Call any check_password plugins
846         my @plugins = Koha::Plugins->new()->GetPlugins({
847             method => 'check_password',
848         });
849         foreach my $plugin ( @plugins ) {
850             # This plugin hook will also be used by a plugin for the Norwegian national
851             # patron database. This is why we need to pass both the password and the
852             # borrowernumber to the plugin.
853             my $ret = $plugin->check_password(
854                 {
855                     password       => $password,
856                     borrowernumber => $self->borrowernumber
857                 }
858             );
859             # This plugin hook will also be used by a plugin for the Norwegian national
860             # patron database. This is why we need to call the actual plugins and then
861             # check skip_validation afterwards.
862             if ( $ret->{'error'} == 1 && !$args->{skip_validation} ) {
863                 Koha::Exceptions::Password::Plugin->throw();
864             }
865         }
866     }
867
868     my $digest = Koha::AuthUtils::hash_password($password);
869
870     # We do not want to call $self->store and retrieve password from DB
871     $self->password($digest);
872     $self->login_attempts(0);
873     $self->SUPER::store;
874
875     logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" )
876         if C4::Context->preference("BorrowersLog");
877
878     return $self;
879 }
880
881
882 =head3 renew_account
883
884 my $new_expiry_date = $patron->renew_account
885
886 Extending the subscription to the expiry date.
887
888 =cut
889
890 sub renew_account {
891     my ($self) = @_;
892     my $date;
893     if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
894         $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
895     } else {
896         $date =
897             C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
898             ? dt_from_string( $self->dateexpiry )
899             : dt_from_string;
900     }
901     my $expiry_date = $self->category->get_expiry_date($date);
902
903     $self->dateexpiry($expiry_date);
904     $self->date_renewed( dt_from_string() );
905     $self->store();
906
907     $self->add_enrolment_fee_if_needed(1);
908
909     logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
910     return dt_from_string( $expiry_date )->truncate( to => 'day' );
911 }
912
913 =head3 has_overdues
914
915 my $has_overdues = $patron->has_overdues;
916
917 Returns the number of patron's overdues
918
919 =cut
920
921 sub has_overdues {
922     my ($self) = @_;
923     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
924     return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
925 }
926
927 =head3 track_login
928
929     $patron->track_login;
930     $patron->track_login({ force => 1 });
931
932     Tracks a (successful) login attempt.
933     The preference TrackLastPatronActivity must be enabled. Or you
934     should pass the force parameter.
935
936 =cut
937
938 sub track_login {
939     my ( $self, $params ) = @_;
940     return if
941         !$params->{force} &&
942         !C4::Context->preference('TrackLastPatronActivity');
943     $self->lastseen( dt_from_string() )->store;
944 }
945
946 =head3 move_to_deleted
947
948 my $is_moved = $patron->move_to_deleted;
949
950 Move a patron to the deletedborrowers table.
951 This can be done before deleting a patron, to make sure the data are not completely deleted.
952
953 =cut
954
955 sub move_to_deleted {
956     my ($self) = @_;
957     my $patron_infos = $self->unblessed;
958     delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
959     return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
960 }
961
962 =head3 article_requests
963
964 my @requests = $borrower->article_requests();
965 my $requests = $borrower->article_requests();
966
967 Returns either a list of ArticleRequests objects,
968 or an ArtitleRequests object, depending on the
969 calling context.
970
971 =cut
972
973 sub article_requests {
974     my ( $self ) = @_;
975
976     $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
977
978     return $self->{_article_requests};
979 }
980
981 =head3 article_requests_current
982
983 my @requests = $patron->article_requests_current
984
985 Returns the article requests associated with this patron that are incomplete
986
987 =cut
988
989 sub article_requests_current {
990     my ( $self ) = @_;
991
992     $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
993         {
994             borrowernumber => $self->id(),
995             -or          => [
996                 { status => Koha::ArticleRequest::Status::Pending },
997                 { status => Koha::ArticleRequest::Status::Processing }
998             ]
999         }
1000     );
1001
1002     return $self->{_article_requests_current};
1003 }
1004
1005 =head3 article_requests_finished
1006
1007 my @requests = $biblio->article_requests_finished
1008
1009 Returns the article requests associated with this patron that are completed
1010
1011 =cut
1012
1013 sub article_requests_finished {
1014     my ( $self, $borrower ) = @_;
1015
1016     $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
1017         {
1018             borrowernumber => $self->id(),
1019             -or          => [
1020                 { status => Koha::ArticleRequest::Status::Completed },
1021                 { status => Koha::ArticleRequest::Status::Canceled }
1022             ]
1023         }
1024     );
1025
1026     return $self->{_article_requests_finished};
1027 }
1028
1029 =head3 add_enrolment_fee_if_needed
1030
1031 my $enrolment_fee = $patron->add_enrolment_fee_if_needed($renewal);
1032
1033 Add enrolment fee for a patron if needed.
1034
1035 $renewal - boolean denoting whether this is an account renewal or not
1036
1037 =cut
1038
1039 sub add_enrolment_fee_if_needed {
1040     my ($self, $renewal) = @_;
1041     my $enrolment_fee = $self->category->enrolmentfee;
1042     if ( $enrolment_fee && $enrolment_fee > 0 ) {
1043         my $type = $renewal ? 'ACCOUNT_RENEW' : 'ACCOUNT';
1044         $self->account->add_debit(
1045             {
1046                 amount     => $enrolment_fee,
1047                 user_id    => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
1048                 interface  => C4::Context->interface,
1049                 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
1050                 type       => $type
1051             }
1052         );
1053     }
1054     return $enrolment_fee || 0;
1055 }
1056
1057 =head3 checkouts
1058
1059 my $checkouts = $patron->checkouts
1060
1061 =cut
1062
1063 sub checkouts {
1064     my ($self) = @_;
1065     my $checkouts = $self->_result->issues;
1066     return Koha::Checkouts->_new_from_dbic( $checkouts );
1067 }
1068
1069 =head3 pending_checkouts
1070
1071 my $pending_checkouts = $patron->pending_checkouts
1072
1073 This method will return the same as $self->checkouts, but with a prefetch on
1074 items, biblio and biblioitems.
1075
1076 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
1077
1078 It should not be used directly, prefer to access fields you need instead of
1079 retrieving all these fields in one go.
1080
1081 =cut
1082
1083 sub pending_checkouts {
1084     my( $self ) = @_;
1085     my $checkouts = $self->_result->issues->search(
1086         {},
1087         {
1088             order_by => [
1089                 { -desc => 'me.timestamp' },
1090                 { -desc => 'issuedate' },
1091                 { -desc => 'issue_id' }, # Sort by issue_id should be enough
1092             ],
1093             prefetch => { item => { biblio => 'biblioitems' } },
1094         }
1095     );
1096     return Koha::Checkouts->_new_from_dbic( $checkouts );
1097 }
1098
1099 =head3 old_checkouts
1100
1101 my $old_checkouts = $patron->old_checkouts
1102
1103 =cut
1104
1105 sub old_checkouts {
1106     my ($self) = @_;
1107     my $old_checkouts = $self->_result->old_issues;
1108     return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
1109 }
1110
1111 =head3 get_overdues
1112
1113 my $overdue_items = $patron->get_overdues
1114
1115 Return the overdue items
1116
1117 =cut
1118
1119 sub get_overdues {
1120     my ($self) = @_;
1121     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
1122     return $self->checkouts->search(
1123         {
1124             'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
1125         },
1126         {
1127             prefetch => { item => { biblio => 'biblioitems' } },
1128         }
1129     );
1130 }
1131
1132 =head3 get_routing_lists
1133
1134 my @routinglists = $patron->get_routing_lists
1135
1136 Returns the routing lists a patron is subscribed to.
1137
1138 =cut
1139
1140 sub get_routing_lists {
1141     my ($self) = @_;
1142     my $routing_list_rs = $self->_result->subscriptionroutinglists;
1143     return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
1144 }
1145
1146 =head3 get_age
1147
1148 my $age = $patron->get_age
1149
1150 Return the age of the patron
1151
1152 =cut
1153
1154 sub get_age {
1155     my ($self)    = @_;
1156     my $today_str = dt_from_string->strftime("%Y-%m-%d");
1157     return unless $self->dateofbirth;
1158     my $dob_str   = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
1159
1160     my ( $dob_y,   $dob_m,   $dob_d )   = split /-/, $dob_str;
1161     my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
1162
1163     my $age = $today_y - $dob_y;
1164     if ( $dob_m . $dob_d > $today_m . $today_d ) {
1165         $age--;
1166     }
1167
1168     return $age;
1169 }
1170
1171 =head3 is_valid_age
1172
1173 my $is_valid = $patron->is_valid_age
1174
1175 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
1176
1177 =cut
1178
1179 sub is_valid_age {
1180     my ($self) = @_;
1181     my $age = $self->get_age;
1182
1183     my $patroncategory = $self->category;
1184     my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
1185
1186     return (defined($age) && (($high && ($age > $high)) or ($low && ($age < $low)))) ? 0 : 1;
1187 }
1188
1189 =head3 account
1190
1191 my $account = $patron->account
1192
1193 =cut
1194
1195 sub account {
1196     my ($self) = @_;
1197     return Koha::Account->new( { patron_id => $self->borrowernumber } );
1198 }
1199
1200 =head3 holds
1201
1202 my $holds = $patron->holds
1203
1204 Return all the holds placed by this patron
1205
1206 =cut
1207
1208 sub holds {
1209     my ($self) = @_;
1210     my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1211     return Koha::Holds->_new_from_dbic($holds_rs);
1212 }
1213
1214 =head3 old_holds
1215
1216 my $old_holds = $patron->old_holds
1217
1218 Return all the historical holds for this patron
1219
1220 =cut
1221
1222 sub old_holds {
1223     my ($self) = @_;
1224     my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1225     return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1226 }
1227
1228 =head3 return_claims
1229
1230 my $return_claims = $patron->return_claims
1231
1232 =cut
1233
1234 sub return_claims {
1235     my ($self) = @_;
1236     my $return_claims = $self->_result->return_claims_borrowernumbers;
1237     return Koha::Checkouts::ReturnClaims->_new_from_dbic( $return_claims );
1238 }
1239
1240 =head3 notice_email_address
1241
1242   my $email = $patron->notice_email_address;
1243
1244 Return the email address of patron used for notices.
1245 Returns the empty string if no email address.
1246
1247 =cut
1248
1249 sub notice_email_address{
1250     my ( $self ) = @_;
1251
1252     my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1253     # if syspref is set to 'first valid' (value == OFF), look up email address
1254     if ( $which_address eq 'OFF' ) {
1255         return $self->first_valid_email_address;
1256     }
1257
1258     return $self->$which_address || '';
1259 }
1260
1261 =head3 first_valid_email_address
1262
1263 my $first_valid_email_address = $patron->first_valid_email_address
1264
1265 Return the first valid email address for a patron.
1266 For now, the order  is defined as email, emailpro, B_email.
1267 Returns the empty string if the borrower has no email addresses.
1268
1269 =cut
1270
1271 sub first_valid_email_address {
1272     my ($self) = @_;
1273
1274     return $self->email() || $self->emailpro() || $self->B_email() || q{};
1275 }
1276
1277 =head3 get_club_enrollments
1278
1279 =cut
1280
1281 sub get_club_enrollments {
1282     my ( $self, $return_scalar ) = @_;
1283
1284     my $e = Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1285
1286     return $e if $return_scalar;
1287
1288     return wantarray ? $e->as_list : $e;
1289 }
1290
1291 =head3 get_enrollable_clubs
1292
1293 =cut
1294
1295 sub get_enrollable_clubs {
1296     my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
1297
1298     my $params;
1299     $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1300       if $is_enrollable_from_opac;
1301     $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1302
1303     $params->{borrower} = $self;
1304
1305     my $e = Koha::Clubs->get_enrollable($params);
1306
1307     return $e if $return_scalar;
1308
1309     return wantarray ? $e->as_list : $e;
1310 }
1311
1312 =head3 account_locked
1313
1314 my $is_locked = $patron->account_locked
1315
1316 Return true if the patron has reached the maximum number of login attempts
1317 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1318 as an administrative lockout (independent of FailedLoginAttempts; see also
1319 Koha::Patron->lock).
1320 Otherwise return false.
1321 If the pref is not set (empty string, null or 0), the feature is considered as
1322 disabled.
1323
1324 =cut
1325
1326 sub account_locked {
1327     my ($self) = @_;
1328     my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1329     return 1 if $FailedLoginAttempts
1330           and $self->login_attempts
1331           and $self->login_attempts >= $FailedLoginAttempts;
1332     return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1333     return 0;
1334 }
1335
1336 =head3 can_see_patron_infos
1337
1338 my $can_see = $patron->can_see_patron_infos( $patron );
1339
1340 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1341
1342 =cut
1343
1344 sub can_see_patron_infos {
1345     my ( $self, $patron ) = @_;
1346     return unless $patron;
1347     return $self->can_see_patrons_from( $patron->library->branchcode );
1348 }
1349
1350 =head3 can_see_patrons_from
1351
1352 my $can_see = $patron->can_see_patrons_from( $branchcode );
1353
1354 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1355
1356 =cut
1357
1358 sub can_see_patrons_from {
1359     my ( $self, $branchcode ) = @_;
1360     my $can = 0;
1361     if ( $self->branchcode eq $branchcode ) {
1362         $can = 1;
1363     } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1364         $can = 1;
1365     } elsif ( my $library_groups = $self->library->library_groups ) {
1366         while ( my $library_group = $library_groups->next ) {
1367             if ( $library_group->parent->has_child( $branchcode ) ) {
1368                 $can = 1;
1369                 last;
1370             }
1371         }
1372     }
1373     return $can;
1374 }
1375
1376 =head3 can_log_into
1377
1378 my $can_log_into = $patron->can_log_into( $library );
1379
1380 Given a I<Koha::Library> object, it returns a boolean representing
1381 the fact the patron can log into a the library.
1382
1383 =cut
1384
1385 sub can_log_into {
1386     my ( $self, $library ) = @_;
1387
1388     my $can = 0;
1389
1390     if ( C4::Context->preference('IndependentBranches') ) {
1391         $can = 1
1392           if $self->is_superlibrarian
1393           or $self->branchcode eq $library->id;
1394     }
1395     else {
1396         # no restrictions
1397         $can = 1;
1398     }
1399
1400    return $can;
1401 }
1402
1403 =head3 libraries_where_can_see_patrons
1404
1405 my $libraries = $patron-libraries_where_can_see_patrons;
1406
1407 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1408 The branchcodes are arbitrarily returned sorted.
1409 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1410
1411 An empty array means no restriction, the patron can see patron's infos from any libraries.
1412
1413 =cut
1414
1415 sub libraries_where_can_see_patrons {
1416     my ( $self ) = @_;
1417     my $userenv = C4::Context->userenv;
1418
1419     return () unless $userenv; # For tests, but userenv should be defined in tests...
1420
1421     my @restricted_branchcodes;
1422     if (C4::Context::only_my_library) {
1423         push @restricted_branchcodes, $self->branchcode;
1424     }
1425     else {
1426         unless (
1427             $self->has_permission(
1428                 { borrowers => 'view_borrower_infos_from_any_libraries' }
1429             )
1430           )
1431         {
1432             my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1433             if ( $library_groups->count )
1434             {
1435                 while ( my $library_group = $library_groups->next ) {
1436                     my $parent = $library_group->parent;
1437                     if ( $parent->has_child( $self->branchcode ) ) {
1438                         push @restricted_branchcodes, $parent->children->get_column('branchcode');
1439                     }
1440                 }
1441             }
1442
1443             @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1444         }
1445     }
1446
1447     @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1448     @restricted_branchcodes = uniq(@restricted_branchcodes);
1449     @restricted_branchcodes = sort(@restricted_branchcodes);
1450     return @restricted_branchcodes;
1451 }
1452
1453 =head3 has_permission
1454
1455 my $permission = $patron->has_permission($required);
1456
1457 See C4::Auth::haspermission for details of syntax for $required
1458
1459 =cut
1460
1461 sub has_permission {
1462     my ( $self, $flagsrequired ) = @_;
1463     return unless $self->userid;
1464     # TODO code from haspermission needs to be moved here!
1465     return C4::Auth::haspermission( $self->userid, $flagsrequired );
1466 }
1467
1468 =head3 is_superlibrarian
1469
1470   my $is_superlibrarian = $patron->is_superlibrarian;
1471
1472 Return true if the patron is a superlibrarian.
1473
1474 =cut
1475
1476 sub is_superlibrarian {
1477     my ($self) = @_;
1478     return $self->has_permission( { superlibrarian => 1 } ) ? 1 : 0;
1479 }
1480
1481 =head3 is_adult
1482
1483 my $is_adult = $patron->is_adult
1484
1485 Return true if the patron has a category with a type Adult (A) or Organization (I)
1486
1487 =cut
1488
1489 sub is_adult {
1490     my ( $self ) = @_;
1491     return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1492 }
1493
1494 =head3 is_child
1495
1496 my $is_child = $patron->is_child
1497
1498 Return true if the patron has a category with a type Child (C)
1499
1500 =cut
1501
1502 sub is_child {
1503     my( $self ) = @_;
1504     return $self->category->category_type eq 'C' ? 1 : 0;
1505 }
1506
1507 =head3 has_valid_userid
1508
1509 my $patron = Koha::Patrons->find(42);
1510 $patron->userid( $new_userid );
1511 my $has_a_valid_userid = $patron->has_valid_userid
1512
1513 my $patron = Koha::Patron->new( $params );
1514 my $has_a_valid_userid = $patron->has_valid_userid
1515
1516 Return true if the current userid of this patron is valid/unique, otherwise false.
1517
1518 Note that this should be done in $self->store instead and raise an exception if needed.
1519
1520 =cut
1521
1522 sub has_valid_userid {
1523     my ($self) = @_;
1524
1525     return 0 unless $self->userid;
1526
1527     return 0 if ( $self->userid eq C4::Context->config('user') );    # DB user
1528
1529     my $already_exists = Koha::Patrons->search(
1530         {
1531             userid => $self->userid,
1532             (
1533                 $self->in_storage
1534                 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1535                 : ()
1536             ),
1537         }
1538     )->count;
1539     return $already_exists ? 0 : 1;
1540 }
1541
1542 =head3 generate_userid
1543
1544 my $patron = Koha::Patron->new( $params );
1545 $patron->generate_userid
1546
1547 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1548
1549 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).
1550
1551 =cut
1552
1553 sub generate_userid {
1554     my ($self) = @_;
1555     my $offset = 0;
1556     my $firstname = $self->firstname // q{};
1557     my $surname = $self->surname // q{};
1558     #The script will "do" the following code and increment the $offset until the generated userid is unique
1559     do {
1560       $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1561       $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1562       my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1563       $userid = NFKD( $userid );
1564       $userid =~ s/\p{NonspacingMark}//g;
1565       $userid .= $offset unless $offset == 0;
1566       $self->userid( $userid );
1567       $offset++;
1568      } while (! $self->has_valid_userid );
1569
1570      return $self;
1571 }
1572
1573 =head3 add_extended_attribute
1574
1575 =cut
1576
1577 sub add_extended_attribute {
1578     my ($self, $attribute) = @_;
1579
1580     return Koha::Patron::Attribute->new(
1581         {
1582             %$attribute,
1583             ( borrowernumber => $self->borrowernumber ),
1584         }
1585     )->store;
1586
1587 }
1588
1589 =head3 extended_attributes
1590
1591 Return object of Koha::Patron::Attributes type with all attributes set for this patron
1592
1593 Or setter FIXME
1594
1595 =cut
1596
1597 sub extended_attributes {
1598     my ( $self, $attributes ) = @_;
1599     if ($attributes) {    # setter
1600         my $schema = $self->_result->result_source->schema;
1601         $schema->txn_do(
1602             sub {
1603                 # Remove the existing one
1604                 $self->extended_attributes->filter_by_branch_limitations->delete;
1605
1606                 # Insert the new ones
1607                 my $new_types = {};
1608                 for my $attribute (@$attributes) {
1609                     $self->add_extended_attribute($attribute);
1610                     $new_types->{$attribute->{code}} = 1;
1611                 }
1612
1613                 # Check globally mandatory types
1614                 my @required_attribute_types =
1615                     Koha::Patron::Attribute::Types->search(
1616                         {
1617                             mandatory => 1,
1618                             'borrower_attribute_types_branches.b_branchcode' =>
1619                               undef
1620                         },
1621                         { join => 'borrower_attribute_types_branches' }
1622                     )->get_column('code');
1623                 for my $type ( @required_attribute_types ) {
1624                     Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute->throw(
1625                         type => $type,
1626                     ) if !$new_types->{$type};
1627                 }
1628             }
1629         );
1630     }
1631
1632     my $rs = $self->_result->borrower_attributes;
1633     # We call search to use the filters in Koha::Patron::Attributes->search
1634     return Koha::Patron::Attributes->_new_from_dbic($rs)->search;
1635 }
1636
1637 =head3 lock
1638
1639     Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
1640
1641     Lock and optionally expire a patron account.
1642     Remove holds and article requests if remove flag set.
1643     In order to distinguish from locking by entering a wrong password, let's
1644     call this an administrative lockout.
1645
1646 =cut
1647
1648 sub lock {
1649     my ( $self, $params ) = @_;
1650     $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
1651     if( $params->{expire} ) {
1652         $self->dateexpiry( dt_from_string->subtract(days => 1) );
1653     }
1654     $self->store;
1655     if( $params->{remove} ) {
1656         $self->holds->delete;
1657         $self->article_requests->delete;
1658     }
1659     return $self;
1660 }
1661
1662 =head3 anonymize
1663
1664     Koha::Patrons->find($id)->anonymize;
1665
1666     Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
1667     are randomized, other personal data is cleared too.
1668     Patrons with issues are skipped.
1669
1670 =cut
1671
1672 sub anonymize {
1673     my ( $self ) = @_;
1674     if( $self->_result->issues->count ) {
1675         warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
1676         return;
1677     }
1678     # Mandatory fields come from the corresponding pref, but email fields
1679     # are removed since scrambled email addresses only generate errors
1680     my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
1681         split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
1682     $mandatory->{userid} = 1; # needed since sub store does not clear field
1683     my @columns = $self->_result->result_source->columns;
1684     @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized/ } @columns;
1685     push @columns, 'dateofbirth'; # add this date back in
1686     foreach my $col (@columns) {
1687         $self->_anonymize_column($col, $mandatory->{lc $col} );
1688     }
1689     $self->anonymized(1)->store;
1690 }
1691
1692 sub _anonymize_column {
1693     my ( $self, $col, $mandatory ) = @_;
1694     my $col_info = $self->_result->result_source->column_info($col);
1695     my $type = $col_info->{data_type};
1696     my $nullable = $col_info->{is_nullable};
1697     my $val;
1698     if( $type =~ /char|text/ ) {
1699         $val = $mandatory
1700             ? Koha::Token->new->generate({ pattern => '\w{10}' })
1701             : $nullable
1702             ? undef
1703             : q{};
1704     } elsif( $type =~ /integer|int$|float|dec|double/ ) {
1705         $val = $nullable ? undef : 0;
1706     } elsif( $type =~ /date|time/ ) {
1707         $val = $nullable ? undef : dt_from_string;
1708     }
1709     $self->$col($val);
1710 }
1711
1712 =head3 add_guarantor
1713
1714     my @relationships = $patron->add_guarantor(
1715         {
1716             borrowernumber => $borrowernumber,
1717             relationships  => $relationship,
1718         }
1719     );
1720
1721     Adds a new guarantor to a patron.
1722
1723 =cut
1724
1725 sub add_guarantor {
1726     my ( $self, $params ) = @_;
1727
1728     my $guarantor_id = $params->{guarantor_id};
1729     my $relationship = $params->{relationship};
1730
1731     return Koha::Patron::Relationship->new(
1732         {
1733             guarantee_id => $self->id,
1734             guarantor_id => $guarantor_id,
1735             relationship => $relationship
1736         }
1737     )->store();
1738 }
1739
1740 =head3 get_extended_attribute
1741
1742 my $attribute_value = $patron->get_extended_attribute( $code );
1743
1744 Return the attribute for the code passed in parameter.
1745
1746 It not exist it returns undef
1747
1748 Note that this will not work for repeatable attribute types.
1749
1750 Maybe you certainly not want to use this method, it is actually only used for SHOW_BARCODE
1751 (which should be a real patron's attribute (not extended)
1752
1753 =cut
1754
1755 sub get_extended_attribute {
1756     my ( $self, $code, $value ) = @_;
1757     my $rs = $self->_result->borrower_attributes;
1758     return unless $rs;
1759     my $attribute = $rs->search({ code => $code, ( $value ? ( attribute => $value ) : () ) });
1760     return unless $attribute->count;
1761     return $attribute->next;
1762 }
1763
1764 =head3 to_api
1765
1766     my $json = $patron->to_api;
1767
1768 Overloaded method that returns a JSON representation of the Koha::Patron object,
1769 suitable for API output.
1770
1771 =cut
1772
1773 sub to_api {
1774     my ( $self, $params ) = @_;
1775
1776     my $json_patron = $self->SUPER::to_api( $params );
1777
1778     $json_patron->{restricted} = ( $self->is_debarred )
1779                                     ? Mojo::JSON->true
1780                                     : Mojo::JSON->false;
1781
1782     return $json_patron;
1783 }
1784
1785 =head3 to_api_mapping
1786
1787 This method returns the mapping for representing a Koha::Patron object
1788 on the API.
1789
1790 =cut
1791
1792 sub to_api_mapping {
1793     return {
1794         borrowernotes       => 'staff_notes',
1795         borrowernumber      => 'patron_id',
1796         branchcode          => 'library_id',
1797         categorycode        => 'category_id',
1798         checkprevcheckout   => 'check_previous_checkout',
1799         contactfirstname    => undef,                     # Unused
1800         contactname         => undef,                     # Unused
1801         contactnote         => 'altaddress_notes',
1802         contacttitle        => undef,                     # Unused
1803         dateenrolled        => 'date_enrolled',
1804         dateexpiry          => 'expiry_date',
1805         dateofbirth         => 'date_of_birth',
1806         debarred            => undef,                     # replaced by 'restricted'
1807         debarredcomment     => undef,    # calculated, API consumers will use /restrictions instead
1808         emailpro            => 'secondary_email',
1809         flags               => undef,    # permissions manipulation handled in /permissions
1810         gonenoaddress       => 'incorrect_address',
1811         guarantorid         => 'guarantor_id',
1812         lastseen            => 'last_seen',
1813         lost                => 'patron_card_lost',
1814         opacnote            => 'opac_notes',
1815         othernames          => 'other_name',
1816         password            => undef,            # password manipulation handled in /password
1817         phonepro            => 'secondary_phone',
1818         relationship        => 'relationship_type',
1819         sex                 => 'gender',
1820         smsalertnumber      => 'sms_number',
1821         sort1               => 'statistics_1',
1822         sort2               => 'statistics_2',
1823         autorenew_checkouts => 'autorenew_checkouts',
1824         streetnumber        => 'street_number',
1825         streettype          => 'street_type',
1826         zipcode             => 'postal_code',
1827         B_address           => 'altaddress_address',
1828         B_address2          => 'altaddress_address2',
1829         B_city              => 'altaddress_city',
1830         B_country           => 'altaddress_country',
1831         B_email             => 'altaddress_email',
1832         B_phone             => 'altaddress_phone',
1833         B_state             => 'altaddress_state',
1834         B_streetnumber      => 'altaddress_street_number',
1835         B_streettype        => 'altaddress_street_type',
1836         B_zipcode           => 'altaddress_postal_code',
1837         altcontactaddress1  => 'altcontact_address',
1838         altcontactaddress2  => 'altcontact_address2',
1839         altcontactaddress3  => 'altcontact_city',
1840         altcontactcountry   => 'altcontact_country',
1841         altcontactfirstname => 'altcontact_firstname',
1842         altcontactphone     => 'altcontact_phone',
1843         altcontactsurname   => 'altcontact_surname',
1844         altcontactstate     => 'altcontact_state',
1845         altcontactzipcode   => 'altcontact_postal_code',
1846         primary_contact_method => undef,
1847     };
1848 }
1849
1850 =head3 queue_notice
1851
1852     Koha::Patrons->queue_notice({ letter_params => $letter_params, message_name => 'DUE'});
1853     Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports });
1854     Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports, test_mode => 1 });
1855
1856     Queue messages to a patron. Can pass a message that is part of the message_attributes
1857     table or supply the transport to use.
1858
1859     If passed a message name we retrieve the patrons preferences for transports
1860     Otherwise we use the supplied transport. In the case of email or sms we fall back to print if
1861     we have no address/number for sending
1862
1863     $letter_params is a hashref of the values to be passed to GetPreparedLetter
1864
1865     test_mode will only report which notices would be sent, but nothing will be queued
1866
1867 =cut
1868
1869 sub queue_notice {
1870     my ( $self, $params ) = @_;
1871     my $letter_params = $params->{letter_params};
1872     my $test_mode = $params->{test_mode};
1873
1874     return unless $letter_params;
1875     return unless exists $params->{message_name} xor $params->{message_transports}; # We only want one of these
1876
1877     my $library = Koha::Libraries->find( $letter_params->{branchcode} );
1878     my $from_email_address = $library->from_email_address;
1879
1880     my @message_transports;
1881     my $letter_code;
1882     $letter_code = $letter_params->{letter_code};
1883     if( $params->{message_name} ){
1884         my $messaging_prefs = C4::Members::Messaging::GetMessagingPreferences( {
1885                 borrowernumber => $letter_params->{borrowernumber},
1886                 message_name => $params->{message_name}
1887         } );
1888         @message_transports = ( keys %{ $messaging_prefs->{transports} } );
1889         $letter_code = $messaging_prefs->{transports}->{$message_transports[0]} unless $letter_code;
1890     } else {
1891         @message_transports = @{$params->{message_transports}};
1892     }
1893     return unless defined $letter_code;
1894     $letter_params->{letter_code} = $letter_code;
1895     my $print_sent = 0;
1896     my %return;
1897     foreach my $mtt (@message_transports){
1898         next if ($mtt eq 'itiva' and C4::Context->preference('TalkingTechItivaPhoneNotification') );
1899         # Notice is handled by TalkingTech_itiva_outbound.pl
1900         if (   ( $mtt eq 'email' and not $self->notice_email_address )
1901             or ( $mtt eq 'sms'   and not $self->smsalertnumber )
1902             or ( $mtt eq 'phone' and not $self->phone ) )
1903         {
1904             push @{ $return{fallback} }, $mtt;
1905             $mtt = 'print';
1906         }
1907         next if $mtt eq 'print' && $print_sent;
1908         $letter_params->{message_transport_type} = $mtt;
1909         my $letter = C4::Letters::GetPreparedLetter( %$letter_params );
1910         C4::Letters::EnqueueLetter({
1911             letter => $letter,
1912             borrowernumber => $self->borrowernumber,
1913             from_address   => $from_email_address,
1914             message_transport_type => $mtt
1915         }) unless $test_mode;
1916         push @{$return{sent}}, $mtt;
1917         $print_sent = 1 if $mtt eq 'print';
1918     }
1919     return \%return;
1920 }
1921
1922 =head2 Internal methods
1923
1924 =head3 _type
1925
1926 =cut
1927
1928 sub _type {
1929     return 'Borrower';
1930 }
1931
1932 =head1 AUTHORS
1933
1934 Kyle M Hall <kyle@bywatersolutions.com>
1935 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1936 Martin Renvoize <martin.renvoize@ptfs-europe.com>
1937
1938 =cut
1939
1940 1;