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