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