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