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