Bug 30194: Adapt Koha to the latest JSON::Validator version
[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
1235     return unless $self->dateofbirth;
1236
1237     my $date_of_birth = dt_from_string( $self->dateofbirth );
1238     my $today         = dt_from_string->truncate( to => 'day' );
1239
1240     return $today->subtract_datetime( $date_of_birth )->years;
1241 }
1242
1243 =head3 is_valid_age
1244
1245 my $is_valid = $patron->is_valid_age
1246
1247 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
1248
1249 =cut
1250
1251 sub is_valid_age {
1252     my ($self) = @_;
1253     my $age = $self->get_age;
1254
1255     my $patroncategory = $self->category;
1256     my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
1257
1258     return (defined($age) && (($high && ($age > $high)) or ($low && ($age < $low)))) ? 0 : 1;
1259 }
1260
1261 =head3 account
1262
1263 my $account = $patron->account
1264
1265 =cut
1266
1267 sub account {
1268     my ($self) = @_;
1269     return Koha::Account->new( { patron_id => $self->borrowernumber } );
1270 }
1271
1272 =head3 holds
1273
1274 my $holds = $patron->holds
1275
1276 Return all the holds placed by this patron
1277
1278 =cut
1279
1280 sub holds {
1281     my ($self) = @_;
1282     my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1283     return Koha::Holds->_new_from_dbic($holds_rs);
1284 }
1285
1286 =head3 old_holds
1287
1288 my $old_holds = $patron->old_holds
1289
1290 Return all the historical holds for this patron
1291
1292 =cut
1293
1294 sub old_holds {
1295     my ($self) = @_;
1296     my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1297     return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1298 }
1299
1300 =head3 return_claims
1301
1302 my $return_claims = $patron->return_claims
1303
1304 =cut
1305
1306 sub return_claims {
1307     my ($self) = @_;
1308     my $return_claims = $self->_result->return_claims_borrowernumbers;
1309     return Koha::Checkouts::ReturnClaims->_new_from_dbic( $return_claims );
1310 }
1311
1312 =head3 notice_email_address
1313
1314   my $email = $patron->notice_email_address;
1315
1316 Return the email address of patron used for notices.
1317 Returns the empty string if no email address.
1318
1319 =cut
1320
1321 sub notice_email_address{
1322     my ( $self ) = @_;
1323
1324     my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1325     # if syspref is set to 'first valid' (value == OFF), look up email address
1326     if ( $which_address eq 'OFF' ) {
1327         return $self->first_valid_email_address;
1328     }
1329
1330     return $self->$which_address || '';
1331 }
1332
1333 =head3 first_valid_email_address
1334
1335 my $first_valid_email_address = $patron->first_valid_email_address
1336
1337 Return the first valid email address for a patron.
1338 For now, the order  is defined as email, emailpro, B_email.
1339 Returns the empty string if the borrower has no email addresses.
1340
1341 =cut
1342
1343 sub first_valid_email_address {
1344     my ($self) = @_;
1345
1346     return $self->email() || $self->emailpro() || $self->B_email() || q{};
1347 }
1348
1349 =head3 get_club_enrollments
1350
1351 =cut
1352
1353 sub get_club_enrollments {
1354     my ( $self ) = @_;
1355
1356     return Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1357 }
1358
1359 =head3 get_enrollable_clubs
1360
1361 =cut
1362
1363 sub get_enrollable_clubs {
1364     my ( $self, $is_enrollable_from_opac ) = @_;
1365
1366     my $params;
1367     $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1368       if $is_enrollable_from_opac;
1369     $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1370
1371     $params->{borrower} = $self;
1372
1373     return Koha::Clubs->get_enrollable($params);
1374 }
1375
1376 =head3 account_locked
1377
1378 my $is_locked = $patron->account_locked
1379
1380 Return true if the patron has reached the maximum number of login attempts
1381 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1382 as an administrative lockout (independent of FailedLoginAttempts; see also
1383 Koha::Patron->lock).
1384 Otherwise return false.
1385 If the pref is not set (empty string, null or 0), the feature is considered as
1386 disabled.
1387
1388 =cut
1389
1390 sub account_locked {
1391     my ($self) = @_;
1392     my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1393     return 1 if $FailedLoginAttempts
1394           and $self->login_attempts
1395           and $self->login_attempts >= $FailedLoginAttempts;
1396     return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1397     return 0;
1398 }
1399
1400 =head3 can_see_patron_infos
1401
1402 my $can_see = $patron->can_see_patron_infos( $patron );
1403
1404 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1405
1406 =cut
1407
1408 sub can_see_patron_infos {
1409     my ( $self, $patron ) = @_;
1410     return unless $patron;
1411     return $self->can_see_patrons_from( $patron->branchcode );
1412 }
1413
1414 =head3 can_see_patrons_from
1415
1416 my $can_see = $patron->can_see_patrons_from( $branchcode );
1417
1418 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1419
1420 =cut
1421
1422 sub can_see_patrons_from {
1423     my ( $self, $branchcode ) = @_;
1424     my $can = 0;
1425     if ( $self->branchcode eq $branchcode ) {
1426         $can = 1;
1427     } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1428         $can = 1;
1429     } elsif ( my $library_groups = $self->library->library_groups ) {
1430         while ( my $library_group = $library_groups->next ) {
1431             if ( $library_group->parent->has_child( $branchcode ) ) {
1432                 $can = 1;
1433                 last;
1434             }
1435         }
1436     }
1437     return $can;
1438 }
1439
1440 =head3 can_log_into
1441
1442 my $can_log_into = $patron->can_log_into( $library );
1443
1444 Given a I<Koha::Library> object, it returns a boolean representing
1445 the fact the patron can log into a the library.
1446
1447 =cut
1448
1449 sub can_log_into {
1450     my ( $self, $library ) = @_;
1451
1452     my $can = 0;
1453
1454     if ( C4::Context->preference('IndependentBranches') ) {
1455         $can = 1
1456           if $self->is_superlibrarian
1457           or $self->branchcode eq $library->id;
1458     }
1459     else {
1460         # no restrictions
1461         $can = 1;
1462     }
1463
1464    return $can;
1465 }
1466
1467 =head3 libraries_where_can_see_patrons
1468
1469 my $libraries = $patron-libraries_where_can_see_patrons;
1470
1471 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1472 The branchcodes are arbitrarily returned sorted.
1473 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1474
1475 An empty array means no restriction, the patron can see patron's infos from any libraries.
1476
1477 =cut
1478
1479 sub libraries_where_can_see_patrons {
1480     my ( $self ) = @_;
1481     my $userenv = C4::Context->userenv;
1482
1483     return () unless $userenv; # For tests, but userenv should be defined in tests...
1484
1485     my @restricted_branchcodes;
1486     if (C4::Context::only_my_library) {
1487         push @restricted_branchcodes, $self->branchcode;
1488     }
1489     else {
1490         unless (
1491             $self->has_permission(
1492                 { borrowers => 'view_borrower_infos_from_any_libraries' }
1493             )
1494           )
1495         {
1496             my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1497             if ( $library_groups->count )
1498             {
1499                 while ( my $library_group = $library_groups->next ) {
1500                     my $parent = $library_group->parent;
1501                     if ( $parent->has_child( $self->branchcode ) ) {
1502                         push @restricted_branchcodes, $parent->children->get_column('branchcode');
1503                     }
1504                 }
1505             }
1506
1507             @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1508         }
1509     }
1510
1511     @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1512     @restricted_branchcodes = uniq(@restricted_branchcodes);
1513     @restricted_branchcodes = sort(@restricted_branchcodes);
1514     return @restricted_branchcodes;
1515 }
1516
1517 =head3 has_permission
1518
1519 my $permission = $patron->has_permission($required);
1520
1521 See C4::Auth::haspermission for details of syntax for $required
1522
1523 =cut
1524
1525 sub has_permission {
1526     my ( $self, $flagsrequired ) = @_;
1527     return unless $self->userid;
1528     # TODO code from haspermission needs to be moved here!
1529     return C4::Auth::haspermission( $self->userid, $flagsrequired );
1530 }
1531
1532 =head3 is_superlibrarian
1533
1534   my $is_superlibrarian = $patron->is_superlibrarian;
1535
1536 Return true if the patron is a superlibrarian.
1537
1538 =cut
1539
1540 sub is_superlibrarian {
1541     my ($self) = @_;
1542     return $self->has_permission( { superlibrarian => 1 } ) ? 1 : 0;
1543 }
1544
1545 =head3 is_adult
1546
1547 my $is_adult = $patron->is_adult
1548
1549 Return true if the patron has a category with a type Adult (A) or Organization (I)
1550
1551 =cut
1552
1553 sub is_adult {
1554     my ( $self ) = @_;
1555     return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1556 }
1557
1558 =head3 is_child
1559
1560 my $is_child = $patron->is_child
1561
1562 Return true if the patron has a category with a type Child (C)
1563
1564 =cut
1565
1566 sub is_child {
1567     my( $self ) = @_;
1568     return $self->category->category_type eq 'C' ? 1 : 0;
1569 }
1570
1571 =head3 has_valid_userid
1572
1573 my $patron = Koha::Patrons->find(42);
1574 $patron->userid( $new_userid );
1575 my $has_a_valid_userid = $patron->has_valid_userid
1576
1577 my $patron = Koha::Patron->new( $params );
1578 my $has_a_valid_userid = $patron->has_valid_userid
1579
1580 Return true if the current userid of this patron is valid/unique, otherwise false.
1581
1582 Note that this should be done in $self->store instead and raise an exception if needed.
1583
1584 =cut
1585
1586 sub has_valid_userid {
1587     my ($self) = @_;
1588
1589     return 0 unless $self->userid;
1590
1591     return 0 if ( $self->userid eq C4::Context->config('user') );    # DB user
1592
1593     my $already_exists = Koha::Patrons->search(
1594         {
1595             userid => $self->userid,
1596             (
1597                 $self->in_storage
1598                 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1599                 : ()
1600             ),
1601         }
1602     )->count;
1603     return $already_exists ? 0 : 1;
1604 }
1605
1606 =head3 generate_userid
1607
1608 my $patron = Koha::Patron->new( $params );
1609 $patron->generate_userid
1610
1611 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1612
1613 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).
1614
1615 =cut
1616
1617 sub generate_userid {
1618     my ($self) = @_;
1619     my $offset = 0;
1620     my $firstname = $self->firstname // q{};
1621     my $surname = $self->surname // q{};
1622     #The script will "do" the following code and increment the $offset until the generated userid is unique
1623     do {
1624       $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1625       $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1626       my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1627       $userid = NFKD( $userid );
1628       $userid =~ s/\p{NonspacingMark}//g;
1629       $userid .= $offset unless $offset == 0;
1630       $self->userid( $userid );
1631       $offset++;
1632      } while (! $self->has_valid_userid );
1633
1634      return $self;
1635 }
1636
1637 =head3 add_extended_attribute
1638
1639 =cut
1640
1641 sub add_extended_attribute {
1642     my ($self, $attribute) = @_;
1643
1644     return Koha::Patron::Attribute->new(
1645         {
1646             %$attribute,
1647             ( borrowernumber => $self->borrowernumber ),
1648         }
1649     )->store;
1650
1651 }
1652
1653 =head3 extended_attributes
1654
1655 Return object of Koha::Patron::Attributes type with all attributes set for this patron
1656
1657 Or setter FIXME
1658
1659 =cut
1660
1661 sub extended_attributes {
1662     my ( $self, $attributes ) = @_;
1663     if ($attributes) {    # setter
1664         my $schema = $self->_result->result_source->schema;
1665         $schema->txn_do(
1666             sub {
1667                 # Remove the existing one
1668                 $self->extended_attributes->filter_by_branch_limitations->delete;
1669
1670                 # Insert the new ones
1671                 my $new_types = {};
1672                 for my $attribute (@$attributes) {
1673                     $self->add_extended_attribute($attribute);
1674                     $new_types->{$attribute->{code}} = 1;
1675                 }
1676
1677                 # Check globally mandatory types
1678                 my @required_attribute_types =
1679                     Koha::Patron::Attribute::Types->search(
1680                         {
1681                             mandatory => 1,
1682                             'borrower_attribute_types_branches.b_branchcode' =>
1683                               undef
1684                         },
1685                         { join => 'borrower_attribute_types_branches' }
1686                     )->get_column('code');
1687                 for my $type ( @required_attribute_types ) {
1688                     Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute->throw(
1689                         type => $type,
1690                     ) if !$new_types->{$type};
1691                 }
1692             }
1693         );
1694     }
1695
1696     my $rs = $self->_result->borrower_attributes;
1697     # We call search to use the filters in Koha::Patron::Attributes->search
1698     return Koha::Patron::Attributes->_new_from_dbic($rs)->search;
1699 }
1700
1701 =head3 messages
1702
1703     my $messages = $patron->messages;
1704
1705 Return the message attached to the patron.
1706
1707 =cut
1708
1709 sub messages {
1710     my ( $self ) = @_;
1711     my $messages_rs = $self->_result->messages_borrowernumbers->search;
1712     return Koha::Patron::Messages->_new_from_dbic($messages_rs);
1713 }
1714
1715 =head3 lock
1716
1717     Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
1718
1719     Lock and optionally expire a patron account.
1720     Remove holds and article requests if remove flag set.
1721     In order to distinguish from locking by entering a wrong password, let's
1722     call this an administrative lockout.
1723
1724 =cut
1725
1726 sub lock {
1727     my ( $self, $params ) = @_;
1728     $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
1729     if( $params->{expire} ) {
1730         $self->dateexpiry( dt_from_string->subtract(days => 1) );
1731     }
1732     $self->store;
1733     if( $params->{remove} ) {
1734         $self->holds->delete;
1735         $self->article_requests->delete;
1736     }
1737     return $self;
1738 }
1739
1740 =head3 anonymize
1741
1742     Koha::Patrons->find($id)->anonymize;
1743
1744     Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
1745     are randomized, other personal data is cleared too.
1746     Patrons with issues are skipped.
1747
1748 =cut
1749
1750 sub anonymize {
1751     my ( $self ) = @_;
1752     if( $self->_result->issues->count ) {
1753         warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
1754         return;
1755     }
1756     # Mandatory fields come from the corresponding pref, but email fields
1757     # are removed since scrambled email addresses only generate errors
1758     my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
1759         split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
1760     $mandatory->{userid} = 1; # needed since sub store does not clear field
1761     my @columns = $self->_result->result_source->columns;
1762     @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized/ } @columns;
1763     push @columns, 'dateofbirth'; # add this date back in
1764     foreach my $col (@columns) {
1765         $self->_anonymize_column($col, $mandatory->{lc $col} );
1766     }
1767     $self->anonymized(1)->store;
1768 }
1769
1770 sub _anonymize_column {
1771     my ( $self, $col, $mandatory ) = @_;
1772     my $col_info = $self->_result->result_source->column_info($col);
1773     my $type = $col_info->{data_type};
1774     my $nullable = $col_info->{is_nullable};
1775     my $val;
1776     if( $type =~ /char|text/ ) {
1777         $val = $mandatory
1778             ? Koha::Token->new->generate({ pattern => '\w{10}' })
1779             : $nullable
1780             ? undef
1781             : q{};
1782     } elsif( $type =~ /integer|int$|float|dec|double/ ) {
1783         $val = $nullable ? undef : 0;
1784     } elsif( $type =~ /date|time/ ) {
1785         $val = $nullable ? undef : dt_from_string;
1786     }
1787     $self->$col($val);
1788 }
1789
1790 =head3 add_guarantor
1791
1792     my $relationship = $patron->add_guarantor(
1793         {
1794             borrowernumber => $borrowernumber,
1795             relationships  => $relationship,
1796         }
1797     );
1798
1799     Adds a new guarantor to a patron.
1800
1801 =cut
1802
1803 sub add_guarantor {
1804     my ( $self, $params ) = @_;
1805
1806     my $guarantor_id = $params->{guarantor_id};
1807     my $relationship = $params->{relationship};
1808
1809     return Koha::Patron::Relationship->new(
1810         {
1811             guarantee_id => $self->id,
1812             guarantor_id => $guarantor_id,
1813             relationship => $relationship
1814         }
1815     )->store();
1816 }
1817
1818 =head3 get_extended_attribute
1819
1820 my $attribute_value = $patron->get_extended_attribute( $code );
1821
1822 Return the attribute for the code passed in parameter.
1823
1824 It not exist it returns undef
1825
1826 Note that this will not work for repeatable attribute types.
1827
1828 Maybe you certainly not want to use this method, it is actually only used for SHOW_BARCODE
1829 (which should be a real patron's attribute (not extended)
1830
1831 =cut
1832
1833 sub get_extended_attribute {
1834     my ( $self, $code, $value ) = @_;
1835     my $rs = $self->_result->borrower_attributes;
1836     return unless $rs;
1837     my $attribute = $rs->search({ code => $code, ( $value ? ( attribute => $value ) : () ) });
1838     return unless $attribute->count;
1839     return $attribute->next;
1840 }
1841
1842 =head3 to_api
1843
1844     my $json = $patron->to_api;
1845
1846 Overloaded method that returns a JSON representation of the Koha::Patron object,
1847 suitable for API output.
1848
1849 =cut
1850
1851 sub to_api {
1852     my ( $self, $params ) = @_;
1853
1854     my $json_patron = $self->SUPER::to_api( $params );
1855
1856     $json_patron->{restricted} = ( $self->is_debarred )
1857                                     ? Mojo::JSON->true
1858                                     : Mojo::JSON->false;
1859
1860     return $json_patron;
1861 }
1862
1863 =head3 to_api_mapping
1864
1865 This method returns the mapping for representing a Koha::Patron object
1866 on the API.
1867
1868 =cut
1869
1870 sub to_api_mapping {
1871     return {
1872         borrowernotes       => 'staff_notes',
1873         borrowernumber      => 'patron_id',
1874         branchcode          => 'library_id',
1875         categorycode        => 'category_id',
1876         checkprevcheckout   => 'check_previous_checkout',
1877         contactfirstname    => undef,                     # Unused
1878         contactname         => undef,                     # Unused
1879         contactnote         => 'altaddress_notes',
1880         contacttitle        => undef,                     # Unused
1881         dateenrolled        => 'date_enrolled',
1882         dateexpiry          => 'expiry_date',
1883         dateofbirth         => 'date_of_birth',
1884         debarred            => undef,                     # replaced by 'restricted'
1885         debarredcomment     => undef,    # calculated, API consumers will use /restrictions instead
1886         emailpro            => 'secondary_email',
1887         flags               => undef,    # permissions manipulation handled in /permissions
1888         gonenoaddress       => 'incorrect_address',
1889         guarantorid         => 'guarantor_id',
1890         lastseen            => 'last_seen',
1891         lost                => 'patron_card_lost',
1892         opacnote            => 'opac_notes',
1893         othernames          => 'other_name',
1894         password            => undef,            # password manipulation handled in /password
1895         phonepro            => 'secondary_phone',
1896         relationship        => 'relationship_type',
1897         sex                 => 'gender',
1898         smsalertnumber      => 'sms_number',
1899         sort1               => 'statistics_1',
1900         sort2               => 'statistics_2',
1901         autorenew_checkouts => 'autorenew_checkouts',
1902         streetnumber        => 'street_number',
1903         streettype          => 'street_type',
1904         zipcode             => 'postal_code',
1905         B_address           => 'altaddress_address',
1906         B_address2          => 'altaddress_address2',
1907         B_city              => 'altaddress_city',
1908         B_country           => 'altaddress_country',
1909         B_email             => 'altaddress_email',
1910         B_phone             => 'altaddress_phone',
1911         B_state             => 'altaddress_state',
1912         B_streetnumber      => 'altaddress_street_number',
1913         B_streettype        => 'altaddress_street_type',
1914         B_zipcode           => 'altaddress_postal_code',
1915         altcontactaddress1  => 'altcontact_address',
1916         altcontactaddress2  => 'altcontact_address2',
1917         altcontactaddress3  => 'altcontact_city',
1918         altcontactcountry   => 'altcontact_country',
1919         altcontactfirstname => 'altcontact_firstname',
1920         altcontactphone     => 'altcontact_phone',
1921         altcontactsurname   => 'altcontact_surname',
1922         altcontactstate     => 'altcontact_state',
1923         altcontactzipcode   => 'altcontact_postal_code',
1924         primary_contact_method => undef,
1925     };
1926 }
1927
1928 =head3 queue_notice
1929
1930     Koha::Patrons->queue_notice({ letter_params => $letter_params, message_name => 'DUE'});
1931     Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports });
1932     Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports, test_mode => 1 });
1933
1934     Queue messages to a patron. Can pass a message that is part of the message_attributes
1935     table or supply the transport to use.
1936
1937     If passed a message name we retrieve the patrons preferences for transports
1938     Otherwise we use the supplied transport. In the case of email or sms we fall back to print if
1939     we have no address/number for sending
1940
1941     $letter_params is a hashref of the values to be passed to GetPreparedLetter
1942
1943     test_mode will only report which notices would be sent, but nothing will be queued
1944
1945 =cut
1946
1947 sub queue_notice {
1948     my ( $self, $params ) = @_;
1949     my $letter_params = $params->{letter_params};
1950     my $test_mode = $params->{test_mode};
1951
1952     return unless $letter_params;
1953     return unless exists $params->{message_name} xor $params->{message_transports}; # We only want one of these
1954
1955     my $library = Koha::Libraries->find( $letter_params->{branchcode} );
1956     my $from_email_address = $library->from_email_address;
1957
1958     my @message_transports;
1959     my $letter_code;
1960     $letter_code = $letter_params->{letter_code};
1961     if( $params->{message_name} ){
1962         my $messaging_prefs = C4::Members::Messaging::GetMessagingPreferences( {
1963                 borrowernumber => $letter_params->{borrowernumber},
1964                 message_name => $params->{message_name}
1965         } );
1966         @message_transports = ( keys %{ $messaging_prefs->{transports} } );
1967         $letter_code = $messaging_prefs->{transports}->{$message_transports[0]} unless $letter_code;
1968     } else {
1969         @message_transports = @{$params->{message_transports}};
1970     }
1971     return unless defined $letter_code;
1972     $letter_params->{letter_code} = $letter_code;
1973     my $print_sent = 0;
1974     my %return;
1975     foreach my $mtt (@message_transports){
1976         next if ($mtt eq 'itiva' and C4::Context->preference('TalkingTechItivaPhoneNotification') );
1977         # Notice is handled by TalkingTech_itiva_outbound.pl
1978         if (   ( $mtt eq 'email' and not $self->notice_email_address )
1979             or ( $mtt eq 'sms'   and not $self->smsalertnumber )
1980             or ( $mtt eq 'phone' and not $self->phone ) )
1981         {
1982             push @{ $return{fallback} }, $mtt;
1983             $mtt = 'print';
1984         }
1985         next if $mtt eq 'print' && $print_sent;
1986         $letter_params->{message_transport_type} = $mtt;
1987         my $letter = C4::Letters::GetPreparedLetter( %$letter_params );
1988         C4::Letters::EnqueueLetter({
1989             letter => $letter,
1990             borrowernumber => $self->borrowernumber,
1991             from_address   => $from_email_address,
1992             message_transport_type => $mtt
1993         }) unless $test_mode;
1994         push @{$return{sent}}, $mtt;
1995         $print_sent = 1 if $mtt eq 'print';
1996     }
1997     return \%return;
1998 }
1999
2000 =head3 safe_to_delete
2001
2002     my $result = $patron->safe_to_delete;
2003     if ( $result eq 'has_guarantees' ) { ... }
2004     elsif ( $result ) { ... }
2005     else { # cannot delete }
2006
2007 This method tells if the Koha:Patron object can be deleted. Possible return values
2008
2009 =over 4
2010
2011 =item 'ok'
2012
2013 =item 'has_checkouts'
2014
2015 =item 'has_debt'
2016
2017 =item 'has_guarantees'
2018
2019 =item 'is_anonymous_patron'
2020
2021 =back
2022
2023 =cut
2024
2025 sub safe_to_delete {
2026     my ($self) = @_;
2027
2028     my $anonymous_patron = C4::Context->preference('AnonymousPatron');
2029
2030     my $error;
2031
2032     if ( $anonymous_patron && $self->id eq $anonymous_patron ) {
2033         $error = 'is_anonymous_patron';
2034     }
2035     elsif ( $self->checkouts->count ) {
2036         $error = 'has_checkouts';
2037     }
2038     elsif ( $self->account->outstanding_debits->total_outstanding > 0 ) {
2039         $error = 'has_debt';
2040     }
2041     elsif ( $self->guarantee_relationships->count ) {
2042         $error = 'has_guarantees';
2043     }
2044
2045     if ( $error ) {
2046         return Koha::Result::Boolean->new(0)->add_message({ message => $error });
2047     }
2048
2049     return Koha::Result::Boolean->new(1);
2050 }
2051
2052 =head3 recalls
2053
2054     my $recalls = $patron->recalls;
2055
2056 Return the patron's recalls.
2057
2058 =cut
2059
2060 sub recalls {
2061     my ( $self ) = @_;
2062
2063     return Koha::Recalls->search({ borrowernumber => $self->borrowernumber });
2064 }
2065
2066 =head2 Internal methods
2067
2068 =head3 _type
2069
2070 =cut
2071
2072 sub _type {
2073     return 'Borrower';
2074 }
2075
2076 =head1 AUTHORS
2077
2078 Kyle M Hall <kyle@bywatersolutions.com>
2079 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
2080 Martin Renvoize <martin.renvoize@ptfs-europe.com>
2081
2082 =cut
2083
2084 1;