Bug 26015: Terminology: Fix tons more staff clients to staff interfaces
[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 Carp;
24 use List::MoreUtils qw( any uniq );
25 use JSON qw( to_json );
26 use Unicode::Normalize;
27
28 use C4::Context;
29 use C4::Log;
30 use Koha::Account;
31 use Koha::ArticleRequests;
32 use Koha::AuthUtils;
33 use Koha::Checkouts;
34 use Koha::Club::Enrollments;
35 use Koha::Database;
36 use Koha::DateUtils;
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::HouseboundProfile;
43 use Koha::Patron::HouseboundRole;
44 use Koha::Patron::Images;
45 use Koha::Patron::Modifications;
46 use Koha::Patron::Relationships;
47 use Koha::Patrons;
48 use Koha::Plugins;
49 use Koha::Subscription::Routinglists;
50 use Koha::Token;
51 use Koha::Virtualshelves;
52
53 use base qw(Koha::Object);
54
55 use constant ADMINISTRATIVE_LOCKOUT => -1;
56
57 our $RESULTSET_PATRON_ID_MAPPING = {
58     Accountline          => 'borrowernumber',
59     Aqbasketuser         => 'borrowernumber',
60     Aqbudget             => 'budget_owner_id',
61     Aqbudgetborrower     => 'borrowernumber',
62     ArticleRequest       => 'borrowernumber',
63     BorrowerAttribute    => '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     $self->_result->result_source->schema->txn_do(
364         sub {
365             # Cancel Patron's holds
366             my $holds = $self->holds;
367             while( my $hold = $holds->next ){
368                 $hold->cancel;
369             }
370
371             # Delete all lists and all shares of this borrower
372             # Consistent with the approach Koha uses on deleting individual lists
373             # Note that entries in virtualshelfcontents added by this borrower to
374             # lists of others will be handled by a table constraint: the borrower
375             # is set to NULL in those entries.
376             # NOTE:
377             # We could handle the above deletes via a constraint too.
378             # But a new BZ report 11889 has been opened to discuss another approach.
379             # Instead of deleting we could also disown lists (based on a pref).
380             # In that way we could save shared and public lists.
381             # The current table constraints support that idea now.
382             # This pref should then govern the results of other routines/methods such as
383             # Koha::Virtualshelf->new->delete too.
384             # FIXME Could be $patron->get_lists
385             $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
386
387             # We cannot have a FK on borrower_modifications.borrowernumber, the table is also used
388             # for patron selfreg
389             $_->delete for Koha::Patron::Modifications->search( { borrowernumber => $self->borrowernumber } );
390
391             $self->SUPER::delete;
392
393             logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
394         }
395     );
396     return $self;
397 }
398
399
400 =head3 category
401
402 my $patron_category = $patron->category
403
404 Return the patron category for this patron
405
406 =cut
407
408 sub category {
409     my ( $self ) = @_;
410     return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
411 }
412
413 =head3 image
414
415 =cut
416
417 sub image {
418     my ( $self ) = @_;
419
420     return Koha::Patron::Images->find( $self->borrowernumber );
421 }
422
423 =head3 library
424
425 Returns a Koha::Library object representing the patron's home library.
426
427 =cut
428
429 sub library {
430     my ( $self ) = @_;
431     return Koha::Library->_new_from_dbic($self->_result->branchcode);
432 }
433
434 =head3 sms_provider
435
436 Returns a Koha::SMS::Provider object representing the patron's SMS provider.
437
438 =cut
439
440 sub sms_provider {
441     my ( $self ) = @_;
442     my $sms_provider_rs = $self->_result->sms_provider;
443     return unless $sms_provider_rs;
444     return Koha::SMS::Provider->_new_from_dbic($sms_provider_rs);
445 }
446
447 =head3 guarantor_relationships
448
449 Returns Koha::Patron::Relationships object for this patron's guarantors
450
451 Returns the set of relationships for the patrons that are guarantors for this patron.
452
453 This is returned instead of a Koha::Patron object because the guarantor
454 may not exist as a patron in Koha. If this is true, the guarantors name
455 exists in the Koha::Patron::Relationship object and will have no guarantor_id.
456
457 =cut
458
459 sub guarantor_relationships {
460     my ($self) = @_;
461
462     return Koha::Patron::Relationships->search( { guarantee_id => $self->id } );
463 }
464
465 =head3 guarantee_relationships
466
467 Returns Koha::Patron::Relationships object for this patron's guarantors
468
469 Returns the set of relationships for the patrons that are guarantees for this patron.
470
471 The method returns Koha::Patron::Relationship objects for the sake
472 of consistency with the guantors method.
473 A guarantee by definition must exist as a patron in Koha.
474
475 =cut
476
477 sub guarantee_relationships {
478     my ($self) = @_;
479
480     return Koha::Patron::Relationships->search(
481         { guarantor_id => $self->id },
482         {
483             prefetch => 'guarantee',
484             order_by => { -asc => [ 'guarantee.surname', 'guarantee.firstname' ] },
485         }
486     );
487 }
488
489 =head3 housebound_profile
490
491 Returns the HouseboundProfile associated with this patron.
492
493 =cut
494
495 sub housebound_profile {
496     my ( $self ) = @_;
497     my $profile = $self->_result->housebound_profile;
498     return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
499         if ( $profile );
500     return;
501 }
502
503 =head3 housebound_role
504
505 Returns the HouseboundRole associated with this patron.
506
507 =cut
508
509 sub housebound_role {
510     my ( $self ) = @_;
511
512     my $role = $self->_result->housebound_role;
513     return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
514     return;
515 }
516
517 =head3 siblings
518
519 Returns the siblings of this patron.
520
521 =cut
522
523 sub siblings {
524     my ($self) = @_;
525
526     my @guarantors = $self->guarantor_relationships()->guarantors();
527
528     return unless @guarantors;
529
530     my @siblings =
531       map { $_->guarantee_relationships()->guarantees() } @guarantors;
532
533     return unless @siblings;
534
535     my %seen;
536     @siblings =
537       grep { !$seen{ $_->id }++ && ( $_->id != $self->id ) } @siblings;
538
539     return wantarray ? @siblings : Koha::Patrons->search( { borrowernumber => { -in => [ map { $_->id } @siblings ] } } );
540 }
541
542 =head3 merge_with
543
544     my $patron = Koha::Patrons->find($id);
545     $patron->merge_with( \@patron_ids );
546
547     This subroutine merges a list of patrons into the patron record. This is accomplished by finding
548     all related patron ids for the patrons to be merged in other tables and changing the ids to be that
549     of the keeper patron.
550
551 =cut
552
553 sub merge_with {
554     my ( $self, $patron_ids ) = @_;
555
556     my @patron_ids = @{ $patron_ids };
557
558     # Ensure the keeper isn't in the list of patrons to merge
559     @patron_ids = grep { $_ ne $self->id } @patron_ids;
560
561     my $schema = Koha::Database->new()->schema();
562
563     my $results;
564
565     $self->_result->result_source->schema->txn_do( sub {
566         foreach my $patron_id (@patron_ids) {
567             my $patron = Koha::Patrons->find( $patron_id );
568
569             next unless $patron;
570
571             # Unbless for safety, the patron will end up being deleted
572             $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
573
574             while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
575                 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
576                 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
577                 $rs->update({ $field => $self->id });
578             }
579
580             $patron->move_to_deleted();
581             $patron->delete();
582         }
583     });
584
585     return $results;
586 }
587
588
589
590 =head3 wants_check_for_previous_checkout
591
592     $wants_check = $patron->wants_check_for_previous_checkout;
593
594 Return 1 if Koha needs to perform PrevIssue checking, else 0.
595
596 =cut
597
598 sub wants_check_for_previous_checkout {
599     my ( $self ) = @_;
600     my $syspref = C4::Context->preference("checkPrevCheckout");
601
602     # Simple cases
603     ## Hard syspref trumps all
604     return 1 if ($syspref eq 'hardyes');
605     return 0 if ($syspref eq 'hardno');
606     ## Now, patron pref trumps all
607     return 1 if ($self->checkprevcheckout eq 'yes');
608     return 0 if ($self->checkprevcheckout eq 'no');
609
610     # More complex: patron inherits -> determine category preference
611     my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
612     return 1 if ($checkPrevCheckoutByCat eq 'yes');
613     return 0 if ($checkPrevCheckoutByCat eq 'no');
614
615     # Finally: category preference is inherit, default to 0
616     if ($syspref eq 'softyes') {
617         return 1;
618     } else {
619         return 0;
620     }
621 }
622
623 =head3 do_check_for_previous_checkout
624
625     $do_check = $patron->do_check_for_previous_checkout($item);
626
627 Return 1 if the bib associated with $ITEM has previously been checked out to
628 $PATRON, 0 otherwise.
629
630 =cut
631
632 sub do_check_for_previous_checkout {
633     my ( $self, $item ) = @_;
634
635     my @item_nos;
636     my $biblio = Koha::Biblios->find( $item->{biblionumber} );
637     if ( $biblio->is_serial ) {
638         push @item_nos, $item->{itemnumber};
639     } else {
640         # Get all itemnumbers for given bibliographic record.
641         @item_nos = $biblio->items->get_column( 'itemnumber' );
642     }
643
644     # Create (old)issues search criteria
645     my $criteria = {
646         borrowernumber => $self->borrowernumber,
647         itemnumber => \@item_nos,
648     };
649
650     # Check current issues table
651     my $issues = Koha::Checkouts->search($criteria);
652     return 1 if $issues->count; # 0 || N
653
654     # Check old issues table
655     my $old_issues = Koha::Old::Checkouts->search($criteria);
656     return $old_issues->count;  # 0 || N
657 }
658
659 =head3 is_debarred
660
661 my $debarment_expiration = $patron->is_debarred;
662
663 Returns the date a patron debarment will expire, or undef if the patron is not
664 debarred
665
666 =cut
667
668 sub is_debarred {
669     my ($self) = @_;
670
671     return unless $self->debarred;
672     return $self->debarred
673       if $self->debarred =~ '^9999'
674       or dt_from_string( $self->debarred ) > dt_from_string;
675     return;
676 }
677
678 =head3 is_expired
679
680 my $is_expired = $patron->is_expired;
681
682 Returns 1 if the patron is expired or 0;
683
684 =cut
685
686 sub is_expired {
687     my ($self) = @_;
688     return 0 unless $self->dateexpiry;
689     return 0 if $self->dateexpiry =~ '^9999';
690     return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
691     return 0;
692 }
693
694 =head3 is_going_to_expire
695
696 my $is_going_to_expire = $patron->is_going_to_expire;
697
698 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
699
700 =cut
701
702 sub is_going_to_expire {
703     my ($self) = @_;
704
705     my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
706
707     return 0 unless $delay;
708     return 0 unless $self->dateexpiry;
709     return 0 if $self->dateexpiry =~ '^9999';
710     return 1 if dt_from_string( $self->dateexpiry, undef, 'floating' )->subtract( days => $delay ) < dt_from_string(undef, undef, 'floating')->truncate( to => 'day' );
711     return 0;
712 }
713
714 =head3 set_password
715
716     $patron->set_password({ password => $plain_text_password [, skip_validation => 1 ] });
717
718 Set the patron's password.
719
720 =head4 Exceptions
721
722 The passed string is validated against the current password enforcement policy.
723 Validation can be skipped by passing the I<skip_validation> parameter.
724
725 Exceptions are thrown if the password is not good enough.
726
727 =over 4
728
729 =item Koha::Exceptions::Password::TooShort
730
731 =item Koha::Exceptions::Password::WhitespaceCharacters
732
733 =item Koha::Exceptions::Password::TooWeak
734
735 =item Koha::Exceptions::Password::Plugin (if a "check password" plugin is enabled)
736
737 =back
738
739 =cut
740
741 sub set_password {
742     my ( $self, $args ) = @_;
743
744     my $password = $args->{password};
745
746     unless ( $args->{skip_validation} ) {
747         my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password );
748
749         if ( !$is_valid ) {
750             if ( $error eq 'too_short' ) {
751                 my $min_length = C4::Context->preference('minPasswordLength');
752                 $min_length = 3 if not $min_length or $min_length < 3;
753
754                 my $password_length = length($password);
755                 Koha::Exceptions::Password::TooShort->throw(
756                     length => $password_length, min_length => $min_length );
757             }
758             elsif ( $error eq 'has_whitespaces' ) {
759                 Koha::Exceptions::Password::WhitespaceCharacters->throw();
760             }
761             elsif ( $error eq 'too_weak' ) {
762                 Koha::Exceptions::Password::TooWeak->throw();
763             }
764         }
765     }
766
767     if ( C4::Context->config("enable_plugins") ) {
768         # Call any check_password plugins
769         my @plugins = Koha::Plugins->new()->GetPlugins({
770             method => 'check_password',
771         });
772         foreach my $plugin ( @plugins ) {
773             # This plugin hook will also be used by a plugin for the Norwegian national
774             # patron database. This is why we need to pass both the password and the
775             # borrowernumber to the plugin.
776             my $ret = $plugin->check_password(
777                 {
778                     password       => $password,
779                     borrowernumber => $self->borrowernumber
780                 }
781             );
782             # This plugin hook will also be used by a plugin for the Norwegian national
783             # patron database. This is why we need to call the actual plugins and then
784             # check skip_validation afterwards.
785             if ( $ret->{'error'} == 1 && !$args->{skip_validation} ) {
786                 Koha::Exceptions::Password::Plugin->throw();
787             }
788         }
789     }
790
791     my $digest = Koha::AuthUtils::hash_password($password);
792
793     # We do not want to call $self->store and retrieve password from DB
794     $self->password($digest);
795     $self->login_attempts(0);
796     $self->SUPER::store;
797
798     logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" )
799         if C4::Context->preference("BorrowersLog");
800
801     return $self;
802 }
803
804
805 =head3 renew_account
806
807 my $new_expiry_date = $patron->renew_account
808
809 Extending the subscription to the expiry date.
810
811 =cut
812
813 sub renew_account {
814     my ($self) = @_;
815     my $date;
816     if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
817         $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
818     } else {
819         $date =
820             C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
821             ? dt_from_string( $self->dateexpiry )
822             : dt_from_string;
823     }
824     my $expiry_date = $self->category->get_expiry_date($date);
825
826     $self->dateexpiry($expiry_date);
827     $self->date_renewed( dt_from_string() );
828     $self->store();
829
830     $self->add_enrolment_fee_if_needed(1);
831
832     logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
833     return dt_from_string( $expiry_date )->truncate( to => 'day' );
834 }
835
836 =head3 has_overdues
837
838 my $has_overdues = $patron->has_overdues;
839
840 Returns the number of patron's overdues
841
842 =cut
843
844 sub has_overdues {
845     my ($self) = @_;
846     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
847     return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
848 }
849
850 =head3 track_login
851
852     $patron->track_login;
853     $patron->track_login({ force => 1 });
854
855     Tracks a (successful) login attempt.
856     The preference TrackLastPatronActivity must be enabled. Or you
857     should pass the force parameter.
858
859 =cut
860
861 sub track_login {
862     my ( $self, $params ) = @_;
863     return if
864         !$params->{force} &&
865         !C4::Context->preference('TrackLastPatronActivity');
866     $self->lastseen( dt_from_string() )->store;
867 }
868
869 =head3 move_to_deleted
870
871 my $is_moved = $patron->move_to_deleted;
872
873 Move a patron to the deletedborrowers table.
874 This can be done before deleting a patron, to make sure the data are not completely deleted.
875
876 =cut
877
878 sub move_to_deleted {
879     my ($self) = @_;
880     my $patron_infos = $self->unblessed;
881     delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
882     return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
883 }
884
885 =head3 article_requests
886
887 my @requests = $borrower->article_requests();
888 my $requests = $borrower->article_requests();
889
890 Returns either a list of ArticleRequests objects,
891 or an ArtitleRequests object, depending on the
892 calling context.
893
894 =cut
895
896 sub article_requests {
897     my ( $self ) = @_;
898
899     $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
900
901     return $self->{_article_requests};
902 }
903
904 =head3 article_requests_current
905
906 my @requests = $patron->article_requests_current
907
908 Returns the article requests associated with this patron that are incomplete
909
910 =cut
911
912 sub article_requests_current {
913     my ( $self ) = @_;
914
915     $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
916         {
917             borrowernumber => $self->id(),
918             -or          => [
919                 { status => Koha::ArticleRequest::Status::Pending },
920                 { status => Koha::ArticleRequest::Status::Processing }
921             ]
922         }
923     );
924
925     return $self->{_article_requests_current};
926 }
927
928 =head3 article_requests_finished
929
930 my @requests = $biblio->article_requests_finished
931
932 Returns the article requests associated with this patron that are completed
933
934 =cut
935
936 sub article_requests_finished {
937     my ( $self, $borrower ) = @_;
938
939     $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
940         {
941             borrowernumber => $self->id(),
942             -or          => [
943                 { status => Koha::ArticleRequest::Status::Completed },
944                 { status => Koha::ArticleRequest::Status::Canceled }
945             ]
946         }
947     );
948
949     return $self->{_article_requests_finished};
950 }
951
952 =head3 add_enrolment_fee_if_needed
953
954 my $enrolment_fee = $patron->add_enrolment_fee_if_needed($renewal);
955
956 Add enrolment fee for a patron if needed.
957
958 $renewal - boolean denoting whether this is an account renewal or not
959
960 =cut
961
962 sub add_enrolment_fee_if_needed {
963     my ($self, $renewal) = @_;
964     my $enrolment_fee = $self->category->enrolmentfee;
965     if ( $enrolment_fee && $enrolment_fee > 0 ) {
966         my $type = $renewal ? 'ACCOUNT_RENEW' : 'ACCOUNT';
967         $self->account->add_debit(
968             {
969                 amount     => $enrolment_fee,
970                 user_id    => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
971                 interface  => C4::Context->interface,
972                 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
973                 type       => $type
974             }
975         );
976     }
977     return $enrolment_fee || 0;
978 }
979
980 =head3 checkouts
981
982 my $checkouts = $patron->checkouts
983
984 =cut
985
986 sub checkouts {
987     my ($self) = @_;
988     my $checkouts = $self->_result->issues;
989     return Koha::Checkouts->_new_from_dbic( $checkouts );
990 }
991
992 =head3 pending_checkouts
993
994 my $pending_checkouts = $patron->pending_checkouts
995
996 This method will return the same as $self->checkouts, but with a prefetch on
997 items, biblio and biblioitems.
998
999 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
1000
1001 It should not be used directly, prefer to access fields you need instead of
1002 retrieving all these fields in one go.
1003
1004 =cut
1005
1006 sub pending_checkouts {
1007     my( $self ) = @_;
1008     my $checkouts = $self->_result->issues->search(
1009         {},
1010         {
1011             order_by => [
1012                 { -desc => 'me.timestamp' },
1013                 { -desc => 'issuedate' },
1014                 { -desc => 'issue_id' }, # Sort by issue_id should be enough
1015             ],
1016             prefetch => { item => { biblio => 'biblioitems' } },
1017         }
1018     );
1019     return Koha::Checkouts->_new_from_dbic( $checkouts );
1020 }
1021
1022 =head3 old_checkouts
1023
1024 my $old_checkouts = $patron->old_checkouts
1025
1026 =cut
1027
1028 sub old_checkouts {
1029     my ($self) = @_;
1030     my $old_checkouts = $self->_result->old_issues;
1031     return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
1032 }
1033
1034 =head3 get_overdues
1035
1036 my $overdue_items = $patron->get_overdues
1037
1038 Return the overdue items
1039
1040 =cut
1041
1042 sub get_overdues {
1043     my ($self) = @_;
1044     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
1045     return $self->checkouts->search(
1046         {
1047             'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
1048         },
1049         {
1050             prefetch => { item => { biblio => 'biblioitems' } },
1051         }
1052     );
1053 }
1054
1055 =head3 get_routing_lists
1056
1057 my @routinglists = $patron->get_routing_lists
1058
1059 Returns the routing lists a patron is subscribed to.
1060
1061 =cut
1062
1063 sub get_routing_lists {
1064     my ($self) = @_;
1065     my $routing_list_rs = $self->_result->subscriptionroutinglists;
1066     return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
1067 }
1068
1069 =head3 get_age
1070
1071 my $age = $patron->get_age
1072
1073 Return the age of the patron
1074
1075 =cut
1076
1077 sub get_age {
1078     my ($self)    = @_;
1079     my $today_str = dt_from_string->strftime("%Y-%m-%d");
1080     return unless $self->dateofbirth;
1081     my $dob_str   = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
1082
1083     my ( $dob_y,   $dob_m,   $dob_d )   = split /-/, $dob_str;
1084     my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
1085
1086     my $age = $today_y - $dob_y;
1087     if ( $dob_m . $dob_d > $today_m . $today_d ) {
1088         $age--;
1089     }
1090
1091     return $age;
1092 }
1093
1094 =head3 is_valid_age
1095
1096 my $is_valid = $patron->is_valid_age
1097
1098 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
1099
1100 =cut
1101
1102 sub is_valid_age {
1103     my ($self) = @_;
1104     my $age = $self->get_age;
1105
1106     my $patroncategory = $self->category;
1107     my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
1108
1109     return (defined($age) && (($high && ($age > $high)) or ($age < $low))) ? 0 : 1;
1110 }
1111
1112 =head3 account
1113
1114 my $account = $patron->account
1115
1116 =cut
1117
1118 sub account {
1119     my ($self) = @_;
1120     return Koha::Account->new( { patron_id => $self->borrowernumber } );
1121 }
1122
1123 =head3 holds
1124
1125 my $holds = $patron->holds
1126
1127 Return all the holds placed by this patron
1128
1129 =cut
1130
1131 sub holds {
1132     my ($self) = @_;
1133     my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1134     return Koha::Holds->_new_from_dbic($holds_rs);
1135 }
1136
1137 =head3 old_holds
1138
1139 my $old_holds = $patron->old_holds
1140
1141 Return all the historical holds for this patron
1142
1143 =cut
1144
1145 sub old_holds {
1146     my ($self) = @_;
1147     my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1148     return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1149 }
1150
1151 =head3 return_claims
1152
1153 my $return_claims = $patron->return_claims
1154
1155 =cut
1156
1157 sub return_claims {
1158     my ($self) = @_;
1159     my $return_claims = $self->_result->return_claims_borrowernumbers;
1160     return Koha::Checkouts::ReturnClaims->_new_from_dbic( $return_claims );
1161 }
1162
1163 =head3 notice_email_address
1164
1165   my $email = $patron->notice_email_address;
1166
1167 Return the email address of patron used for notices.
1168 Returns the empty string if no email address.
1169
1170 =cut
1171
1172 sub notice_email_address{
1173     my ( $self ) = @_;
1174
1175     my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1176     # if syspref is set to 'first valid' (value == OFF), look up email address
1177     if ( $which_address eq 'OFF' ) {
1178         return $self->first_valid_email_address;
1179     }
1180
1181     return $self->$which_address || '';
1182 }
1183
1184 =head3 first_valid_email_address
1185
1186 my $first_valid_email_address = $patron->first_valid_email_address
1187
1188 Return the first valid email address for a patron.
1189 For now, the order  is defined as email, emailpro, B_email.
1190 Returns the empty string if the borrower has no email addresses.
1191
1192 =cut
1193
1194 sub first_valid_email_address {
1195     my ($self) = @_;
1196
1197     return $self->email() || $self->emailpro() || $self->B_email() || q{};
1198 }
1199
1200 =head3 get_club_enrollments
1201
1202 =cut
1203
1204 sub get_club_enrollments {
1205     my ( $self, $return_scalar ) = @_;
1206
1207     my $e = Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1208
1209     return $e if $return_scalar;
1210
1211     return wantarray ? $e->as_list : $e;
1212 }
1213
1214 =head3 get_enrollable_clubs
1215
1216 =cut
1217
1218 sub get_enrollable_clubs {
1219     my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
1220
1221     my $params;
1222     $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1223       if $is_enrollable_from_opac;
1224     $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1225
1226     $params->{borrower} = $self;
1227
1228     my $e = Koha::Clubs->get_enrollable($params);
1229
1230     return $e if $return_scalar;
1231
1232     return wantarray ? $e->as_list : $e;
1233 }
1234
1235 =head3 account_locked
1236
1237 my $is_locked = $patron->account_locked
1238
1239 Return true if the patron has reached the maximum number of login attempts
1240 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1241 as an administrative lockout (independent of FailedLoginAttempts; see also
1242 Koha::Patron->lock).
1243 Otherwise return false.
1244 If the pref is not set (empty string, null or 0), the feature is considered as
1245 disabled.
1246
1247 =cut
1248
1249 sub account_locked {
1250     my ($self) = @_;
1251     my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1252     return 1 if $FailedLoginAttempts
1253           and $self->login_attempts
1254           and $self->login_attempts >= $FailedLoginAttempts;
1255     return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1256     return 0;
1257 }
1258
1259 =head3 can_see_patron_infos
1260
1261 my $can_see = $patron->can_see_patron_infos( $patron );
1262
1263 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1264
1265 =cut
1266
1267 sub can_see_patron_infos {
1268     my ( $self, $patron ) = @_;
1269     return unless $patron;
1270     return $self->can_see_patrons_from( $patron->library->branchcode );
1271 }
1272
1273 =head3 can_see_patrons_from
1274
1275 my $can_see = $patron->can_see_patrons_from( $branchcode );
1276
1277 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1278
1279 =cut
1280
1281 sub can_see_patrons_from {
1282     my ( $self, $branchcode ) = @_;
1283     my $can = 0;
1284     if ( $self->branchcode eq $branchcode ) {
1285         $can = 1;
1286     } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1287         $can = 1;
1288     } elsif ( my $library_groups = $self->library->library_groups ) {
1289         while ( my $library_group = $library_groups->next ) {
1290             if ( $library_group->parent->has_child( $branchcode ) ) {
1291                 $can = 1;
1292                 last;
1293             }
1294         }
1295     }
1296     return $can;
1297 }
1298
1299 =head3 libraries_where_can_see_patrons
1300
1301 my $libraries = $patron-libraries_where_can_see_patrons;
1302
1303 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1304 The branchcodes are arbitrarily returned sorted.
1305 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1306
1307 An empty array means no restriction, the patron can see patron's infos from any libraries.
1308
1309 =cut
1310
1311 sub libraries_where_can_see_patrons {
1312     my ( $self ) = @_;
1313     my $userenv = C4::Context->userenv;
1314
1315     return () unless $userenv; # For tests, but userenv should be defined in tests...
1316
1317     my @restricted_branchcodes;
1318     if (C4::Context::only_my_library) {
1319         push @restricted_branchcodes, $self->branchcode;
1320     }
1321     else {
1322         unless (
1323             $self->has_permission(
1324                 { borrowers => 'view_borrower_infos_from_any_libraries' }
1325             )
1326           )
1327         {
1328             my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1329             if ( $library_groups->count )
1330             {
1331                 while ( my $library_group = $library_groups->next ) {
1332                     my $parent = $library_group->parent;
1333                     if ( $parent->has_child( $self->branchcode ) ) {
1334                         push @restricted_branchcodes, $parent->children->get_column('branchcode');
1335                     }
1336                 }
1337             }
1338
1339             @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1340         }
1341     }
1342
1343     @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1344     @restricted_branchcodes = uniq(@restricted_branchcodes);
1345     @restricted_branchcodes = sort(@restricted_branchcodes);
1346     return @restricted_branchcodes;
1347 }
1348
1349 =head3 has_permission
1350
1351 my $permission = $patron->has_permission($required);
1352
1353 See C4::Auth::haspermission for details of syntax for $required
1354
1355 =cut
1356
1357 sub has_permission {
1358     my ( $self, $flagsrequired ) = @_;
1359     return unless $self->userid;
1360     # TODO code from haspermission needs to be moved here!
1361     return C4::Auth::haspermission( $self->userid, $flagsrequired );
1362 }
1363
1364 =head3 is_adult
1365
1366 my $is_adult = $patron->is_adult
1367
1368 Return true if the patron has a category with a type Adult (A) or Organization (I)
1369
1370 =cut
1371
1372 sub is_adult {
1373     my ( $self ) = @_;
1374     return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1375 }
1376
1377 =head3 is_child
1378
1379 my $is_child = $patron->is_child
1380
1381 Return true if the patron has a category with a type Child (C)
1382
1383 =cut
1384
1385 sub is_child {
1386     my( $self ) = @_;
1387     return $self->category->category_type eq 'C' ? 1 : 0;
1388 }
1389
1390 =head3 has_valid_userid
1391
1392 my $patron = Koha::Patrons->find(42);
1393 $patron->userid( $new_userid );
1394 my $has_a_valid_userid = $patron->has_valid_userid
1395
1396 my $patron = Koha::Patron->new( $params );
1397 my $has_a_valid_userid = $patron->has_valid_userid
1398
1399 Return true if the current userid of this patron is valid/unique, otherwise false.
1400
1401 Note that this should be done in $self->store instead and raise an exception if needed.
1402
1403 =cut
1404
1405 sub has_valid_userid {
1406     my ($self) = @_;
1407
1408     return 0 unless $self->userid;
1409
1410     return 0 if ( $self->userid eq C4::Context->config('user') );    # DB user
1411
1412     my $already_exists = Koha::Patrons->search(
1413         {
1414             userid => $self->userid,
1415             (
1416                 $self->in_storage
1417                 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1418                 : ()
1419             ),
1420         }
1421     )->count;
1422     return $already_exists ? 0 : 1;
1423 }
1424
1425 =head3 generate_userid
1426
1427 my $patron = Koha::Patron->new( $params );
1428 $patron->generate_userid
1429
1430 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1431
1432 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).
1433
1434 =cut
1435
1436 sub generate_userid {
1437     my ($self) = @_;
1438     my $offset = 0;
1439     my $firstname = $self->firstname // q{};
1440     my $surname = $self->surname // q{};
1441     #The script will "do" the following code and increment the $offset until the generated userid is unique
1442     do {
1443       $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1444       $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1445       my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1446       $userid = NFKD( $userid );
1447       $userid =~ s/\p{NonspacingMark}//g;
1448       $userid .= $offset unless $offset == 0;
1449       $self->userid( $userid );
1450       $offset++;
1451      } while (! $self->has_valid_userid );
1452
1453      return $self;
1454 }
1455
1456 =head3 add_extended_attribute
1457
1458 =cut
1459
1460 sub add_extended_attribute {
1461     my ($self, $attribute) = @_;
1462     $attribute->{borrowernumber} = $self->borrowernumber;
1463     return Koha::Patron::Attribute->new($attribute)->store;
1464 }
1465
1466 =head3 extended_attributes
1467
1468 Return object of Koha::Patron::Attributes type with all attributes set for this patron
1469
1470 Or setter FIXME
1471
1472 =cut
1473
1474 sub extended_attributes {
1475     my ( $self, $attributes ) = @_;
1476     if ($attributes) {    # setter
1477         my $schema = $self->_result->result_source->schema;
1478         $schema->txn_do(
1479             sub {
1480                 # Remove the existing one
1481                 $self->extended_attributes->filter_by_branch_limitations->delete;
1482
1483                 # Insert the new ones
1484                 for my $attribute (@$attributes) {
1485                     eval {
1486                         $self->_result->create_related('borrower_attributes', $attribute);
1487                     };
1488                     # FIXME We should:
1489                     # 1 - Raise an exception
1490                     # 2 - Execute in a transaction and don't save
1491                     #  or Insert anyway but display a message on the UI
1492                     warn $@ if $@;
1493                 }
1494             }
1495         );
1496     }
1497
1498     my $rs = $self->_result->borrower_attributes;
1499     # We call search to use the filters in Koha::Patron::Attributes->search
1500     return Koha::Patron::Attributes->_new_from_dbic($rs)->search;
1501 }
1502
1503 =head3 lock
1504
1505     Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
1506
1507     Lock and optionally expire a patron account.
1508     Remove holds and article requests if remove flag set.
1509     In order to distinguish from locking by entering a wrong password, let's
1510     call this an administrative lockout.
1511
1512 =cut
1513
1514 sub lock {
1515     my ( $self, $params ) = @_;
1516     $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
1517     if( $params->{expire} ) {
1518         $self->dateexpiry( dt_from_string->subtract(days => 1) );
1519     }
1520     $self->store;
1521     if( $params->{remove} ) {
1522         $self->holds->delete;
1523         $self->article_requests->delete;
1524     }
1525     return $self;
1526 }
1527
1528 =head3 anonymize
1529
1530     Koha::Patrons->find($id)->anonymize;
1531
1532     Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
1533     are randomized, other personal data is cleared too.
1534     Patrons with issues are skipped.
1535
1536 =cut
1537
1538 sub anonymize {
1539     my ( $self ) = @_;
1540     if( $self->_result->issues->count ) {
1541         warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
1542         return;
1543     }
1544     # Mandatory fields come from the corresponding pref, but email fields
1545     # are removed since scrambled email addresses only generate errors
1546     my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
1547         split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
1548     $mandatory->{userid} = 1; # needed since sub store does not clear field
1549     my @columns = $self->_result->result_source->columns;
1550     @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized/ } @columns;
1551     push @columns, 'dateofbirth'; # add this date back in
1552     foreach my $col (@columns) {
1553         $self->_anonymize_column($col, $mandatory->{lc $col} );
1554     }
1555     $self->anonymized(1)->store;
1556 }
1557
1558 sub _anonymize_column {
1559     my ( $self, $col, $mandatory ) = @_;
1560     my $col_info = $self->_result->result_source->column_info($col);
1561     my $type = $col_info->{data_type};
1562     my $nullable = $col_info->{is_nullable};
1563     my $val;
1564     if( $type =~ /char|text/ ) {
1565         $val = $mandatory
1566             ? Koha::Token->new->generate({ pattern => '\w{10}' })
1567             : $nullable
1568             ? undef
1569             : q{};
1570     } elsif( $type =~ /integer|int$|float|dec|double/ ) {
1571         $val = $nullable ? undef : 0;
1572     } elsif( $type =~ /date|time/ ) {
1573         $val = $nullable ? undef : dt_from_string;
1574     }
1575     $self->$col($val);
1576 }
1577
1578 =head3 add_guarantor
1579
1580     my @relationships = $patron->add_guarantor(
1581         {
1582             borrowernumber => $borrowernumber,
1583             relationships  => $relationship,
1584         }
1585     );
1586
1587     Adds a new guarantor to a patron.
1588
1589 =cut
1590
1591 sub add_guarantor {
1592     my ( $self, $params ) = @_;
1593
1594     my $guarantor_id = $params->{guarantor_id};
1595     my $relationship = $params->{relationship};
1596
1597     return Koha::Patron::Relationship->new(
1598         {
1599             guarantee_id => $self->id,
1600             guarantor_id => $guarantor_id,
1601             relationship => $relationship
1602         }
1603     )->store();
1604 }
1605
1606 =head3 get_extended_attribute
1607
1608 my $attribute_value = $patron->get_extended_attribute( $code );
1609
1610 Return the attribute for the code passed in parameter.
1611
1612 It not exist it returns undef
1613
1614 Note that this will not work for repeatable attribute types.
1615
1616 Maybe you certainly not want to use this method, it is actually only used for SHOW_BARCODE
1617 (which should be a real patron's attribute (not extended)
1618
1619 =cut
1620
1621 sub get_extended_attribute {
1622     my ( $self, $code, $value ) = @_;
1623     my $rs = $self->_result->borrower_attributes;
1624     return unless $rs;
1625     my $attribute = $rs->search({ code => $code, ( $value ? ( attribute => $value ) : () ) });
1626     return unless $attribute->count;
1627     return $attribute->next;
1628 }
1629
1630 =head3 to_api
1631
1632     my $json = $patron->to_api;
1633
1634 Overloaded method that returns a JSON representation of the Koha::Patron object,
1635 suitable for API output.
1636
1637 =cut
1638
1639 sub to_api {
1640     my ( $self, $params ) = @_;
1641
1642     my $json_patron = $self->SUPER::to_api( $params );
1643
1644     $json_patron->{restricted} = ( $self->is_debarred )
1645                                     ? Mojo::JSON->true
1646                                     : Mojo::JSON->false;
1647
1648     return $json_patron;
1649 }
1650
1651 =head3 to_api_mapping
1652
1653 This method returns the mapping for representing a Koha::Patron object
1654 on the API.
1655
1656 =cut
1657
1658 sub to_api_mapping {
1659     return {
1660         borrowernotes       => 'staff_notes',
1661         borrowernumber      => 'patron_id',
1662         branchcode          => 'library_id',
1663         categorycode        => 'category_id',
1664         checkprevcheckout   => 'check_previous_checkout',
1665         contactfirstname    => undef,                     # Unused
1666         contactname         => undef,                     # Unused
1667         contactnote         => 'altaddress_notes',
1668         contacttitle        => undef,                     # Unused
1669         dateenrolled        => 'date_enrolled',
1670         dateexpiry          => 'expiry_date',
1671         dateofbirth         => 'date_of_birth',
1672         debarred            => undef,                     # replaced by 'restricted'
1673         debarredcomment     => undef,    # calculated, API consumers will use /restrictions instead
1674         emailpro            => 'secondary_email',
1675         flags               => undef,    # permissions manipulation handled in /permissions
1676         gonenoaddress       => 'incorrect_address',
1677         guarantorid         => 'guarantor_id',
1678         lastseen            => 'last_seen',
1679         lost                => 'patron_card_lost',
1680         opacnote            => 'opac_notes',
1681         othernames          => 'other_name',
1682         password            => undef,            # password manipulation handled in /password
1683         phonepro            => 'secondary_phone',
1684         relationship        => 'relationship_type',
1685         sex                 => 'gender',
1686         smsalertnumber      => 'sms_number',
1687         sort1               => 'statistics_1',
1688         sort2               => 'statistics_2',
1689         autorenew_checkouts => 'autorenew_checkouts',
1690         streetnumber        => 'street_number',
1691         streettype          => 'street_type',
1692         zipcode             => 'postal_code',
1693         B_address           => 'altaddress_address',
1694         B_address2          => 'altaddress_address2',
1695         B_city              => 'altaddress_city',
1696         B_country           => 'altaddress_country',
1697         B_email             => 'altaddress_email',
1698         B_phone             => 'altaddress_phone',
1699         B_state             => 'altaddress_state',
1700         B_streetnumber      => 'altaddress_street_number',
1701         B_streettype        => 'altaddress_street_type',
1702         B_zipcode           => 'altaddress_postal_code',
1703         altcontactaddress1  => 'altcontact_address',
1704         altcontactaddress2  => 'altcontact_address2',
1705         altcontactaddress3  => 'altcontact_city',
1706         altcontactcountry   => 'altcontact_country',
1707         altcontactfirstname => 'altcontact_firstname',
1708         altcontactphone     => 'altcontact_phone',
1709         altcontactsurname   => 'altcontact_surname',
1710         altcontactstate     => 'altcontact_state',
1711         altcontactzipcode   => 'altcontact_postal_code'
1712     };
1713 }
1714
1715 =head2 Internal methods
1716
1717 =head3 _type
1718
1719 =cut
1720
1721 sub _type {
1722     return 'Borrower';
1723 }
1724
1725 =head1 AUTHORS
1726
1727 Kyle M Hall <kyle@bywatersolutions.com>
1728 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1729 Martin Renvoize <martin.renvoize@ptfs-europe.com>
1730
1731 =cut
1732
1733 1;