Bug 33974: (follow-up) biblionumber column needs special handling
[koha.git] / Koha / Patron.pm
1 package Koha::Patron;
2
3 # Copyright ByWater Solutions 2014
4 # Copyright PTFS Europe 2016
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20
21 use Modern::Perl;
22
23 use List::MoreUtils qw( any uniq );
24 use JSON qw( to_json );
25 use Unicode::Normalize qw( NFKD );
26 use Try::Tiny;
27
28 use C4::Context;
29 use C4::Auth qw( checkpw_hash );
30 use C4::Log qw( logaction );
31 use Koha::Account;
32 use Koha::ArticleRequests;
33 use C4::Letters qw( GetPreparedLetter EnqueueLetter SendQueuedMessages );
34 use Koha::AuthUtils;
35 use Koha::Checkouts;
36 use Koha::CirculationRules;
37 use Koha::Club::Enrollments;
38 use Koha::Database;
39 use Koha::DateUtils qw( dt_from_string );
40 use Koha::Encryption;
41 use Koha::Exceptions::Password;
42 use Koha::Holds;
43 use Koha::CurbsidePickups;
44 use Koha::Old::Checkouts;
45 use Koha::Patron::Attributes;
46 use Koha::Patron::Categories;
47 use Koha::Patron::Debarments;
48 use Koha::Patron::HouseboundProfile;
49 use Koha::Patron::HouseboundRole;
50 use Koha::Patron::Images;
51 use Koha::Patron::Messages;
52 use Koha::Patron::Modifications;
53 use Koha::Patron::Relationships;
54 use Koha::Patron::Restrictions;
55 use Koha::Patrons;
56 use Koha::Plugins;
57 use Koha::Recalls;
58 use Koha::Result::Boolean;
59 use Koha::Subscription::Routinglists;
60 use Koha::Token;
61 use Koha::Virtualshelves;
62
63 use base qw(Koha::Object);
64
65 use constant ADMINISTRATIVE_LOCKOUT => -1;
66
67 our $RESULTSET_PATRON_ID_MAPPING = {
68     Accountline          => 'borrowernumber',
69     Aqbasketuser         => 'borrowernumber',
70     Aqbudget             => 'budget_owner_id',
71     Aqbudgetborrower     => 'borrowernumber',
72     ArticleRequest       => 'borrowernumber',
73     BorrowerDebarment    => 'borrowernumber',
74     BorrowerFile         => 'borrowernumber',
75     BorrowerModification => 'borrowernumber',
76     ClubEnrollment       => 'borrowernumber',
77     Issue                => 'borrowernumber',
78     ItemsLastBorrower    => 'borrowernumber',
79     Linktracker          => 'borrowernumber',
80     Message              => 'borrowernumber',
81     MessageQueue         => 'borrowernumber',
82     OldIssue             => 'borrowernumber',
83     OldReserve           => 'borrowernumber',
84     Rating               => 'borrowernumber',
85     Reserve              => 'borrowernumber',
86     Review               => 'borrowernumber',
87     SearchHistory        => 'userid',
88     Statistic            => 'borrowernumber',
89     Suggestion           => 'suggestedby',
90     TagAll               => 'borrowernumber',
91     Virtualshelfcontent  => 'borrowernumber',
92     Virtualshelfshare    => 'borrowernumber',
93     Virtualshelve        => 'owner',
94 };
95
96 =head1 NAME
97
98 Koha::Patron - Koha Patron Object class
99
100 =head1 API
101
102 =head2 Class Methods
103
104 =head3 new
105
106 =cut
107
108 sub new {
109     my ( $class, $params ) = @_;
110
111     return $class->SUPER::new($params);
112 }
113
114 =head3 fixup_cardnumber
115
116 Autogenerate next cardnumber from highest value found in database
117
118 =cut
119
120 sub fixup_cardnumber {
121     my ( $self ) = @_;
122
123     my $max = $self->cardnumber;
124     Koha::Plugins->call( 'patron_barcode_transform', \$max );
125
126     $max ||= Koha::Patrons->search({
127         cardnumber => {-regexp => '^-?[0-9]+$'}
128     }, {
129         select => \'CAST(cardnumber AS SIGNED)',
130         as => ['cast_cardnumber']
131     })->_resultset->get_column('cast_cardnumber')->max;
132     $self->cardnumber(($max || 0) +1);
133 }
134
135 =head3 trim_whitespace
136
137 trim whitespace from data which has some non-whitespace in it.
138 Could be moved to Koha::Object if need to be reused
139
140 =cut
141
142 sub trim_whitespaces {
143     my( $self ) = @_;
144
145     my $schema  = Koha::Database->new->schema;
146     my @columns = $schema->source($self->_type)->columns;
147
148     for my $column( @columns ) {
149         my $value = $self->$column;
150         if ( defined $value ) {
151             $value =~ s/^\s*|\s*$//g;
152             $self->$column($value);
153         }
154     }
155     return $self;
156 }
157
158 =head3 plain_text_password
159
160 $patron->plain_text_password( $password );
161
162 stores a copy of the unencrypted password in the object
163 for use in code before encrypting for db
164
165 =cut
166
167 sub plain_text_password {
168     my ( $self, $password ) = @_;
169     if ( $password ) {
170         $self->{_plain_text_password} = $password;
171         return $self;
172     }
173     return $self->{_plain_text_password}
174         if $self->{_plain_text_password};
175
176     return;
177 }
178
179 =head3 store
180
181 Patron specific store method to cleanup record
182 and do other necessary things before saving
183 to db
184
185 =cut
186
187 sub store {
188     my ($self) = @_;
189
190     $self->_result->result_source->schema->txn_do(
191         sub {
192             if (
193                 C4::Context->preference("autoMemberNum")
194                 and ( not defined $self->cardnumber
195                     or $self->cardnumber eq '' )
196               )
197             {
198                 # Warning: The caller is responsible for locking the members table in write
199                 # mode, to avoid database corruption.
200                 # We are in a transaction but the table is not locked
201                 $self->fixup_cardnumber;
202             }
203
204             unless( $self->category->in_storage ) {
205                 Koha::Exceptions::Object::FKConstraint->throw(
206                     broken_fk => 'categorycode',
207                     value     => $self->categorycode,
208                 );
209             }
210
211             $self->trim_whitespaces;
212
213             my $new_cardnumber = $self->cardnumber;
214             Koha::Plugins->call( 'patron_barcode_transform', \$new_cardnumber );
215             $self->cardnumber( $new_cardnumber );
216
217             # Set surname to uppercase if uppercasesurname is true
218             $self->surname( uc($self->surname) )
219                 if C4::Context->preference("uppercasesurnames");
220
221             $self->relationship(undef) # We do not want to store an empty string in this field
222               if defined $self->relationship
223                      and $self->relationship eq "";
224
225             unless ( $self->in_storage ) {    #AddMember
226
227                 # Generate a valid userid/login if needed
228                 $self->generate_userid unless $self->userid;
229                 Koha::Exceptions::Patron::InvalidUserid->throw( userid => $self->userid )
230                     unless $self->has_valid_userid;
231
232                 # Add expiration date if it isn't already there
233                 unless ( $self->dateexpiry ) {
234                     $self->dateexpiry( $self->category->get_expiry_date );
235                 }
236
237                 # Add enrollment date if it isn't already there
238                 unless ( $self->dateenrolled ) {
239                     $self->dateenrolled(dt_from_string);
240                 }
241
242                 # Set the privacy depending on the patron's category
243                 my $default_privacy = $self->category->default_privacy || q{};
244                 $default_privacy =
245                     $default_privacy eq 'default' ? 1
246                   : $default_privacy eq 'never'   ? 2
247                   : $default_privacy eq 'forever' ? 0
248                   :                                                   undef;
249                 $self->privacy($default_privacy);
250
251                 # Call any check_password plugins if password is passed
252                 if ( C4::Context->config("enable_plugins") && $self->password ) {
253                     my @plugins = Koha::Plugins->new()->GetPlugins({
254                         method => 'check_password',
255                     });
256                     foreach my $plugin ( @plugins ) {
257                         # This plugin hook will also be used by a plugin for the Norwegian national
258                         # patron database. This is why we need to pass both the password and the
259                         # borrowernumber to the plugin.
260                         my $ret = $plugin->check_password(
261                             {
262                                 password       => $self->password,
263                                 borrowernumber => $self->borrowernumber
264                             }
265                         );
266                         if ( $ret->{'error'} == 1 ) {
267                             Koha::Exceptions::Password::Plugin->throw();
268                         }
269                     }
270                 }
271
272                 # Make a copy of the plain text password for later use
273                 $self->plain_text_password( $self->password );
274
275                 $self->password_expiration_date( $self->password
276                     ? $self->category->get_password_expiry_date || undef
277                     : undef );
278                 # Create a disabled account if no password provided
279                 $self->password( $self->password
280                     ? Koha::AuthUtils::hash_password( $self->password )
281                     : '!' );
282
283                 $self->borrowernumber(undef);
284
285                 $self = $self->SUPER::store;
286
287                 $self->add_enrolment_fee_if_needed(0);
288
289                 logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
290                   if C4::Context->preference("BorrowersLog");
291             }
292             else {    #ModMember
293
294                 my $self_from_storage = $self->get_from_storage;
295
296                 # Do not accept invalid userid here
297                 $self->generate_userid unless $self->userid;
298                 Koha::Exceptions::Patron::InvalidUserid->throw( userid => $self->userid )
299                       unless $self->has_valid_userid;
300
301                 # If a borrower has set their privacy to never we should immediately anonymize
302                 # their checkouts
303                 if( $self->privacy() == 2 && $self_from_storage->privacy() != 2 ){
304                     try{
305                         $self->old_checkouts->anonymize;
306                     }
307                     catch {
308                         Koha::Exceptions::Patron::FailedAnonymizing->throw(
309                             error => @_
310                         );
311                     };
312                 }
313
314                 # Password must be updated using $self->set_password
315                 $self->password($self_from_storage->password);
316
317                 if ( $self->category->categorycode ne
318                     $self_from_storage->category->categorycode )
319                 {
320                     # Add enrolement fee on category change if required
321                     $self->add_enrolment_fee_if_needed(1)
322                       if C4::Context->preference('FeeOnChangePatronCategory');
323
324                     # Clean up guarantors on category change if required
325                     $self->guarantor_relationships->delete
326                       unless ( $self->category->can_be_guarantee );
327
328                 }
329
330                 # Actionlogs
331                 if ( C4::Context->preference("BorrowersLog") ) {
332                     my $info;
333                     my $from_storage = $self_from_storage->unblessed;
334                     my $from_object  = $self->unblessed;
335                     my @skip_fields  = (qw/lastseen updated_on/);
336                     for my $key ( keys %{$from_storage} ) {
337                         next if any { /$key/ } @skip_fields;
338                         if (
339                             (
340                                   !defined( $from_storage->{$key} )
341                                 && defined( $from_object->{$key} )
342                             )
343                             || ( defined( $from_storage->{$key} )
344                                 && !defined( $from_object->{$key} ) )
345                             || (
346                                    defined( $from_storage->{$key} )
347                                 && defined( $from_object->{$key} )
348                                 && ( $from_storage->{$key} ne
349                                     $from_object->{$key} )
350                             )
351                           )
352                         {
353                             $info->{$key} = {
354                                 before => $from_storage->{$key},
355                                 after  => $from_object->{$key}
356                             };
357                         }
358                     }
359
360                     if ( defined($info) ) {
361                         logaction(
362                             "MEMBERS",
363                             "MODIFY",
364                             $self->borrowernumber,
365                             to_json(
366                                 $info,
367                                 { utf8 => 1, pretty => 1, canonical => 1 }
368                             )
369                         );
370                     }
371                 }
372
373                 # Final store
374                 $self = $self->SUPER::store;
375             }
376         }
377     );
378     return $self;
379 }
380
381 =head3 delete
382
383 $patron->delete
384
385 Delete patron's holds, lists and finally the patron.
386
387 Lists owned by the borrower are deleted or ownership is transferred depending on the
388 ListOwnershipUponPatronDeletion pref, but entries from the borrower to other lists are kept.
389
390 =cut
391
392 sub delete {
393     my ($self) = @_;
394
395     my $anonymous_patron = C4::Context->preference("AnonymousPatron");
396     Koha::Exceptions::Patron::FailedDeleteAnonymousPatron->throw() if $anonymous_patron && $self->id eq $anonymous_patron;
397
398     $self->_result->result_source->schema->txn_do(
399         sub {
400             # Cancel Patron's holds
401             my $holds = $self->holds;
402             while( my $hold = $holds->next ){
403                 $hold->cancel;
404             }
405
406             # Handle lists (virtualshelves)
407             $self->virtualshelves->disown_or_delete;
408
409             # We cannot have a FK on borrower_modifications.borrowernumber, the table is also used
410             # for patron selfreg
411             $_->delete for Koha::Patron::Modifications->search( { borrowernumber => $self->borrowernumber } )->as_list;
412
413             $self->SUPER::delete;
414
415             logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
416         }
417     );
418     return $self;
419 }
420
421 =head3 category
422
423 my $patron_category = $patron->category
424
425 Return the patron category for this patron
426
427 =cut
428
429 sub category {
430     my ( $self ) = @_;
431     return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
432 }
433
434 =head3 image
435
436 =cut
437
438 sub image {
439     my ( $self ) = @_;
440
441     return Koha::Patron::Images->find( $self->borrowernumber );
442 }
443
444 =head3 library
445
446 Returns a Koha::Library object representing the patron's home library.
447
448 =cut
449
450 sub library {
451     my ( $self ) = @_;
452     return Koha::Library->_new_from_dbic($self->_result->branchcode);
453 }
454
455 =head3 sms_provider
456
457 Returns a Koha::SMS::Provider object representing the patron's SMS provider.
458
459 =cut
460
461 sub sms_provider {
462     my ( $self ) = @_;
463     my $sms_provider_rs = $self->_result->sms_provider;
464     return unless $sms_provider_rs;
465     return Koha::SMS::Provider->_new_from_dbic($sms_provider_rs);
466 }
467
468 =head3 guarantor_relationships
469
470 Returns Koha::Patron::Relationships object for this patron's guarantors
471
472 Returns the set of relationships for the patrons that are guarantors for this patron.
473
474 Note that a guarantor should exist as a patron in Koha; it was not possible
475 to add them without a guarantor_id in the interface for some time. Bug 30472
476 restricts it on db level.
477
478 =cut
479
480 sub guarantor_relationships {
481     my ($self) = @_;
482
483     return Koha::Patron::Relationships->search( { guarantee_id => $self->id } );
484 }
485
486 =head3 guarantee_relationships
487
488 Returns Koha::Patron::Relationships object for this patron's guarantors
489
490 Returns the set of relationships for the patrons that are guarantees for this patron.
491
492 The method returns Koha::Patron::Relationship objects for the sake
493 of consistency with the guantors method.
494 A guarantee by definition must exist as a patron in Koha.
495
496 =cut
497
498 sub guarantee_relationships {
499     my ($self) = @_;
500
501     return Koha::Patron::Relationships->search(
502         { guarantor_id => $self->id },
503         {
504             prefetch => 'guarantee',
505             order_by => { -asc => [ 'guarantee.surname', 'guarantee.firstname' ] },
506         }
507     );
508 }
509
510 =head3 relationships_debt
511
512 Returns the amount owed by the patron's guarantors *and* the other guarantees of those guarantors
513
514 =cut
515
516 sub relationships_debt {
517     my ($self, $params) = @_;
518
519     my $include_guarantors  = $params->{include_guarantors};
520     my $only_this_guarantor = $params->{only_this_guarantor};
521     my $include_this_patron = $params->{include_this_patron};
522
523     my @guarantors;
524     if ( $only_this_guarantor ) {
525         @guarantors = $self->guarantee_relationships->count ? ( $self ) : ();
526         Koha::Exceptions::BadParameter->throw( { parameter => 'only_this_guarantor' } ) unless @guarantors;
527     } elsif ( $self->guarantor_relationships->count ) {
528         # I am a guarantee, just get all my guarantors
529         @guarantors = $self->guarantor_relationships->guarantors->as_list;
530     } else {
531         # I am a guarantor, I need to get all the guarantors of all my guarantees
532         @guarantors = map { $_->guarantor_relationships->guarantors->as_list } $self->guarantee_relationships->guarantees->as_list;
533     }
534
535     my $non_issues_charges = 0;
536     my $seen = $include_this_patron ? {} : { $self->id => 1 }; # For tracking members already added to the total
537     foreach my $guarantor (@guarantors) {
538         $non_issues_charges += $guarantor->account->non_issues_charges if $include_guarantors && !$seen->{ $guarantor->id };
539
540         # We've added what the guarantor owes, not added in that guarantor's guarantees as well
541         my @guarantees = map { $_->guarantee } $guarantor->guarantee_relationships->as_list;
542         my $guarantees_non_issues_charges = 0;
543         foreach my $guarantee (@guarantees) {
544             next if $seen->{ $guarantee->id };
545             $guarantees_non_issues_charges += $guarantee->account->non_issues_charges;
546             # Mark this guarantee as seen so we don't double count a guarantee linked to multiple guarantors
547             $seen->{ $guarantee->id } = 1;
548         }
549
550         $non_issues_charges += $guarantees_non_issues_charges;
551         $seen->{ $guarantor->id } = 1;
552     }
553
554     return $non_issues_charges;
555 }
556
557 =head3 housebound_profile
558
559 Returns the HouseboundProfile associated with this patron.
560
561 =cut
562
563 sub housebound_profile {
564     my ( $self ) = @_;
565     my $profile = $self->_result->housebound_profile;
566     return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
567         if ( $profile );
568     return;
569 }
570
571 =head3 housebound_role
572
573 Returns the HouseboundRole associated with this patron.
574
575 =cut
576
577 sub housebound_role {
578     my ( $self ) = @_;
579
580     my $role = $self->_result->housebound_role;
581     return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
582     return;
583 }
584
585 =head3 siblings
586
587 Returns the siblings of this patron.
588
589 =cut
590
591 sub siblings {
592     my ($self) = @_;
593
594     my @guarantors = $self->guarantor_relationships()->guarantors()->as_list;
595
596     return unless @guarantors;
597
598     my @siblings =
599       map { $_->guarantee_relationships()->guarantees()->as_list } @guarantors;
600
601     return unless @siblings;
602
603     my %seen;
604     @siblings =
605       grep { !$seen{ $_->id }++ && ( $_->id != $self->id ) } @siblings;
606
607     return Koha::Patrons->search( { borrowernumber => { -in => [ map { $_->id } @siblings ] } } );
608 }
609
610 =head3 merge_with
611
612     my $patron = Koha::Patrons->find($id);
613     $patron->merge_with( \@patron_ids );
614
615     This subroutine merges a list of patrons into the patron record. This is accomplished by finding
616     all related patron ids for the patrons to be merged in other tables and changing the ids to be that
617     of the keeper patron.
618
619 =cut
620
621 sub merge_with {
622     my ( $self, $patron_ids ) = @_;
623
624     my $anonymous_patron = C4::Context->preference("AnonymousPatron");
625     return if $anonymous_patron && $self->id eq $anonymous_patron;
626
627     my @patron_ids = @{ $patron_ids };
628
629     # Ensure the keeper isn't in the list of patrons to merge
630     @patron_ids = grep { $_ ne $self->id } @patron_ids;
631
632     my $schema = Koha::Database->new()->schema();
633
634     my $results;
635
636     $self->_result->result_source->schema->txn_do( sub {
637         foreach my $patron_id (@patron_ids) {
638
639             next if $patron_id eq $anonymous_patron;
640
641             my $patron = Koha::Patrons->find( $patron_id );
642
643             next unless $patron;
644
645             # Unbless for safety, the patron will end up being deleted
646             $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
647
648             my $attributes = $patron->extended_attributes;
649             my $new_attributes = [
650                 map { { code => $_->code, attribute => $_->attribute } }
651                     $attributes->as_list
652             ];
653             $attributes->delete; # We need to delete before trying to merge them to prevent exception on unique and repeatable
654             for my $attribute ( @$new_attributes ) {
655                 try {
656                     $self->add_extended_attribute($attribute);
657                 } catch {
658                     # Don't block the merge if there is a non-repeatable attribute that cannot be added to the current patron.
659                     unless ( $_->isa('Koha::Exceptions::Patron::Attribute::NonRepeatable') ) {
660                         $_->rethrow;
661                     }
662                 };
663             }
664
665             while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
666                 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
667                 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
668                 $rs->update({ $field => $self->id });
669                 if ( $r eq 'BorrowerDebarment' ) {
670                     Koha::Patron::Debarments::UpdateBorrowerDebarmentFlags($self->id);
671                 }
672             }
673
674             $patron->move_to_deleted();
675             $patron->delete();
676         }
677     });
678
679     return $results;
680 }
681
682
683
684 =head3 wants_check_for_previous_checkout
685
686     $wants_check = $patron->wants_check_for_previous_checkout;
687
688 Return 1 if Koha needs to perform PrevIssue checking, else 0.
689
690 =cut
691
692 sub wants_check_for_previous_checkout {
693     my ( $self ) = @_;
694     my $syspref = C4::Context->preference("checkPrevCheckout");
695
696     # Simple cases
697     ## Hard syspref trumps all
698     return 1 if ($syspref eq 'hardyes');
699     return 0 if ($syspref eq 'hardno');
700     ## Now, patron pref trumps all
701     return 1 if ($self->checkprevcheckout eq 'yes');
702     return 0 if ($self->checkprevcheckout eq 'no');
703
704     # More complex: patron inherits -> determine category preference
705     my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
706     return 1 if ($checkPrevCheckoutByCat eq 'yes');
707     return 0 if ($checkPrevCheckoutByCat eq 'no');
708
709     # Finally: category preference is inherit, default to 0
710     if ($syspref eq 'softyes') {
711         return 1;
712     } else {
713         return 0;
714     }
715 }
716
717 =head3 do_check_for_previous_checkout
718
719     $do_check = $patron->do_check_for_previous_checkout($item);
720
721 Return 1 if the bib associated with $ITEM has previously been checked out to
722 $PATRON, 0 otherwise.
723
724 =cut
725
726 sub do_check_for_previous_checkout {
727     my ( $self, $item ) = @_;
728
729     my @item_nos;
730     my $biblio = Koha::Biblios->find( $item->{biblionumber} );
731     if ( $biblio->is_serial ) {
732         push @item_nos, $item->{itemnumber};
733     } else {
734         # Get all itemnumbers for given bibliographic record.
735         @item_nos = $biblio->items->get_column( 'itemnumber' );
736     }
737
738     # Create (old)issues search criteria
739     my $criteria = {
740         borrowernumber => $self->borrowernumber,
741         itemnumber => \@item_nos,
742     };
743
744     my $delay = C4::Context->preference('CheckPrevCheckoutDelay') || 0;
745     if ($delay) {
746         my $dtf = Koha::Database->new->schema->storage->datetime_parser;
747         my $newer_than = dt_from_string()->subtract( days => $delay );
748         $criteria->{'returndate'} = { '>'   =>  $dtf->format_datetime($newer_than), };
749     }
750
751     # Check current issues table
752     my $issues = Koha::Checkouts->search($criteria);
753     return 1 if $issues->count; # 0 || N
754
755     # Check old issues table
756     my $old_issues = Koha::Old::Checkouts->search($criteria);
757     return $old_issues->count;  # 0 || N
758 }
759
760 =head3 is_debarred
761
762 my $debarment_expiration = $patron->is_debarred;
763
764 Returns the date a patron debarment will expire, or undef if the patron is not
765 debarred
766
767 =cut
768
769 sub is_debarred {
770     my ($self) = @_;
771
772     return unless $self->debarred;
773     return $self->debarred
774       if $self->debarred =~ '^9999'
775       or dt_from_string( $self->debarred ) > dt_from_string;
776     return;
777 }
778
779 =head3 is_expired
780
781 my $is_expired = $patron->is_expired;
782
783 Returns 1 if the patron is expired or 0;
784
785 =cut
786
787 sub is_expired {
788     my ($self) = @_;
789     return 0 unless $self->dateexpiry;
790     return 0 if $self->dateexpiry =~ '^9999';
791     return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
792     return 0;
793 }
794
795 =head3 password_expired
796
797 my $password_expired = $patron->password_expired;
798
799 Returns 1 if the patron's password is expired or 0;
800
801 =cut
802
803 sub password_expired {
804     my ($self) = @_;
805     return 0 unless $self->password_expiration_date;
806     return 1 if dt_from_string( $self->password_expiration_date ) <= dt_from_string->truncate( to => 'day' );
807     return 0;
808 }
809
810 =head3 is_going_to_expire
811
812 my $is_going_to_expire = $patron->is_going_to_expire;
813
814 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
815
816 =cut
817
818 sub is_going_to_expire {
819     my ($self) = @_;
820
821     my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
822
823     return 0 unless $delay;
824     return 0 unless $self->dateexpiry;
825     return 0 if $self->dateexpiry =~ '^9999';
826     return 1 if dt_from_string( $self->dateexpiry, undef, 'floating' )->subtract( days => $delay ) < dt_from_string(undef, undef, 'floating')->truncate( to => 'day' );
827     return 0;
828 }
829
830 =head3 set_password
831
832     $patron->set_password({ password => $plain_text_password [, skip_validation => 1 ] });
833
834 Set the patron's password.
835
836 =head4 Exceptions
837
838 The passed string is validated against the current password enforcement policy.
839 Validation can be skipped by passing the I<skip_validation> parameter.
840
841 Exceptions are thrown if the password is not good enough.
842
843 =over 4
844
845 =item Koha::Exceptions::Password::TooShort
846
847 =item Koha::Exceptions::Password::WhitespaceCharacters
848
849 =item Koha::Exceptions::Password::TooWeak
850
851 =item Koha::Exceptions::Password::Plugin (if a "check password" plugin is enabled)
852
853 =back
854
855 =cut
856
857 sub set_password {
858     my ( $self, $args ) = @_;
859
860     my $password = $args->{password};
861
862     unless ( $args->{skip_validation} ) {
863         my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password, $self->category );
864
865         if ( !$is_valid ) {
866             if ( $error eq 'too_short' ) {
867                 my $min_length = $self->category->effective_min_password_length;
868                 $min_length = 3 if not $min_length or $min_length < 3;
869
870                 my $password_length = length($password);
871                 Koha::Exceptions::Password::TooShort->throw(
872                     length => $password_length, min_length => $min_length );
873             }
874             elsif ( $error eq 'has_whitespaces' ) {
875                 Koha::Exceptions::Password::WhitespaceCharacters->throw();
876             }
877             elsif ( $error eq 'too_weak' ) {
878                 Koha::Exceptions::Password::TooWeak->throw();
879             }
880         }
881     }
882
883     if ( C4::Context->config("enable_plugins") ) {
884         # Call any check_password plugins
885         my @plugins = Koha::Plugins->new()->GetPlugins({
886             method => 'check_password',
887         });
888         foreach my $plugin ( @plugins ) {
889             # This plugin hook will also be used by a plugin for the Norwegian national
890             # patron database. This is why we need to pass both the password and the
891             # borrowernumber to the plugin.
892             my $ret = $plugin->check_password(
893                 {
894                     password       => $password,
895                     borrowernumber => $self->borrowernumber
896                 }
897             );
898             # This plugin hook will also be used by a plugin for the Norwegian national
899             # patron database. This is why we need to call the actual plugins and then
900             # check skip_validation afterwards.
901             if ( $ret->{'error'} == 1 && !$args->{skip_validation} ) {
902                 Koha::Exceptions::Password::Plugin->throw();
903             }
904         }
905     }
906
907     if ( C4::Context->preference('NotifyPasswordChange') ) {
908         my $self_from_storage = $self->get_from_storage;
909         if ( !C4::Auth::checkpw_hash( $password, $self_from_storage->password ) ) {
910             my $emailaddr = $self_from_storage->notice_email_address;
911
912             # if we manage to find a valid email address, send notice
913             if ($emailaddr) {
914                 my $letter = C4::Letters::GetPreparedLetter(
915                     module      => 'members',
916                     letter_code => 'PASSWORD_CHANGE',
917                     branchcode  => $self_from_storage->branchcode,
918                     ,
919                     lang   => $self_from_storage->lang || 'default',
920                     tables => {
921                         'branches'  => $self_from_storage->branchcode,
922                         'borrowers' => $self_from_storage->borrowernumber,
923                     },
924                     want_librarian => 1,
925                 ) or return;
926
927                 my $message_id = C4::Letters::EnqueueLetter(
928                     {
929                         letter                 => $letter,
930                         borrowernumber         => $self_from_storage->id,
931                         to_address             => $emailaddr,
932                         message_transport_type => 'email'
933                     }
934                 );
935                 C4::Letters::SendQueuedMessages( { message_id => $message_id } );
936             }
937         }
938     }
939
940     my $digest = Koha::AuthUtils::hash_password($password);
941
942     $self->password_expiration_date( $self->category->get_password_expiry_date || undef );
943
944     # We do not want to call $self->store and retrieve password from DB
945     $self->password($digest);
946     $self->login_attempts(0);
947     $self->SUPER::store;
948
949     logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" )
950         if C4::Context->preference("BorrowersLog");
951
952     return $self;
953 }
954
955
956 =head3 renew_account
957
958 my $new_expiry_date = $patron->renew_account
959
960 Extending the subscription to the expiry date.
961
962 =cut
963
964 sub renew_account {
965     my ($self) = @_;
966     my $date;
967     if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
968         $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
969     } else {
970         $date =
971             C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
972             ? dt_from_string( $self->dateexpiry )
973             : dt_from_string;
974     }
975     my $expiry_date = $self->category->get_expiry_date($date);
976
977     $self->dateexpiry($expiry_date);
978     $self->date_renewed( dt_from_string() );
979     $self->store();
980
981     $self->add_enrolment_fee_if_needed(1);
982
983     logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
984     return dt_from_string( $expiry_date )->truncate( to => 'day' );
985 }
986
987 =head3 has_overdues
988
989 my $has_overdues = $patron->has_overdues;
990
991 Returns the number of patron's overdues
992
993 =cut
994
995 sub has_overdues {
996     my ($self) = @_;
997     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
998     return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
999 }
1000
1001 =head3 track_login
1002
1003     $patron->track_login;
1004     $patron->track_login({ force => 1 });
1005
1006     Tracks a (successful) login attempt.
1007     The preference TrackLastPatronActivity must be enabled. Or you
1008     should pass the force parameter.
1009
1010 =cut
1011
1012 sub track_login {
1013     my ( $self, $params ) = @_;
1014     return if
1015         !$params->{force} &&
1016         !C4::Context->preference('TrackLastPatronActivity');
1017     $self->lastseen( dt_from_string() )->store;
1018 }
1019
1020 =head3 move_to_deleted
1021
1022 my $is_moved = $patron->move_to_deleted;
1023
1024 Move a patron to the deletedborrowers table.
1025 This can be done before deleting a patron, to make sure the data are not completely deleted.
1026
1027 =cut
1028
1029 sub move_to_deleted {
1030     my ($self) = @_;
1031     my $patron_infos = $self->unblessed;
1032     delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
1033     return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
1034 }
1035
1036 =head3 can_request_article
1037
1038     if ( $patron->can_request_article( $library->id ) ) { ... }
1039
1040 Returns true if the patron can request articles. As limits apply for the patron
1041 on the same day, those completed the same day are considered as current.
1042
1043 A I<library_id> can be passed as parameter, falling back to userenv if absent.
1044
1045 =cut
1046
1047 sub can_request_article {
1048     my ($self, $library_id) = @_;
1049
1050     $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1051
1052     my $rule = Koha::CirculationRules->get_effective_rule(
1053         {
1054             branchcode   => $library_id,
1055             categorycode => $self->categorycode,
1056             rule_name    => 'open_article_requests_limit'
1057         }
1058     );
1059
1060     my $limit = ($rule) ? $rule->rule_value : undef;
1061
1062     return 1 unless defined $limit;
1063
1064     my $count = Koha::ArticleRequests->search(
1065         [   { borrowernumber => $self->borrowernumber, status => [ 'REQUESTED', 'PENDING', 'PROCESSING' ] },
1066             { borrowernumber => $self->borrowernumber, status => 'COMPLETED', updated_on => { '>=' => \'CAST(NOW() AS DATE)' } },
1067         ]
1068     )->count;
1069     return $count < $limit ? 1 : 0;
1070 }
1071
1072 =head3 article_request_fee
1073
1074     my $fee = $patron->article_request_fee(
1075         {
1076           [ library_id => $library->id, ]
1077         }
1078     );
1079
1080 Returns the fee to be charged to the patron when it places an article request.
1081
1082 A I<library_id> can be passed as parameter, falling back to userenv if absent.
1083
1084 =cut
1085
1086 sub article_request_fee {
1087     my ($self, $params) = @_;
1088
1089     my $library_id = $params->{library_id};
1090
1091     $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1092
1093     my $rule = Koha::CirculationRules->get_effective_rule(
1094         {
1095             branchcode   => $library_id,
1096             categorycode => $self->categorycode,
1097             rule_name    => 'article_request_fee'
1098         }
1099     );
1100
1101     my $fee = ($rule) ? $rule->rule_value + 0 : 0;
1102
1103     return $fee;
1104 }
1105
1106 =head3 add_article_request_fee_if_needed
1107
1108     my $fee = $patron->add_article_request_fee_if_needed(
1109         {
1110           [ item_id    => $item->id,
1111             library_id => $library->id, ]
1112         }
1113     );
1114
1115 If an article request fee needs to be charged, it adds a debit to the patron's
1116 account.
1117
1118 Returns the fee line.
1119
1120 A I<library_id> can be passed as parameter, falling back to userenv if absent.
1121
1122 =cut
1123
1124 sub add_article_request_fee_if_needed {
1125     my ($self, $params) = @_;
1126
1127     my $library_id = $params->{library_id};
1128     my $item_id    = $params->{item_id};
1129
1130     $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1131
1132     my $amount = $self->article_request_fee(
1133         {
1134             library_id => $library_id,
1135         }
1136     );
1137
1138     my $debit_line;
1139
1140     if ( $amount > 0 ) {
1141         $debit_line = $self->account->add_debit(
1142             {
1143                 amount     => $amount,
1144                 user_id    => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
1145                 interface  => C4::Context->interface,
1146                 library_id => $library_id,
1147                 type       => 'ARTICLE_REQUEST',
1148                 item_id    => $item_id,
1149             }
1150         );
1151     }
1152
1153     return $debit_line;
1154 }
1155
1156 =head3 article_requests
1157
1158     my $article_requests = $patron->article_requests;
1159
1160 Returns the patron article requests.
1161
1162 =cut
1163
1164 sub article_requests {
1165     my ($self) = @_;
1166
1167     return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
1168 }
1169
1170 =head3 add_enrolment_fee_if_needed
1171
1172 my $enrolment_fee = $patron->add_enrolment_fee_if_needed($renewal);
1173
1174 Add enrolment fee for a patron if needed.
1175
1176 $renewal - boolean denoting whether this is an account renewal or not
1177
1178 =cut
1179
1180 sub add_enrolment_fee_if_needed {
1181     my ($self, $renewal) = @_;
1182     my $enrolment_fee = $self->category->enrolmentfee;
1183     if ( $enrolment_fee && $enrolment_fee > 0 ) {
1184         my $type = $renewal ? 'ACCOUNT_RENEW' : 'ACCOUNT';
1185         $self->account->add_debit(
1186             {
1187                 amount     => $enrolment_fee,
1188                 user_id    => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
1189                 interface  => C4::Context->interface,
1190                 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
1191                 type       => $type
1192             }
1193         );
1194     }
1195     return $enrolment_fee || 0;
1196 }
1197
1198 =head3 checkouts
1199
1200 my $checkouts = $patron->checkouts
1201
1202 =cut
1203
1204 sub checkouts {
1205     my ($self) = @_;
1206     my $checkouts = $self->_result->issues;
1207     return Koha::Checkouts->_new_from_dbic( $checkouts );
1208 }
1209
1210 =head3 pending_checkouts
1211
1212 my $pending_checkouts = $patron->pending_checkouts
1213
1214 This method will return the same as $self->checkouts, but with a prefetch on
1215 items, biblio and biblioitems.
1216
1217 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
1218
1219 It should not be used directly, prefer to access fields you need instead of
1220 retrieving all these fields in one go.
1221
1222 =cut
1223
1224 sub pending_checkouts {
1225     my( $self ) = @_;
1226     my $checkouts = $self->_result->issues->search(
1227         {},
1228         {
1229             order_by => [
1230                 { -desc => 'me.timestamp' },
1231                 { -desc => 'issuedate' },
1232                 { -desc => 'issue_id' }, # Sort by issue_id should be enough
1233             ],
1234             prefetch => { item => { biblio => 'biblioitems' } },
1235         }
1236     );
1237     return Koha::Checkouts->_new_from_dbic( $checkouts );
1238 }
1239
1240 =head3 old_checkouts
1241
1242 my $old_checkouts = $patron->old_checkouts
1243
1244 =cut
1245
1246 sub old_checkouts {
1247     my ($self) = @_;
1248     my $old_checkouts = $self->_result->old_issues;
1249     return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
1250 }
1251
1252 =head3 overdues
1253
1254 my $overdue_items = $patron->overdues
1255
1256 Return the overdue items
1257
1258 =cut
1259
1260 sub overdues {
1261     my ($self) = @_;
1262     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
1263     return $self->checkouts->search(
1264         {
1265             'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
1266         },
1267         {
1268             prefetch => { item => { biblio => 'biblioitems' } },
1269         }
1270     );
1271 }
1272
1273
1274 =head3 restrictions
1275
1276   my $restrictions = $patron->restrictions;
1277
1278 Returns the patron restrictions.
1279
1280 =cut
1281
1282 sub restrictions {
1283     my ($self) = @_;
1284     my $restrictions_rs = $self->_result->restrictions;
1285     return Koha::Patron::Restrictions->_new_from_dbic($restrictions_rs);
1286 }
1287
1288 =head3 get_routing_lists
1289
1290 my $routinglists = $patron->get_routing_lists
1291
1292 Returns the routing lists a patron is subscribed to.
1293
1294 =cut
1295
1296 sub get_routing_lists {
1297     my ($self) = @_;
1298     my $routing_list_rs = $self->_result->subscriptionroutinglists;
1299     return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
1300 }
1301
1302 =head3 get_age
1303
1304     my $age = $patron->get_age
1305
1306 Return the age of the patron
1307
1308 =cut
1309
1310 sub get_age {
1311     my ($self)    = @_;
1312
1313     return unless $self->dateofbirth;
1314
1315     #Set timezone to floating to avoid any datetime math issues caused by DST
1316     my $date_of_birth = dt_from_string( $self->dateofbirth, undef, 'floating' );
1317     my $today         = dt_from_string(undef, undef, 'floating')->truncate( to => 'day' );
1318
1319     return $today->subtract_datetime( $date_of_birth )->years;
1320 }
1321
1322 =head3 is_valid_age
1323
1324 my $is_valid = $patron->is_valid_age
1325
1326 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
1327
1328 =cut
1329
1330 sub is_valid_age {
1331     my ($self) = @_;
1332     my $age = $self->get_age;
1333
1334     my $patroncategory = $self->category;
1335     my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
1336
1337     return (defined($age) && (($high && ($age > $high)) or ($low && ($age < $low)))) ? 0 : 1;
1338 }
1339
1340 =head3 account
1341
1342 my $account = $patron->account
1343
1344 =cut
1345
1346 sub account {
1347     my ($self) = @_;
1348     return Koha::Account->new( { patron_id => $self->borrowernumber } );
1349 }
1350
1351 =head3 holds
1352
1353 my $holds = $patron->holds
1354
1355 Return all the holds placed by this patron
1356
1357 =cut
1358
1359 sub holds {
1360     my ($self) = @_;
1361     my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1362     return Koha::Holds->_new_from_dbic($holds_rs);
1363 }
1364
1365 =head3 old_holds
1366
1367 my $old_holds = $patron->old_holds
1368
1369 Return all the historical holds for this patron
1370
1371 =cut
1372
1373 sub old_holds {
1374     my ($self) = @_;
1375     my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1376     return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1377 }
1378
1379 =head3 curbside_pickups
1380
1381 my $curbside_pickups = $patron->curbside_pickups;
1382
1383 Return all the curbside pickups for this patron
1384
1385 =cut
1386
1387 sub curbside_pickups {
1388     my ($self) = @_;
1389     my $curbside_pickups_rs = $self->_result->curbside_pickups_borrowernumbers->search;
1390     return Koha::CurbsidePickups->_new_from_dbic($curbside_pickups_rs);
1391 }
1392
1393 =head3 return_claims
1394
1395 my $return_claims = $patron->return_claims
1396
1397 =cut
1398
1399 sub return_claims {
1400     my ($self) = @_;
1401     my $return_claims = $self->_result->return_claims_borrowernumbers;
1402     return Koha::Checkouts::ReturnClaims->_new_from_dbic( $return_claims );
1403 }
1404
1405 =head3 notice_email_address
1406
1407   my $email = $patron->notice_email_address;
1408
1409 Return the email address of patron used for notices.
1410 Returns the empty string if no email address.
1411
1412 =cut
1413
1414 sub notice_email_address{
1415     my ( $self ) = @_;
1416
1417     my $which_address = C4::Context->preference("EmailFieldPrimary");
1418     # if syspref is set to 'first valid' (value == OFF), look up email address
1419     if ( $which_address eq 'OFF' ) {
1420         return $self->first_valid_email_address;
1421     }
1422
1423     return $self->$which_address || '';
1424 }
1425
1426 =head3 first_valid_email_address
1427
1428 my $first_valid_email_address = $patron->first_valid_email_address
1429
1430 Return the first valid email address for a patron.
1431 For now, the order  is defined as email, emailpro, B_email.
1432 Returns the empty string if the borrower has no email addresses.
1433
1434 =cut
1435
1436 sub first_valid_email_address {
1437     my ($self) = @_;
1438
1439     my $email = q{};
1440
1441     my @fields = split /\s*\|\s*/,
1442       C4::Context->preference('EmailFieldPrecedence');
1443     for my $field (@fields) {
1444         $email = $self->$field;
1445         last if ( $email ne q{} );
1446     }
1447
1448     return $email;
1449 }
1450
1451 =head3 get_club_enrollments
1452
1453 =cut
1454
1455 sub get_club_enrollments {
1456     my ( $self ) = @_;
1457
1458     return Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1459 }
1460
1461 =head3 get_enrollable_clubs
1462
1463 =cut
1464
1465 sub get_enrollable_clubs {
1466     my ( $self, $is_enrollable_from_opac ) = @_;
1467
1468     my $params;
1469     $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1470       if $is_enrollable_from_opac;
1471     $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1472
1473     $params->{borrower} = $self;
1474
1475     return Koha::Clubs->get_enrollable($params);
1476 }
1477
1478 =head3 account_locked
1479
1480 my $is_locked = $patron->account_locked
1481
1482 Return true if the patron has reached the maximum number of login attempts
1483 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1484 as an administrative lockout (independent of FailedLoginAttempts; see also
1485 Koha::Patron->lock).
1486 Otherwise return false.
1487 If the pref is not set (empty string, null or 0), the feature is considered as
1488 disabled.
1489
1490 =cut
1491
1492 sub account_locked {
1493     my ($self) = @_;
1494     my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1495     return 1 if $FailedLoginAttempts
1496           and $self->login_attempts
1497           and $self->login_attempts >= $FailedLoginAttempts;
1498     return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1499     return 0;
1500 }
1501
1502 =head3 can_see_patron_infos
1503
1504 my $can_see = $patron->can_see_patron_infos( $patron );
1505
1506 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1507
1508 =cut
1509
1510 sub can_see_patron_infos {
1511     my ( $self, $patron ) = @_;
1512     return unless $patron;
1513     return $self->can_see_patrons_from( $patron->branchcode );
1514 }
1515
1516 =head3 can_see_patrons_from
1517
1518 my $can_see = $patron->can_see_patrons_from( $branchcode );
1519
1520 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1521
1522 =cut
1523
1524 sub can_see_patrons_from {
1525     my ( $self, $branchcode ) = @_;
1526
1527     return $self->can_see_things_from(
1528         {
1529             branchcode => $branchcode,
1530             permission => 'borrowers',
1531             subpermission => 'view_borrower_infos_from_any_libraries',
1532         }
1533     );
1534 }
1535
1536 =head3 can_edit_items_from
1537
1538     my $can_edit = $patron->can_edit_items_from( $branchcode );
1539
1540 Return true if the I<Koha::Patron> can edit items from the given branchcode
1541
1542 =cut
1543
1544 sub can_edit_items_from {
1545     my ( $self, $branchcode ) = @_;
1546
1547     return 1 if C4::Context->IsSuperLibrarian();
1548
1549     my $userenv = C4::Context->userenv();
1550     if ( $userenv && C4::Context->preference('IndependentBranches') ) {
1551         return $userenv->{branch} eq $branchcode;
1552     }
1553
1554     return $self->can_see_things_from(
1555         {
1556             branchcode    => $branchcode,
1557             permission    => 'editcatalogue',
1558             subpermission => 'edit_any_item',
1559         }
1560     );
1561 }
1562
1563 =head3 libraries_where_can_edit_items
1564
1565     my $libraries = $patron->libraries_where_can_edit_items;
1566
1567 Return the list of branchcodes(!) of libraries the patron is allowed to items for.
1568 The branchcodes are arbitrarily returned sorted.
1569 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1570
1571 An empty array means no restriction, the user can edit any item.
1572
1573 =cut
1574
1575 sub libraries_where_can_edit_items {
1576     my ($self) = @_;
1577
1578     return $self->libraries_where_can_see_things(
1579         {
1580             permission    => 'editcatalogue',
1581             subpermission => 'edit_any_item',
1582             group_feature => 'ft_limit_item_editing',
1583         }
1584     );
1585 }
1586
1587 =head3 libraries_where_can_see_patrons
1588
1589 my $libraries = $patron->libraries_where_can_see_patrons;
1590
1591 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1592 The branchcodes are arbitrarily returned sorted.
1593 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1594
1595 An empty array means no restriction, the patron can see patron's infos from any libraries.
1596
1597 =cut
1598
1599 sub libraries_where_can_see_patrons {
1600     my ($self) = @_;
1601
1602     return $self->libraries_where_can_see_things(
1603         {
1604             permission    => 'borrowers',
1605             subpermission => 'view_borrower_infos_from_any_libraries',
1606             group_feature => 'ft_hide_patron_info',
1607         }
1608     );
1609 }
1610
1611 =head3 can_see_things_from
1612
1613 my $can_see = $patron->can_see_things_from( $branchcode );
1614
1615 Return true if the I<Koha::Patron> can perform some action on the given thing
1616
1617 =cut
1618
1619 sub can_see_things_from {
1620     my ( $self, $params ) = @_;
1621
1622     my $branchcode    = $params->{branchcode};
1623     my $permission    = $params->{permission};
1624     my $subpermission = $params->{subpermission};
1625
1626     return 1 if C4::Context->IsSuperLibrarian();
1627
1628     my $can = 0;
1629     if ( $self->branchcode eq $branchcode ) {
1630         $can = 1;
1631     } elsif ( $self->has_permission( { $permission => $subpermission } ) ) {
1632         $can = 1;
1633     } elsif ( my $library_groups = $self->library->library_groups ) {
1634         while ( my $library_group = $library_groups->next ) {
1635             if ( $library_group->parent->has_child( $branchcode ) ) {
1636                 $can = 1;
1637                 last;
1638             }
1639         }
1640     }
1641     return $can;
1642 }
1643
1644 =head3 can_log_into
1645
1646 my $can_log_into = $patron->can_log_into( $library );
1647
1648 Given a I<Koha::Library> object, it returns a boolean representing
1649 the fact the patron can log into a the library.
1650
1651 =cut
1652
1653 sub can_log_into {
1654     my ( $self, $library ) = @_;
1655
1656     my $can = 0;
1657
1658     if ( C4::Context->preference('IndependentBranches') ) {
1659         $can = 1
1660           if $self->is_superlibrarian
1661           or $self->branchcode eq $library->id;
1662     }
1663     else {
1664         # no restrictions
1665         $can = 1;
1666     }
1667
1668    return $can;
1669 }
1670
1671 =head3 libraries_where_can_see_things
1672
1673     my $libraries = $patron->libraries_where_can_see_things;
1674
1675 Returns a list of libraries where an aribitarary action is allowed to be taken by the logged in librarian
1676 against an object based on some branchcode related to the object ( patron branchcode, item homebranch, etc ).
1677
1678 We are supposing here that the object is related to the logged in librarian (use of C4::Context::only_my_library)
1679
1680 An empty array means no restriction, the thing can see thing's infos from any libraries.
1681
1682 =cut
1683
1684 sub libraries_where_can_see_things {
1685     my ( $self, $params ) = @_;
1686     my $permission    = $params->{permission};
1687     my $subpermission = $params->{subpermission};
1688     my $group_feature = $params->{group_feature};
1689
1690     my $userenv = C4::Context->userenv;
1691
1692     return () unless $userenv; # For tests, but userenv should be defined in tests...
1693
1694     my @restricted_branchcodes;
1695     if (C4::Context::only_my_library) {
1696         push @restricted_branchcodes, $self->branchcode;
1697     }
1698     else {
1699         unless (
1700             $self->has_permission(
1701                 { $permission => $subpermission }
1702             )
1703           )
1704         {
1705             my $library_groups = $self->library->library_groups({ $group_feature => 1 });
1706             if ( $library_groups->count )
1707             {
1708                 while ( my $library_group = $library_groups->next ) {
1709                     my $parent = $library_group->parent;
1710                     if ( $parent->has_child( $self->branchcode ) ) {
1711                         push @restricted_branchcodes, $parent->children->get_column('branchcode');
1712                     }
1713                 }
1714             }
1715
1716             @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1717         }
1718     }
1719
1720     @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1721     @restricted_branchcodes = uniq(@restricted_branchcodes);
1722     @restricted_branchcodes = sort(@restricted_branchcodes);
1723     return @restricted_branchcodes;
1724 }
1725
1726 =head3 has_permission
1727
1728 my $permission = $patron->has_permission($required);
1729
1730 See C4::Auth::haspermission for details of syntax for $required
1731
1732 =cut
1733
1734 sub has_permission {
1735     my ( $self, $flagsrequired ) = @_;
1736     return unless $self->userid;
1737     # TODO code from haspermission needs to be moved here!
1738     return C4::Auth::haspermission( $self->userid, $flagsrequired );
1739 }
1740
1741 =head3 is_superlibrarian
1742
1743   my $is_superlibrarian = $patron->is_superlibrarian;
1744
1745 Return true if the patron is a superlibrarian.
1746
1747 =cut
1748
1749 sub is_superlibrarian {
1750     my ($self) = @_;
1751     return $self->has_permission( { superlibrarian => 1 } ) ? 1 : 0;
1752 }
1753
1754 =head3 is_adult
1755
1756 my $is_adult = $patron->is_adult
1757
1758 Return true if the patron has a category with a type Adult (A) or Organization (I)
1759
1760 =cut
1761
1762 sub is_adult {
1763     my ( $self ) = @_;
1764     return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1765 }
1766
1767 =head3 is_child
1768
1769 my $is_child = $patron->is_child
1770
1771 Return true if the patron has a category with a type Child (C)
1772
1773 =cut
1774
1775 sub is_child {
1776     my( $self ) = @_;
1777     return $self->category->category_type eq 'C' ? 1 : 0;
1778 }
1779
1780 =head3 has_valid_userid
1781
1782 my $patron = Koha::Patrons->find(42);
1783 $patron->userid( $new_userid );
1784 my $has_a_valid_userid = $patron->has_valid_userid
1785
1786 my $patron = Koha::Patron->new( $params );
1787 my $has_a_valid_userid = $patron->has_valid_userid
1788
1789 Return true if the current userid of this patron is valid/unique, otherwise false.
1790
1791 Note that this should be done in $self->store instead and raise an exception if needed.
1792
1793 =cut
1794
1795 sub has_valid_userid {
1796     my ($self) = @_;
1797
1798     return 0 unless $self->userid;
1799
1800     return 0 if ( $self->userid eq C4::Context->config('user') );    # DB user
1801
1802     my $already_exists = Koha::Patrons->search(
1803         {
1804             userid => $self->userid,
1805             (
1806                 $self->in_storage
1807                 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1808                 : ()
1809             ),
1810         }
1811     )->count;
1812     return $already_exists ? 0 : 1;
1813 }
1814
1815 =head3 generate_userid
1816
1817     $patron->generate_userid;
1818
1819     If you do not have a plugin for generating a userid, we will call
1820     the internal method here that returns firstname.surname[.number],
1821     where number is an optional suffix to make the userid unique.
1822     (Its behavior has not been changed on bug 32426.)
1823
1824     If you have plugin(s), the first valid response will be used.
1825     A plugin is assumed to return a valid userid as suggestion, but not
1826     assumed to save it already.
1827     Does not fallback to internal (you could arrange for that in your plugin).
1828     Clears userid when there are no valid plugin responses.
1829
1830 =cut
1831
1832 sub generate_userid {
1833     my ( $self ) = @_;
1834     my @responses = Koha::Plugins->call(
1835         'patron_generate_userid', { patron => $self },
1836     );
1837     unless( @responses ) {
1838         # Empty list only possible when there are NO enabled plugins for this method.
1839         # In that case we provide internal response.
1840         return $self->_generate_userid_internal;
1841     }
1842     # If a plugin returned false value or invalid value, we do however not return
1843     # internal response. The plugins should deal with that themselves. So we prevent
1844     # unexpected/unwelcome internal codes for plugin failures.
1845     foreach my $response ( grep { $_ } @responses ) {
1846         $self->userid( $response );
1847         return $self if $self->has_valid_userid;
1848     }
1849     $self->userid(undef);
1850     return $self;
1851 }
1852
1853 sub _generate_userid_internal { # as we always did
1854     my ($self) = @_;
1855     my $offset = 0;
1856     my $firstname = $self->firstname // q{};
1857     my $surname = $self->surname // q{};
1858     #The script will "do" the following code and increment the $offset until the generated userid is unique
1859     do {
1860       $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1861       $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1862       my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1863       $userid = NFKD( $userid );
1864       $userid =~ s/\p{NonspacingMark}//g;
1865       $userid .= $offset unless $offset == 0;
1866       $self->userid( $userid );
1867       $offset++;
1868      } while (! $self->has_valid_userid );
1869
1870      return $self;
1871 }
1872
1873 =head3 add_extended_attribute
1874
1875 =cut
1876
1877 sub add_extended_attribute {
1878     my ($self, $attribute) = @_;
1879
1880     return Koha::Patron::Attribute->new(
1881         {
1882             %$attribute,
1883             ( borrowernumber => $self->borrowernumber ),
1884         }
1885     )->store;
1886
1887 }
1888
1889 =head3 extended_attributes
1890
1891 Return object of Koha::Patron::Attributes type with all attributes set for this patron
1892
1893 Or setter FIXME
1894
1895 =cut
1896
1897 sub extended_attributes {
1898     my ( $self, $attributes ) = @_;
1899     if ($attributes) {    # setter
1900         my $schema = $self->_result->result_source->schema;
1901         $schema->txn_do(
1902             sub {
1903                 # Remove the existing one
1904                 $self->extended_attributes->filter_by_branch_limitations->delete;
1905
1906                 # Insert the new ones
1907                 my $new_types = {};
1908                 for my $attribute (@$attributes) {
1909                     $self->add_extended_attribute($attribute);
1910                     $new_types->{$attribute->{code}} = 1;
1911                 }
1912
1913                 # Check globally mandatory types
1914                 my @required_attribute_types =
1915                     Koha::Patron::Attribute::Types->search(
1916                         {
1917                             mandatory => 1,
1918                             category_code => [ undef, $self->categorycode ],
1919                             'borrower_attribute_types_branches.b_branchcode' =>
1920                               undef,
1921                         },
1922                         { join => 'borrower_attribute_types_branches' }
1923                     )->get_column('code');
1924                 for my $type ( @required_attribute_types ) {
1925                     Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute->throw(
1926                         type => $type,
1927                     ) if !$new_types->{$type};
1928                 }
1929             }
1930         );
1931     }
1932
1933     my $rs = $self->_result->borrower_attributes;
1934     # We call search to use the filters in Koha::Patron::Attributes->search
1935     return Koha::Patron::Attributes->_new_from_dbic($rs)->search;
1936 }
1937
1938 =head3 messages
1939
1940     my $messages = $patron->messages;
1941
1942 Return the message attached to the patron.
1943
1944 =cut
1945
1946 sub messages {
1947     my ( $self ) = @_;
1948     my $messages_rs = $self->_result->messages_borrowernumbers->search;
1949     return Koha::Patron::Messages->_new_from_dbic($messages_rs);
1950 }
1951
1952 =head3 lock
1953
1954     Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
1955
1956     Lock and optionally expire a patron account.
1957     Remove holds and article requests if remove flag set.
1958     In order to distinguish from locking by entering a wrong password, let's
1959     call this an administrative lockout.
1960
1961 =cut
1962
1963 sub lock {
1964     my ( $self, $params ) = @_;
1965     $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
1966     if( $params->{expire} ) {
1967         $self->dateexpiry( dt_from_string->subtract(days => 1) );
1968     }
1969     $self->store;
1970     if( $params->{remove} ) {
1971         $self->holds->delete;
1972         $self->article_requests->delete;
1973     }
1974     return $self;
1975 }
1976
1977 =head3 anonymize
1978
1979     Koha::Patrons->find($id)->anonymize;
1980
1981     Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
1982     are randomized, other personal data is cleared too.
1983     Patrons with issues are skipped.
1984
1985 =cut
1986
1987 sub anonymize {
1988     my ( $self ) = @_;
1989     if( $self->_result->issues->count ) {
1990         warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
1991         return;
1992     }
1993     # Mandatory fields come from the corresponding pref, but email fields
1994     # are removed since scrambled email addresses only generate errors
1995     my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
1996         split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
1997     $mandatory->{userid} = 1; # needed since sub store does not clear field
1998     my @columns = $self->_result->result_source->columns;
1999     @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized|auth_method/ } @columns;
2000     push @columns, 'dateofbirth'; # add this date back in
2001     foreach my $col (@columns) {
2002         $self->_anonymize_column($col, $mandatory->{lc $col} );
2003     }
2004     $self->anonymized(1)->store;
2005 }
2006
2007 sub _anonymize_column {
2008     my ( $self, $col, $mandatory ) = @_;
2009     my $col_info = $self->_result->result_source->column_info($col);
2010     my $type = $col_info->{data_type};
2011     my $nullable = $col_info->{is_nullable};
2012     my $val;
2013     if( $type =~ /char|text/ ) {
2014         $val = $mandatory
2015             ? Koha::Token->new->generate({ pattern => '\w{10}' })
2016             : $nullable
2017             ? undef
2018             : q{};
2019     } elsif( $type =~ /integer|int$|float|dec|double/ ) {
2020         $val = $nullable ? undef : 0;
2021     } elsif( $type =~ /date|time/ ) {
2022         $val = $nullable ? undef : dt_from_string;
2023     }
2024     $self->$col($val);
2025 }
2026
2027 =head3 add_guarantor
2028
2029     my $relationship = $patron->add_guarantor(
2030         {
2031             borrowernumber => $borrowernumber,
2032             relationships  => $relationship,
2033         }
2034     );
2035
2036     Adds a new guarantor to a patron.
2037
2038 =cut
2039
2040 sub add_guarantor {
2041     my ( $self, $params ) = @_;
2042
2043     my $guarantor_id = $params->{guarantor_id};
2044     my $relationship = $params->{relationship};
2045
2046     return Koha::Patron::Relationship->new(
2047         {
2048             guarantee_id => $self->id,
2049             guarantor_id => $guarantor_id,
2050             relationship => $relationship
2051         }
2052     )->store();
2053 }
2054
2055 =head3 get_extended_attribute
2056
2057 my $attribute_value = $patron->get_extended_attribute( $code );
2058
2059 Return the attribute for the code passed in parameter.
2060
2061 It not exist it returns undef
2062
2063 Note that this will not work for repeatable attribute types.
2064
2065 Maybe you certainly not want to use this method, it is actually only used for SHOW_BARCODE
2066 (which should be a real patron's attribute (not extended)
2067
2068 =cut
2069
2070 sub get_extended_attribute {
2071     my ( $self, $code, $value ) = @_;
2072     my $rs = $self->_result->borrower_attributes;
2073     return unless $rs;
2074     my $attribute = $rs->search({ code => $code, ( $value ? ( attribute => $value ) : () ) });
2075     return unless $attribute->count;
2076     return $attribute->next;
2077 }
2078
2079 =head3 to_api
2080
2081     my $json = $patron->to_api;
2082
2083 Overloaded method that returns a JSON representation of the Koha::Patron object,
2084 suitable for API output.
2085
2086 =cut
2087
2088 sub to_api {
2089     my ( $self, $params ) = @_;
2090
2091     my $json_patron = $self->SUPER::to_api( $params );
2092
2093     $json_patron->{restricted} = ( $self->is_debarred )
2094                                     ? Mojo::JSON->true
2095                                     : Mojo::JSON->false;
2096
2097     return $json_patron;
2098 }
2099
2100 =head3 to_api_mapping
2101
2102 This method returns the mapping for representing a Koha::Patron object
2103 on the API.
2104
2105 =cut
2106
2107 sub to_api_mapping {
2108     return {
2109         borrowernotes       => 'staff_notes',
2110         borrowernumber      => 'patron_id',
2111         branchcode          => 'library_id',
2112         categorycode        => 'category_id',
2113         checkprevcheckout   => 'check_previous_checkout',
2114         contactfirstname    => undef,                     # Unused
2115         contactname         => undef,                     # Unused
2116         contactnote         => 'altaddress_notes',
2117         contacttitle        => undef,                     # Unused
2118         dateenrolled        => 'date_enrolled',
2119         dateexpiry          => 'expiry_date',
2120         dateofbirth         => 'date_of_birth',
2121         debarred            => undef,                     # replaced by 'restricted'
2122         debarredcomment     => undef,    # calculated, API consumers will use /restrictions instead
2123         emailpro            => 'secondary_email',
2124         flags               => undef,    # permissions manipulation handled in /permissions
2125         gonenoaddress       => 'incorrect_address',
2126         lastseen            => 'last_seen',
2127         lost                => 'patron_card_lost',
2128         opacnote            => 'opac_notes',
2129         othernames          => 'other_name',
2130         password            => undef,            # password manipulation handled in /password
2131         phonepro            => 'secondary_phone',
2132         relationship        => 'relationship_type',
2133         sex                 => 'gender',
2134         smsalertnumber      => 'sms_number',
2135         sort1               => 'statistics_1',
2136         sort2               => 'statistics_2',
2137         autorenew_checkouts => 'autorenew_checkouts',
2138         streetnumber        => 'street_number',
2139         streettype          => 'street_type',
2140         zipcode             => 'postal_code',
2141         B_address           => 'altaddress_address',
2142         B_address2          => 'altaddress_address2',
2143         B_city              => 'altaddress_city',
2144         B_country           => 'altaddress_country',
2145         B_email             => 'altaddress_email',
2146         B_phone             => 'altaddress_phone',
2147         B_state             => 'altaddress_state',
2148         B_streetnumber      => 'altaddress_street_number',
2149         B_streettype        => 'altaddress_street_type',
2150         B_zipcode           => 'altaddress_postal_code',
2151         altcontactaddress1  => 'altcontact_address',
2152         altcontactaddress2  => 'altcontact_address2',
2153         altcontactaddress3  => 'altcontact_city',
2154         altcontactcountry   => 'altcontact_country',
2155         altcontactfirstname => 'altcontact_firstname',
2156         altcontactphone     => 'altcontact_phone',
2157         altcontactsurname   => 'altcontact_surname',
2158         altcontactstate     => 'altcontact_state',
2159         altcontactzipcode   => 'altcontact_postal_code',
2160         password_expiration_date => undef,
2161         primary_contact_method => undef,
2162         secret              => undef,
2163         auth_method         => undef,
2164     };
2165 }
2166
2167 =head3 queue_notice
2168
2169     Koha::Patrons->queue_notice({ letter_params => $letter_params, message_name => 'DUE'});
2170     Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports });
2171     Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports, test_mode => 1 });
2172
2173     Queue messages to a patron. Can pass a message that is part of the message_attributes
2174     table or supply the transport to use.
2175
2176     If passed a message name we retrieve the patrons preferences for transports
2177     Otherwise we use the supplied transport. In the case of email or sms we fall back to print if
2178     we have no address/number for sending
2179
2180     $letter_params is a hashref of the values to be passed to GetPreparedLetter
2181
2182     test_mode will only report which notices would be sent, but nothing will be queued
2183
2184 =cut
2185
2186 sub queue_notice {
2187     my ( $self, $params ) = @_;
2188     my $letter_params = $params->{letter_params};
2189     my $test_mode = $params->{test_mode};
2190
2191     return unless $letter_params;
2192     return unless exists $params->{message_name} xor $params->{message_transports}; # We only want one of these
2193
2194     my $library = Koha::Libraries->find( $letter_params->{branchcode} );
2195     my $from_email_address = $library->from_email_address;
2196
2197     my @message_transports;
2198     my $letter_code;
2199     $letter_code = $letter_params->{letter_code};
2200     if( $params->{message_name} ){
2201         my $messaging_prefs = C4::Members::Messaging::GetMessagingPreferences( {
2202                 borrowernumber => $letter_params->{borrowernumber},
2203                 message_name => $params->{message_name}
2204         } );
2205         @message_transports = ( keys %{ $messaging_prefs->{transports} } );
2206         $letter_code = $messaging_prefs->{transports}->{$message_transports[0]} unless $letter_code;
2207     } else {
2208         @message_transports = @{$params->{message_transports}};
2209     }
2210     return unless defined $letter_code;
2211     $letter_params->{letter_code} = $letter_code;
2212     my $print_sent = 0;
2213     my %return;
2214     foreach my $mtt (@message_transports){
2215         next if ($mtt eq 'itiva' and C4::Context->preference('TalkingTechItivaPhoneNotification') );
2216         # Notice is handled by TalkingTech_itiva_outbound.pl
2217         if (   ( $mtt eq 'email' and not $self->notice_email_address )
2218             or ( $mtt eq 'sms'   and not $self->smsalertnumber )
2219             or ( $mtt eq 'phone' and not $self->phone ) )
2220         {
2221             push @{ $return{fallback} }, $mtt;
2222             $mtt = 'print';
2223         }
2224         next if $mtt eq 'print' && $print_sent;
2225         $letter_params->{message_transport_type} = $mtt;
2226         my $letter = C4::Letters::GetPreparedLetter( %$letter_params );
2227         C4::Letters::EnqueueLetter({
2228             letter => $letter,
2229             borrowernumber => $self->borrowernumber,
2230             from_address   => $from_email_address,
2231             message_transport_type => $mtt
2232         }) unless $test_mode;
2233         push @{$return{sent}}, $mtt;
2234         $print_sent = 1 if $mtt eq 'print';
2235     }
2236     return \%return;
2237 }
2238
2239 =head3 safe_to_delete
2240
2241     my $result = $patron->safe_to_delete;
2242     if ( $result eq 'has_guarantees' ) { ... }
2243     elsif ( $result ) { ... }
2244     else { # cannot delete }
2245
2246 This method tells if the Koha:Patron object can be deleted. Possible return values
2247
2248 =over 4
2249
2250 =item 'ok'
2251
2252 =item 'has_checkouts'
2253
2254 =item 'has_debt'
2255
2256 =item 'has_guarantees'
2257
2258 =item 'is_anonymous_patron'
2259
2260 =back
2261
2262 =cut
2263
2264 sub safe_to_delete {
2265     my ($self) = @_;
2266
2267     my $anonymous_patron = C4::Context->preference('AnonymousPatron');
2268
2269     my $error;
2270
2271     if ( $anonymous_patron && $self->id eq $anonymous_patron ) {
2272         $error = 'is_anonymous_patron';
2273     }
2274     elsif ( $self->checkouts->count ) {
2275         $error = 'has_checkouts';
2276     }
2277     elsif ( $self->account->outstanding_debits->total_outstanding > 0 ) {
2278         $error = 'has_debt';
2279     }
2280     elsif ( $self->guarantee_relationships->count ) {
2281         $error = 'has_guarantees';
2282     }
2283
2284     if ( $error ) {
2285         return Koha::Result::Boolean->new(0)->add_message({ message => $error });
2286     }
2287
2288     return Koha::Result::Boolean->new(1);
2289 }
2290
2291 =head3 recalls
2292
2293     my $recalls = $patron->recalls;
2294
2295 Return the patron's recalls.
2296
2297 =cut
2298
2299 sub recalls {
2300     my ( $self ) = @_;
2301
2302     return Koha::Recalls->search({ patron_id => $self->borrowernumber });
2303 }
2304
2305 =head3 account_balance
2306
2307     my $balance = $patron->account_balance
2308
2309 Return the patron's account balance
2310
2311 =cut
2312
2313 sub account_balance {
2314     my ($self) = @_;
2315     return $self->account->balance;
2316 }
2317
2318 =head3 notify_library_of_registration
2319
2320 $patron->notify_library_of_registration( $email_patron_registrations );
2321
2322 Send patron registration email to library if EmailPatronRegistrations system preference is enabled.
2323
2324 =cut
2325
2326 sub notify_library_of_registration {
2327     my ( $self, $email_patron_registrations ) = @_;
2328
2329     if (
2330         my $letter = C4::Letters::GetPreparedLetter(
2331             module      => 'members',
2332             letter_code => 'OPAC_REG',
2333             branchcode  => $self->branchcode,
2334             lang        => $self->lang || 'default',
2335             tables      => {
2336                 'borrowers' => $self->borrowernumber
2337             },
2338         )
2339     ) {
2340         my $to_address;
2341         if ( $email_patron_registrations eq "BranchEmailAddress" ) {
2342             my $library = Koha::Libraries->find( $self->branchcode );
2343             $to_address = $library->inbound_email_address;
2344         }
2345         elsif ( $email_patron_registrations eq "KohaAdminEmailAddress" ) {
2346             $to_address = C4::Context->preference('ReplytoDefault')
2347             || C4::Context->preference('KohaAdminEmailAddress');
2348         }
2349         else {
2350             $to_address =
2351                 C4::Context->preference('EmailAddressForPatronRegistrations')
2352                 || C4::Context->preference('ReplytoDefault')
2353                 || C4::Context->preference('KohaAdminEmailAddress');
2354         }
2355
2356         my $message_id = C4::Letters::EnqueueLetter(
2357             {
2358                 letter                 => $letter,
2359                 borrowernumber         => $self->borrowernumber,
2360                 to_address             => $to_address,
2361                 message_transport_type => 'email'
2362             }
2363         ) or warn "can't enqueue letter $letter";
2364         if ( $message_id ) {
2365             return 1;
2366         }
2367     }
2368 }
2369
2370 =head3 has_messaging_preference
2371
2372 my $bool = $patron->has_messaging_preference({
2373     message_name => $message_name, # A value from message_attributes.message_name
2374     message_transport_type => $message_transport_type, # email, sms, phone, itiva, etc...
2375     wants_digest => $wants_digest, # 1 if you are looking for the digest version, don't pass if you just want either
2376 });
2377
2378 =cut
2379
2380 sub has_messaging_preference {
2381     my ( $self, $params ) = @_;
2382
2383     my $message_name           = $params->{message_name};
2384     my $message_transport_type = $params->{message_transport_type};
2385     my $wants_digest           = $params->{wants_digest};
2386
2387     return $self->_result->search_related_rs(
2388         'borrower_message_preferences',
2389         $params,
2390         {
2391             prefetch =>
2392               [ 'borrower_message_transport_preferences', 'message_attribute' ]
2393         }
2394     )->count;
2395 }
2396
2397 =head3 can_patron_change_staff_only_lists
2398
2399 $patron->can_patron_change_staff_only_lists;
2400
2401 Return 1 if a patron has 'Superlibrarian' or 'Catalogue' permission.
2402 Otherwise, return 0.
2403
2404 =cut
2405
2406 sub can_patron_change_staff_only_lists {
2407     my ( $self, $params ) = @_;
2408     return 1 if C4::Auth::haspermission( $self->userid, { 'catalogue' => 1 });
2409     return 0;
2410 }
2411
2412 =head3 can_patron_change_permitted_staff_lists
2413
2414 $patron->can_patron_change_permitted_staff_lists;
2415
2416 Return 1 if a patron has 'Superlibrarian' or 'Catalogue' and 'edit_public_list_contents' permissions.
2417 Otherwise, return 0.
2418
2419 =cut
2420
2421 sub can_patron_change_permitted_staff_lists {
2422     my ( $self, $params ) = @_;
2423     return 1 if C4::Auth::haspermission( $self->userid, { 'catalogue' => 1, lists => 'edit_public_list_contents' } );
2424     return 0;
2425 }
2426
2427 =head3 encode_secret
2428
2429   $patron->encode_secret($secret32);
2430
2431 Secret (TwoFactorAuth expects it in base32 format) is encrypted.
2432 You still need to call ->store.
2433
2434 =cut
2435
2436 sub encode_secret {
2437     my ( $self, $secret ) = @_;
2438     if( $secret ) {
2439         return $self->secret( Koha::Encryption->new->encrypt_hex($secret) );
2440     }
2441     return $self->secret($secret);
2442 }
2443
2444 =head3 decoded_secret
2445
2446   my $secret32 = $patron->decoded_secret;
2447
2448 Decode the patron secret. We expect to get back a base32 string, but this
2449 is not checked here. Caller of encode_secret is responsible for that.
2450
2451 =cut
2452
2453 sub decoded_secret {
2454     my ( $self ) = @_;
2455     if( $self->secret ) {
2456         return Koha::Encryption->new->decrypt_hex( $self->secret );
2457     }
2458     return $self->secret;
2459 }
2460
2461 =head3 virtualshelves
2462
2463     my $shelves = $patron->virtualshelves;
2464
2465 =cut
2466
2467 sub virtualshelves {
2468     my $self = shift;
2469     return Koha::Virtualshelves->_new_from_dbic( scalar $self->_result->virtualshelves );
2470 }
2471
2472 =head3 get_savings
2473
2474     my $savings = $patron->get_savings;
2475
2476 Use the replacement price of patron's old and current issues to calculate how much they have 'saved' by using the library.
2477
2478 =cut
2479
2480 sub get_savings {
2481     my ($self) = @_;
2482
2483     my @itemnumbers = grep { defined $_ } ( $self->old_checkouts->get_column('itemnumber'), $self->checkouts->get_column('itemnumber') );
2484
2485     return Koha::Items->search(
2486         { itemnumber => { -in => \@itemnumbers } },
2487         {   select => [ { sum => 'me.replacementprice' } ],
2488             as     => ['total_savings']
2489         }
2490     )->next->get_column('total_savings') // 0;
2491 }
2492
2493 =head2 Internal methods
2494
2495 =head3 _type
2496
2497 =cut
2498
2499 sub _type {
2500     return 'Borrower';
2501 }
2502
2503 =head1 AUTHORS
2504
2505 Kyle M Hall <kyle@bywatersolutions.com>
2506 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
2507 Martin Renvoize <martin.renvoize@ptfs-europe.com>
2508
2509 =cut
2510
2511 1;