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