Bug 29300: REVERT
[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 Carp;
24 use List::MoreUtils qw( any uniq );
25 use JSON qw( to_json );
26 use Unicode::Normalize;
27
28 use C4::Context;
29 use C4::Log;
30 use Koha::Account;
31 use Koha::ArticleRequests;
32 use Koha::AuthUtils;
33 use Koha::Checkouts;
34 use Koha::Club::Enrollments;
35 use Koha::Database;
36 use Koha::DateUtils;
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 housebound_profile
493
494 Returns the HouseboundProfile associated with this patron.
495
496 =cut
497
498 sub housebound_profile {
499     my ( $self ) = @_;
500     my $profile = $self->_result->housebound_profile;
501     return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
502         if ( $profile );
503     return;
504 }
505
506 =head3 housebound_role
507
508 Returns the HouseboundRole associated with this patron.
509
510 =cut
511
512 sub housebound_role {
513     my ( $self ) = @_;
514
515     my $role = $self->_result->housebound_role;
516     return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
517     return;
518 }
519
520 =head3 siblings
521
522 Returns the siblings of this patron.
523
524 =cut
525
526 sub siblings {
527     my ($self) = @_;
528
529     my @guarantors = $self->guarantor_relationships()->guarantors();
530
531     return unless @guarantors;
532
533     my @siblings =
534       map { $_->guarantee_relationships()->guarantees() } @guarantors;
535
536     return unless @siblings;
537
538     my %seen;
539     @siblings =
540       grep { !$seen{ $_->id }++ && ( $_->id != $self->id ) } @siblings;
541
542     return wantarray ? @siblings : Koha::Patrons->search( { borrowernumber => { -in => [ map { $_->id } @siblings ] } } );
543 }
544
545 =head3 merge_with
546
547     my $patron = Koha::Patrons->find($id);
548     $patron->merge_with( \@patron_ids );
549
550     This subroutine merges a list of patrons into the patron record. This is accomplished by finding
551     all related patron ids for the patrons to be merged in other tables and changing the ids to be that
552     of the keeper patron.
553
554 =cut
555
556 sub merge_with {
557     my ( $self, $patron_ids ) = @_;
558
559     my $anonymous_patron = C4::Context->preference("AnonymousPatron");
560     return if $anonymous_patron && $self->id eq $anonymous_patron;
561
562     my @patron_ids = @{ $patron_ids };
563
564     # Ensure the keeper isn't in the list of patrons to merge
565     @patron_ids = grep { $_ ne $self->id } @patron_ids;
566
567     my $schema = Koha::Database->new()->schema();
568
569     my $results;
570
571     $self->_result->result_source->schema->txn_do( sub {
572         foreach my $patron_id (@patron_ids) {
573
574             next if $patron_id eq $anonymous_patron;
575
576             my $patron = Koha::Patrons->find( $patron_id );
577
578             next unless $patron;
579
580             # Unbless for safety, the patron will end up being deleted
581             $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
582
583             my $attributes = $patron->extended_attributes;
584             my $new_attributes = [
585                 map { { code => $_->code, attribute => $_->attribute } }
586                     $attributes->as_list
587             ];
588             $attributes->delete; # We need to delete before trying to merge them to prevent exception on unique and repeatable
589             for my $attribute ( @$new_attributes ) {
590                 $self->add_extended_attribute($attribute);
591             }
592
593             while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
594                 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
595                 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
596                 $rs->update({ $field => $self->id });
597                 if ( $r eq 'BorrowerDebarment' ) {
598                     Koha::Patron::Debarments::UpdateBorrowerDebarmentFlags($self->id);
599                 }
600             }
601
602             $patron->move_to_deleted();
603             $patron->delete();
604         }
605     });
606
607     return $results;
608 }
609
610
611
612 =head3 wants_check_for_previous_checkout
613
614     $wants_check = $patron->wants_check_for_previous_checkout;
615
616 Return 1 if Koha needs to perform PrevIssue checking, else 0.
617
618 =cut
619
620 sub wants_check_for_previous_checkout {
621     my ( $self ) = @_;
622     my $syspref = C4::Context->preference("checkPrevCheckout");
623
624     # Simple cases
625     ## Hard syspref trumps all
626     return 1 if ($syspref eq 'hardyes');
627     return 0 if ($syspref eq 'hardno');
628     ## Now, patron pref trumps all
629     return 1 if ($self->checkprevcheckout eq 'yes');
630     return 0 if ($self->checkprevcheckout eq 'no');
631
632     # More complex: patron inherits -> determine category preference
633     my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
634     return 1 if ($checkPrevCheckoutByCat eq 'yes');
635     return 0 if ($checkPrevCheckoutByCat eq 'no');
636
637     # Finally: category preference is inherit, default to 0
638     if ($syspref eq 'softyes') {
639         return 1;
640     } else {
641         return 0;
642     }
643 }
644
645 =head3 do_check_for_previous_checkout
646
647     $do_check = $patron->do_check_for_previous_checkout($item);
648
649 Return 1 if the bib associated with $ITEM has previously been checked out to
650 $PATRON, 0 otherwise.
651
652 =cut
653
654 sub do_check_for_previous_checkout {
655     my ( $self, $item ) = @_;
656
657     my @item_nos;
658     my $biblio = Koha::Biblios->find( $item->{biblionumber} );
659     if ( $biblio->is_serial ) {
660         push @item_nos, $item->{itemnumber};
661     } else {
662         # Get all itemnumbers for given bibliographic record.
663         @item_nos = $biblio->items->get_column( 'itemnumber' );
664     }
665
666     # Create (old)issues search criteria
667     my $criteria = {
668         borrowernumber => $self->borrowernumber,
669         itemnumber => \@item_nos,
670     };
671
672     # Check current issues table
673     my $issues = Koha::Checkouts->search($criteria);
674     return 1 if $issues->count; # 0 || N
675
676     # Check old issues table
677     my $old_issues = Koha::Old::Checkouts->search($criteria);
678     return $old_issues->count;  # 0 || N
679 }
680
681 =head3 is_debarred
682
683 my $debarment_expiration = $patron->is_debarred;
684
685 Returns the date a patron debarment will expire, or undef if the patron is not
686 debarred
687
688 =cut
689
690 sub is_debarred {
691     my ($self) = @_;
692
693     return unless $self->debarred;
694     return $self->debarred
695       if $self->debarred =~ '^9999'
696       or dt_from_string( $self->debarred ) > dt_from_string;
697     return;
698 }
699
700 =head3 is_expired
701
702 my $is_expired = $patron->is_expired;
703
704 Returns 1 if the patron is expired or 0;
705
706 =cut
707
708 sub is_expired {
709     my ($self) = @_;
710     return 0 unless $self->dateexpiry;
711     return 0 if $self->dateexpiry =~ '^9999';
712     return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
713     return 0;
714 }
715
716 =head3 is_going_to_expire
717
718 my $is_going_to_expire = $patron->is_going_to_expire;
719
720 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
721
722 =cut
723
724 sub is_going_to_expire {
725     my ($self) = @_;
726
727     my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
728
729     return 0 unless $delay;
730     return 0 unless $self->dateexpiry;
731     return 0 if $self->dateexpiry =~ '^9999';
732     return 1 if dt_from_string( $self->dateexpiry, undef, 'floating' )->subtract( days => $delay ) < dt_from_string(undef, undef, 'floating')->truncate( to => 'day' );
733     return 0;
734 }
735
736 =head3 set_password
737
738     $patron->set_password({ password => $plain_text_password [, skip_validation => 1 ] });
739
740 Set the patron's password.
741
742 =head4 Exceptions
743
744 The passed string is validated against the current password enforcement policy.
745 Validation can be skipped by passing the I<skip_validation> parameter.
746
747 Exceptions are thrown if the password is not good enough.
748
749 =over 4
750
751 =item Koha::Exceptions::Password::TooShort
752
753 =item Koha::Exceptions::Password::WhitespaceCharacters
754
755 =item Koha::Exceptions::Password::TooWeak
756
757 =item Koha::Exceptions::Password::Plugin (if a "check password" plugin is enabled)
758
759 =back
760
761 =cut
762
763 sub set_password {
764     my ( $self, $args ) = @_;
765
766     my $password = $args->{password};
767
768     unless ( $args->{skip_validation} ) {
769         my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password );
770
771         if ( !$is_valid ) {
772             if ( $error eq 'too_short' ) {
773                 my $min_length = C4::Context->preference('minPasswordLength');
774                 $min_length = 3 if not $min_length or $min_length < 3;
775
776                 my $password_length = length($password);
777                 Koha::Exceptions::Password::TooShort->throw(
778                     length => $password_length, min_length => $min_length );
779             }
780             elsif ( $error eq 'has_whitespaces' ) {
781                 Koha::Exceptions::Password::WhitespaceCharacters->throw();
782             }
783             elsif ( $error eq 'too_weak' ) {
784                 Koha::Exceptions::Password::TooWeak->throw();
785             }
786         }
787     }
788
789     if ( C4::Context->config("enable_plugins") ) {
790         # Call any check_password plugins
791         my @plugins = Koha::Plugins->new()->GetPlugins({
792             method => 'check_password',
793         });
794         foreach my $plugin ( @plugins ) {
795             # This plugin hook will also be used by a plugin for the Norwegian national
796             # patron database. This is why we need to pass both the password and the
797             # borrowernumber to the plugin.
798             my $ret = $plugin->check_password(
799                 {
800                     password       => $password,
801                     borrowernumber => $self->borrowernumber
802                 }
803             );
804             # This plugin hook will also be used by a plugin for the Norwegian national
805             # patron database. This is why we need to call the actual plugins and then
806             # check skip_validation afterwards.
807             if ( $ret->{'error'} == 1 && !$args->{skip_validation} ) {
808                 Koha::Exceptions::Password::Plugin->throw();
809             }
810         }
811     }
812
813     my $digest = Koha::AuthUtils::hash_password($password);
814
815     # We do not want to call $self->store and retrieve password from DB
816     $self->password($digest);
817     $self->login_attempts(0);
818     $self->SUPER::store;
819
820     logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" )
821         if C4::Context->preference("BorrowersLog");
822
823     return $self;
824 }
825
826
827 =head3 renew_account
828
829 my $new_expiry_date = $patron->renew_account
830
831 Extending the subscription to the expiry date.
832
833 =cut
834
835 sub renew_account {
836     my ($self) = @_;
837     my $date;
838     if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
839         $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
840     } else {
841         $date =
842             C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
843             ? dt_from_string( $self->dateexpiry )
844             : dt_from_string;
845     }
846     my $expiry_date = $self->category->get_expiry_date($date);
847
848     $self->dateexpiry($expiry_date);
849     $self->date_renewed( dt_from_string() );
850     $self->store();
851
852     $self->add_enrolment_fee_if_needed(1);
853
854     logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
855     return dt_from_string( $expiry_date )->truncate( to => 'day' );
856 }
857
858 =head3 has_overdues
859
860 my $has_overdues = $patron->has_overdues;
861
862 Returns the number of patron's overdues
863
864 =cut
865
866 sub has_overdues {
867     my ($self) = @_;
868     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
869     return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
870 }
871
872 =head3 track_login
873
874     $patron->track_login;
875     $patron->track_login({ force => 1 });
876
877     Tracks a (successful) login attempt.
878     The preference TrackLastPatronActivity must be enabled. Or you
879     should pass the force parameter.
880
881 =cut
882
883 sub track_login {
884     my ( $self, $params ) = @_;
885     return if
886         !$params->{force} &&
887         !C4::Context->preference('TrackLastPatronActivity');
888     $self->lastseen( dt_from_string() )->store;
889 }
890
891 =head3 move_to_deleted
892
893 my $is_moved = $patron->move_to_deleted;
894
895 Move a patron to the deletedborrowers table.
896 This can be done before deleting a patron, to make sure the data are not completely deleted.
897
898 =cut
899
900 sub move_to_deleted {
901     my ($self) = @_;
902     my $patron_infos = $self->unblessed;
903     delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
904     return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
905 }
906
907 =head3 article_requests
908
909 my @requests = $borrower->article_requests();
910 my $requests = $borrower->article_requests();
911
912 Returns either a list of ArticleRequests objects,
913 or an ArtitleRequests object, depending on the
914 calling context.
915
916 =cut
917
918 sub article_requests {
919     my ( $self ) = @_;
920
921     $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
922
923     return $self->{_article_requests};
924 }
925
926 =head3 article_requests_current
927
928 my @requests = $patron->article_requests_current
929
930 Returns the article requests associated with this patron that are incomplete
931
932 =cut
933
934 sub article_requests_current {
935     my ( $self ) = @_;
936
937     $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
938         {
939             borrowernumber => $self->id(),
940             -or          => [
941                 { status => Koha::ArticleRequest::Status::Pending },
942                 { status => Koha::ArticleRequest::Status::Processing }
943             ]
944         }
945     );
946
947     return $self->{_article_requests_current};
948 }
949
950 =head3 article_requests_finished
951
952 my @requests = $biblio->article_requests_finished
953
954 Returns the article requests associated with this patron that are completed
955
956 =cut
957
958 sub article_requests_finished {
959     my ( $self, $borrower ) = @_;
960
961     $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
962         {
963             borrowernumber => $self->id(),
964             -or          => [
965                 { status => Koha::ArticleRequest::Status::Completed },
966                 { status => Koha::ArticleRequest::Status::Canceled }
967             ]
968         }
969     );
970
971     return $self->{_article_requests_finished};
972 }
973
974 =head3 add_enrolment_fee_if_needed
975
976 my $enrolment_fee = $patron->add_enrolment_fee_if_needed($renewal);
977
978 Add enrolment fee for a patron if needed.
979
980 $renewal - boolean denoting whether this is an account renewal or not
981
982 =cut
983
984 sub add_enrolment_fee_if_needed {
985     my ($self, $renewal) = @_;
986     my $enrolment_fee = $self->category->enrolmentfee;
987     if ( $enrolment_fee && $enrolment_fee > 0 ) {
988         my $type = $renewal ? 'ACCOUNT_RENEW' : 'ACCOUNT';
989         $self->account->add_debit(
990             {
991                 amount     => $enrolment_fee,
992                 user_id    => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
993                 interface  => C4::Context->interface,
994                 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
995                 type       => $type
996             }
997         );
998     }
999     return $enrolment_fee || 0;
1000 }
1001
1002 =head3 checkouts
1003
1004 my $checkouts = $patron->checkouts
1005
1006 =cut
1007
1008 sub checkouts {
1009     my ($self) = @_;
1010     my $checkouts = $self->_result->issues;
1011     return Koha::Checkouts->_new_from_dbic( $checkouts );
1012 }
1013
1014 =head3 pending_checkouts
1015
1016 my $pending_checkouts = $patron->pending_checkouts
1017
1018 This method will return the same as $self->checkouts, but with a prefetch on
1019 items, biblio and biblioitems.
1020
1021 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
1022
1023 It should not be used directly, prefer to access fields you need instead of
1024 retrieving all these fields in one go.
1025
1026 =cut
1027
1028 sub pending_checkouts {
1029     my( $self ) = @_;
1030     my $checkouts = $self->_result->issues->search(
1031         {},
1032         {
1033             order_by => [
1034                 { -desc => 'me.timestamp' },
1035                 { -desc => 'issuedate' },
1036                 { -desc => 'issue_id' }, # Sort by issue_id should be enough
1037             ],
1038             prefetch => { item => { biblio => 'biblioitems' } },
1039         }
1040     );
1041     return Koha::Checkouts->_new_from_dbic( $checkouts );
1042 }
1043
1044 =head3 old_checkouts
1045
1046 my $old_checkouts = $patron->old_checkouts
1047
1048 =cut
1049
1050 sub old_checkouts {
1051     my ($self) = @_;
1052     my $old_checkouts = $self->_result->old_issues;
1053     return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
1054 }
1055
1056 =head3 get_overdues
1057
1058 my $overdue_items = $patron->get_overdues
1059
1060 Return the overdue items
1061
1062 =cut
1063
1064 sub get_overdues {
1065     my ($self) = @_;
1066     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
1067     return $self->checkouts->search(
1068         {
1069             'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
1070         },
1071         {
1072             prefetch => { item => { biblio => 'biblioitems' } },
1073         }
1074     );
1075 }
1076
1077 =head3 get_routing_lists
1078
1079 my @routinglists = $patron->get_routing_lists
1080
1081 Returns the routing lists a patron is subscribed to.
1082
1083 =cut
1084
1085 sub get_routing_lists {
1086     my ($self) = @_;
1087     my $routing_list_rs = $self->_result->subscriptionroutinglists;
1088     return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
1089 }
1090
1091 =head3 get_age
1092
1093 my $age = $patron->get_age
1094
1095 Return the age of the patron
1096
1097 =cut
1098
1099 sub get_age {
1100     my ($self)    = @_;
1101     my $today_str = dt_from_string->strftime("%Y-%m-%d");
1102     return unless $self->dateofbirth;
1103     my $dob_str   = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
1104
1105     my ( $dob_y,   $dob_m,   $dob_d )   = split /-/, $dob_str;
1106     my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
1107
1108     my $age = $today_y - $dob_y;
1109     if ( $dob_m . $dob_d > $today_m . $today_d ) {
1110         $age--;
1111     }
1112
1113     return $age;
1114 }
1115
1116 =head3 is_valid_age
1117
1118 my $is_valid = $patron->is_valid_age
1119
1120 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
1121
1122 =cut
1123
1124 sub is_valid_age {
1125     my ($self) = @_;
1126     my $age = $self->get_age;
1127
1128     my $patroncategory = $self->category;
1129     my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
1130
1131     return (defined($age) && (($high && ($age > $high)) or ($low && ($age < $low)))) ? 0 : 1;
1132 }
1133
1134 =head3 account
1135
1136 my $account = $patron->account
1137
1138 =cut
1139
1140 sub account {
1141     my ($self) = @_;
1142     return Koha::Account->new( { patron_id => $self->borrowernumber } );
1143 }
1144
1145 =head3 holds
1146
1147 my $holds = $patron->holds
1148
1149 Return all the holds placed by this patron
1150
1151 =cut
1152
1153 sub holds {
1154     my ($self) = @_;
1155     my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1156     return Koha::Holds->_new_from_dbic($holds_rs);
1157 }
1158
1159 =head3 old_holds
1160
1161 my $old_holds = $patron->old_holds
1162
1163 Return all the historical holds for this patron
1164
1165 =cut
1166
1167 sub old_holds {
1168     my ($self) = @_;
1169     my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1170     return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1171 }
1172
1173 =head3 return_claims
1174
1175 my $return_claims = $patron->return_claims
1176
1177 =cut
1178
1179 sub return_claims {
1180     my ($self) = @_;
1181     my $return_claims = $self->_result->return_claims_borrowernumbers;
1182     return Koha::Checkouts::ReturnClaims->_new_from_dbic( $return_claims );
1183 }
1184
1185 =head3 notice_email_address
1186
1187   my $email = $patron->notice_email_address;
1188
1189 Return the email address of patron used for notices.
1190 Returns the empty string if no email address.
1191
1192 =cut
1193
1194 sub notice_email_address{
1195     my ( $self ) = @_;
1196
1197     my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1198     # if syspref is set to 'first valid' (value == OFF), look up email address
1199     if ( $which_address eq 'OFF' ) {
1200         return $self->first_valid_email_address;
1201     }
1202
1203     return $self->$which_address || '';
1204 }
1205
1206 =head3 first_valid_email_address
1207
1208 my $first_valid_email_address = $patron->first_valid_email_address
1209
1210 Return the first valid email address for a patron.
1211 For now, the order  is defined as email, emailpro, B_email.
1212 Returns the empty string if the borrower has no email addresses.
1213
1214 =cut
1215
1216 sub first_valid_email_address {
1217     my ($self) = @_;
1218
1219     return $self->email() || $self->emailpro() || $self->B_email() || q{};
1220 }
1221
1222 =head3 get_club_enrollments
1223
1224 =cut
1225
1226 sub get_club_enrollments {
1227     my ( $self, $return_scalar ) = @_;
1228
1229     my $e = Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1230
1231     return $e if $return_scalar;
1232
1233     return wantarray ? $e->as_list : $e;
1234 }
1235
1236 =head3 get_enrollable_clubs
1237
1238 =cut
1239
1240 sub get_enrollable_clubs {
1241     my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
1242
1243     my $params;
1244     $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1245       if $is_enrollable_from_opac;
1246     $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1247
1248     $params->{borrower} = $self;
1249
1250     my $e = Koha::Clubs->get_enrollable($params);
1251
1252     return $e if $return_scalar;
1253
1254     return wantarray ? $e->as_list : $e;
1255 }
1256
1257 =head3 account_locked
1258
1259 my $is_locked = $patron->account_locked
1260
1261 Return true if the patron has reached the maximum number of login attempts
1262 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1263 as an administrative lockout (independent of FailedLoginAttempts; see also
1264 Koha::Patron->lock).
1265 Otherwise return false.
1266 If the pref is not set (empty string, null or 0), the feature is considered as
1267 disabled.
1268
1269 =cut
1270
1271 sub account_locked {
1272     my ($self) = @_;
1273     my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1274     return 1 if $FailedLoginAttempts
1275           and $self->login_attempts
1276           and $self->login_attempts >= $FailedLoginAttempts;
1277     return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1278     return 0;
1279 }
1280
1281 =head3 can_see_patron_infos
1282
1283 my $can_see = $patron->can_see_patron_infos( $patron );
1284
1285 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1286
1287 =cut
1288
1289 sub can_see_patron_infos {
1290     my ( $self, $patron ) = @_;
1291     return unless $patron;
1292     return $self->can_see_patrons_from( $patron->library->branchcode );
1293 }
1294
1295 =head3 can_see_patrons_from
1296
1297 my $can_see = $patron->can_see_patrons_from( $branchcode );
1298
1299 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1300
1301 =cut
1302
1303 sub can_see_patrons_from {
1304     my ( $self, $branchcode ) = @_;
1305     my $can = 0;
1306     if ( $self->branchcode eq $branchcode ) {
1307         $can = 1;
1308     } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1309         $can = 1;
1310     } elsif ( my $library_groups = $self->library->library_groups ) {
1311         while ( my $library_group = $library_groups->next ) {
1312             if ( $library_group->parent->has_child( $branchcode ) ) {
1313                 $can = 1;
1314                 last;
1315             }
1316         }
1317     }
1318     return $can;
1319 }
1320
1321 =head3 libraries_where_can_see_patrons
1322
1323 my $libraries = $patron-libraries_where_can_see_patrons;
1324
1325 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1326 The branchcodes are arbitrarily returned sorted.
1327 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1328
1329 An empty array means no restriction, the patron can see patron's infos from any libraries.
1330
1331 =cut
1332
1333 sub libraries_where_can_see_patrons {
1334     my ( $self ) = @_;
1335     my $userenv = C4::Context->userenv;
1336
1337     return () unless $userenv; # For tests, but userenv should be defined in tests...
1338
1339     my @restricted_branchcodes;
1340     if (C4::Context::only_my_library) {
1341         push @restricted_branchcodes, $self->branchcode;
1342     }
1343     else {
1344         unless (
1345             $self->has_permission(
1346                 { borrowers => 'view_borrower_infos_from_any_libraries' }
1347             )
1348           )
1349         {
1350             my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1351             if ( $library_groups->count )
1352             {
1353                 while ( my $library_group = $library_groups->next ) {
1354                     my $parent = $library_group->parent;
1355                     if ( $parent->has_child( $self->branchcode ) ) {
1356                         push @restricted_branchcodes, $parent->children->get_column('branchcode');
1357                     }
1358                 }
1359             }
1360
1361             @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1362         }
1363     }
1364
1365     @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1366     @restricted_branchcodes = uniq(@restricted_branchcodes);
1367     @restricted_branchcodes = sort(@restricted_branchcodes);
1368     return @restricted_branchcodes;
1369 }
1370
1371 =head3 has_permission
1372
1373 my $permission = $patron->has_permission($required);
1374
1375 See C4::Auth::haspermission for details of syntax for $required
1376
1377 =cut
1378
1379 sub has_permission {
1380     my ( $self, $flagsrequired ) = @_;
1381     return unless $self->userid;
1382     # TODO code from haspermission needs to be moved here!
1383     return C4::Auth::haspermission( $self->userid, $flagsrequired );
1384 }
1385
1386 =head3 is_superlibrarian
1387
1388   my $is_superlibrarian = $patron->is_superlibrarian;
1389
1390 Return true if the patron is a superlibrarian.
1391
1392 =cut
1393
1394 sub is_superlibrarian {
1395     my ($self) = @_;
1396     return $self->has_permission( { superlibrarian => 1 } );
1397 }
1398
1399 =head3 is_adult
1400
1401 my $is_adult = $patron->is_adult
1402
1403 Return true if the patron has a category with a type Adult (A) or Organization (I)
1404
1405 =cut
1406
1407 sub is_adult {
1408     my ( $self ) = @_;
1409     return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1410 }
1411
1412 =head3 is_child
1413
1414 my $is_child = $patron->is_child
1415
1416 Return true if the patron has a category with a type Child (C)
1417
1418 =cut
1419
1420 sub is_child {
1421     my( $self ) = @_;
1422     return $self->category->category_type eq 'C' ? 1 : 0;
1423 }
1424
1425 =head3 has_valid_userid
1426
1427 my $patron = Koha::Patrons->find(42);
1428 $patron->userid( $new_userid );
1429 my $has_a_valid_userid = $patron->has_valid_userid
1430
1431 my $patron = Koha::Patron->new( $params );
1432 my $has_a_valid_userid = $patron->has_valid_userid
1433
1434 Return true if the current userid of this patron is valid/unique, otherwise false.
1435
1436 Note that this should be done in $self->store instead and raise an exception if needed.
1437
1438 =cut
1439
1440 sub has_valid_userid {
1441     my ($self) = @_;
1442
1443     return 0 unless $self->userid;
1444
1445     return 0 if ( $self->userid eq C4::Context->config('user') );    # DB user
1446
1447     my $already_exists = Koha::Patrons->search(
1448         {
1449             userid => $self->userid,
1450             (
1451                 $self->in_storage
1452                 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1453                 : ()
1454             ),
1455         }
1456     )->count;
1457     return $already_exists ? 0 : 1;
1458 }
1459
1460 =head3 generate_userid
1461
1462 my $patron = Koha::Patron->new( $params );
1463 $patron->generate_userid
1464
1465 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1466
1467 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).
1468
1469 =cut
1470
1471 sub generate_userid {
1472     my ($self) = @_;
1473     my $offset = 0;
1474     my $firstname = $self->firstname // q{};
1475     my $surname = $self->surname // q{};
1476     #The script will "do" the following code and increment the $offset until the generated userid is unique
1477     do {
1478       $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1479       $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1480       my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1481       $userid = NFKD( $userid );
1482       $userid =~ s/\p{NonspacingMark}//g;
1483       $userid .= $offset unless $offset == 0;
1484       $self->userid( $userid );
1485       $offset++;
1486      } while (! $self->has_valid_userid );
1487
1488      return $self;
1489 }
1490
1491 =head3 add_extended_attribute
1492
1493 =cut
1494
1495 sub add_extended_attribute {
1496     my ($self, $attribute) = @_;
1497     $attribute->{borrowernumber} = $self->borrowernumber;
1498     return Koha::Patron::Attribute->new($attribute)->store;
1499 }
1500
1501 =head3 extended_attributes
1502
1503 Return object of Koha::Patron::Attributes type with all attributes set for this patron
1504
1505 Or setter FIXME
1506
1507 =cut
1508
1509 sub extended_attributes {
1510     my ( $self, $attributes ) = @_;
1511     if ($attributes) {    # setter
1512         my $schema = $self->_result->result_source->schema;
1513         $schema->txn_do(
1514             sub {
1515                 # Remove the existing one
1516                 $self->extended_attributes->filter_by_branch_limitations->delete;
1517
1518                 # Insert the new ones
1519                 for my $attribute (@$attributes) {
1520                     eval {
1521                         $self->_result->create_related('borrower_attributes', $attribute);
1522                     };
1523                     # FIXME We should:
1524                     # 1 - Raise an exception
1525                     # 2 - Execute in a transaction and don't save
1526                     #  or Insert anyway but display a message on the UI
1527                     warn $@ if $@;
1528                 }
1529             }
1530         );
1531     }
1532
1533     my $rs = $self->_result->borrower_attributes;
1534     # We call search to use the filters in Koha::Patron::Attributes->search
1535     return Koha::Patron::Attributes->_new_from_dbic($rs)->search;
1536 }
1537
1538 =head3 lock
1539
1540     Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
1541
1542     Lock and optionally expire a patron account.
1543     Remove holds and article requests if remove flag set.
1544     In order to distinguish from locking by entering a wrong password, let's
1545     call this an administrative lockout.
1546
1547 =cut
1548
1549 sub lock {
1550     my ( $self, $params ) = @_;
1551     $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
1552     if( $params->{expire} ) {
1553         $self->dateexpiry( dt_from_string->subtract(days => 1) );
1554     }
1555     $self->store;
1556     if( $params->{remove} ) {
1557         $self->holds->delete;
1558         $self->article_requests->delete;
1559     }
1560     return $self;
1561 }
1562
1563 =head3 anonymize
1564
1565     Koha::Patrons->find($id)->anonymize;
1566
1567     Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
1568     are randomized, other personal data is cleared too.
1569     Patrons with issues are skipped.
1570
1571 =cut
1572
1573 sub anonymize {
1574     my ( $self ) = @_;
1575     if( $self->_result->issues->count ) {
1576         warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
1577         return;
1578     }
1579     # Mandatory fields come from the corresponding pref, but email fields
1580     # are removed since scrambled email addresses only generate errors
1581     my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
1582         split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
1583     $mandatory->{userid} = 1; # needed since sub store does not clear field
1584     my @columns = $self->_result->result_source->columns;
1585     @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized/ } @columns;
1586     push @columns, 'dateofbirth'; # add this date back in
1587     foreach my $col (@columns) {
1588         $self->_anonymize_column($col, $mandatory->{lc $col} );
1589     }
1590     $self->anonymized(1)->store;
1591 }
1592
1593 sub _anonymize_column {
1594     my ( $self, $col, $mandatory ) = @_;
1595     my $col_info = $self->_result->result_source->column_info($col);
1596     my $type = $col_info->{data_type};
1597     my $nullable = $col_info->{is_nullable};
1598     my $val;
1599     if( $type =~ /char|text/ ) {
1600         $val = $mandatory
1601             ? Koha::Token->new->generate({ pattern => '\w{10}' })
1602             : $nullable
1603             ? undef
1604             : q{};
1605     } elsif( $type =~ /integer|int$|float|dec|double/ ) {
1606         $val = $nullable ? undef : 0;
1607     } elsif( $type =~ /date|time/ ) {
1608         $val = $nullable ? undef : dt_from_string;
1609     }
1610     $self->$col($val);
1611 }
1612
1613 =head3 add_guarantor
1614
1615     my @relationships = $patron->add_guarantor(
1616         {
1617             borrowernumber => $borrowernumber,
1618             relationships  => $relationship,
1619         }
1620     );
1621
1622     Adds a new guarantor to a patron.
1623
1624 =cut
1625
1626 sub add_guarantor {
1627     my ( $self, $params ) = @_;
1628
1629     my $guarantor_id = $params->{guarantor_id};
1630     my $relationship = $params->{relationship};
1631
1632     return Koha::Patron::Relationship->new(
1633         {
1634             guarantee_id => $self->id,
1635             guarantor_id => $guarantor_id,
1636             relationship => $relationship
1637         }
1638     )->store();
1639 }
1640
1641 =head3 get_extended_attribute
1642
1643 my $attribute_value = $patron->get_extended_attribute( $code );
1644
1645 Return the attribute for the code passed in parameter.
1646
1647 It not exist it returns undef
1648
1649 Note that this will not work for repeatable attribute types.
1650
1651 Maybe you certainly not want to use this method, it is actually only used for SHOW_BARCODE
1652 (which should be a real patron's attribute (not extended)
1653
1654 =cut
1655
1656 sub get_extended_attribute {
1657     my ( $self, $code, $value ) = @_;
1658     my $rs = $self->_result->borrower_attributes;
1659     return unless $rs;
1660     my $attribute = $rs->search({ code => $code, ( $value ? ( attribute => $value ) : () ) });
1661     return unless $attribute->count;
1662     return $attribute->next;
1663 }
1664
1665 =head3 to_api
1666
1667     my $json = $patron->to_api;
1668
1669 Overloaded method that returns a JSON representation of the Koha::Patron object,
1670 suitable for API output.
1671
1672 =cut
1673
1674 sub to_api {
1675     my ( $self, $params ) = @_;
1676
1677     my $json_patron = $self->SUPER::to_api( $params );
1678
1679     $json_patron->{restricted} = ( $self->is_debarred )
1680                                     ? Mojo::JSON->true
1681                                     : Mojo::JSON->false;
1682
1683     return $json_patron;
1684 }
1685
1686 =head3 to_api_mapping
1687
1688 This method returns the mapping for representing a Koha::Patron object
1689 on the API.
1690
1691 =cut
1692
1693 sub to_api_mapping {
1694     return {
1695         borrowernotes       => 'staff_notes',
1696         borrowernumber      => 'patron_id',
1697         branchcode          => 'library_id',
1698         categorycode        => 'category_id',
1699         checkprevcheckout   => 'check_previous_checkout',
1700         contactfirstname    => undef,                     # Unused
1701         contactname         => undef,                     # Unused
1702         contactnote         => 'altaddress_notes',
1703         contacttitle        => undef,                     # Unused
1704         dateenrolled        => 'date_enrolled',
1705         dateexpiry          => 'expiry_date',
1706         dateofbirth         => 'date_of_birth',
1707         debarred            => undef,                     # replaced by 'restricted'
1708         debarredcomment     => undef,    # calculated, API consumers will use /restrictions instead
1709         emailpro            => 'secondary_email',
1710         flags               => undef,    # permissions manipulation handled in /permissions
1711         gonenoaddress       => 'incorrect_address',
1712         guarantorid         => 'guarantor_id',
1713         lastseen            => 'last_seen',
1714         lost                => 'patron_card_lost',
1715         opacnote            => 'opac_notes',
1716         othernames          => 'other_name',
1717         password            => undef,            # password manipulation handled in /password
1718         phonepro            => 'secondary_phone',
1719         relationship        => 'relationship_type',
1720         sex                 => 'gender',
1721         smsalertnumber      => 'sms_number',
1722         sort1               => 'statistics_1',
1723         sort2               => 'statistics_2',
1724         autorenew_checkouts => 'autorenew_checkouts',
1725         streetnumber        => 'street_number',
1726         streettype          => 'street_type',
1727         zipcode             => 'postal_code',
1728         B_address           => 'altaddress_address',
1729         B_address2          => 'altaddress_address2',
1730         B_city              => 'altaddress_city',
1731         B_country           => 'altaddress_country',
1732         B_email             => 'altaddress_email',
1733         B_phone             => 'altaddress_phone',
1734         B_state             => 'altaddress_state',
1735         B_streetnumber      => 'altaddress_street_number',
1736         B_streettype        => 'altaddress_street_type',
1737         B_zipcode           => 'altaddress_postal_code',
1738         altcontactaddress1  => 'altcontact_address',
1739         altcontactaddress2  => 'altcontact_address2',
1740         altcontactaddress3  => 'altcontact_city',
1741         altcontactcountry   => 'altcontact_country',
1742         altcontactfirstname => 'altcontact_firstname',
1743         altcontactphone     => 'altcontact_phone',
1744         altcontactsurname   => 'altcontact_surname',
1745         altcontactstate     => 'altcontact_state',
1746         altcontactzipcode   => 'altcontact_postal_code'
1747     };
1748 }
1749
1750 =head2 Internal methods
1751
1752 =head3 _type
1753
1754 =cut
1755
1756 sub _type {
1757     return 'Borrower';
1758 }
1759
1760 =head1 AUTHORS
1761
1762 Kyle M Hall <kyle@bywatersolutions.com>
1763 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1764 Martin Renvoize <martin.renvoize@ptfs-europe.com>
1765
1766 =cut
1767
1768 1;