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