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