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