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