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