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