Bug 20888: (follow-up) revert unecessary change on jquery selector
[koha.git] / Koha / Patron.pm
blob8f3d8e6990950300235f46ea4fe073c579f605fd
1 package Koha::Patron;
3 # Copyright ByWater Solutions 2014
4 # Copyright PTFS Europe 2016
6 # This file is part of Koha.
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.
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.
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>.
21 use Modern::Perl;
23 use Carp;
24 use List::MoreUtils qw( any uniq );
25 use JSON qw( to_json );
26 use Unicode::Normalize;
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;
53 use base qw(Koha::Object);
55 use constant ADMINISTRATIVE_LOCKOUT => -1;
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',
87 =head1 NAME
89 Koha::Patron - Koha Patron Object class
91 =head1 API
93 =head2 Class Methods
95 =head3 new
97 =cut
99 sub new {
100 my ( $class, $params ) = @_;
102 return $class->SUPER::new($params);
105 =head3 fixup_cardnumber
107 Autogenerate next cardnumber from highest value found in database
109 =cut
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);
122 =head3 trim_whitespace
124 trim whitespace from data which has some non-whitespace in it.
125 Could be moved to Koha::Object if need to be reused
127 =cut
129 sub trim_whitespaces {
130 my( $self ) = @_;
132 my $schema = Koha::Database->new->schema;
133 my @columns = $schema->source($self->_type)->columns;
135 for my $column( @columns ) {
136 my $value = $self->$column;
137 if ( defined $value ) {
138 $value =~ s/^\s*|\s*$//g;
139 $self->$column($value);
142 return $self;
145 =head3 plain_text_password
147 $patron->plain_text_password( $password );
149 stores a copy of the unencrypted password in the object
150 for use in code before encrypting for db
152 =cut
154 sub plain_text_password {
155 my ( $self, $password ) = @_;
156 if ( $password ) {
157 $self->{_plain_text_password} = $password;
158 return $self;
160 return $self->{_plain_text_password}
161 if $self->{_plain_text_password};
163 return;
166 =head3 store
168 Patron specific store method to cleanup record
169 and do other necessary things before saving
170 to db
172 =cut
174 sub store {
175 my ($self) = @_;
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 '' )
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;
191 unless( $self->category->in_storage ) {
192 Koha::Exceptions::Object::FKConstraint->throw(
193 broken_fk => 'categorycode',
194 value => $self->categorycode,
198 $self->trim_whitespaces;
200 # Set surname to uppercase if uppercasesurname is true
201 $self->surname( uc($self->surname) )
202 if C4::Context->preference("uppercasesurnames");
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 "";
208 unless ( $self->in_storage ) { #AddMember
210 # Generate a valid userid/login if needed
211 $self->generate_userid
212 if not $self->userid or not $self->has_valid_userid;
214 # Add expiration date if it isn't already there
215 unless ( $self->dateexpiry ) {
216 $self->dateexpiry( $self->category->get_expiry_date );
219 # Add enrollment date if it isn't already there
220 unless ( $self->dateenrolled ) {
221 $self->dateenrolled(dt_from_string);
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);
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',
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(
244 password => $self->password,
245 borrowernumber => $self->borrowernumber
248 if ( $ret->{'error'} == 1 ) {
249 Koha::Exceptions::Password::Plugin->throw();
254 # Make a copy of the plain text password for later use
255 $self->plain_text_password( $self->password );
257 # Create a disabled account if no password provided
258 $self->password( $self->password
259 ? Koha::AuthUtils::hash_password( $self->password )
260 : '!' );
262 $self->borrowernumber(undef);
264 $self = $self->SUPER::store;
266 $self->add_enrolment_fee_if_needed(0);
268 logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
269 if C4::Context->preference("BorrowersLog");
271 else { #ModMember
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);
281 # Password must be updated using $self->set_password
282 $self->password($self_from_storage->password);
284 if ( $self->category->categorycode ne
285 $self_from_storage->category->categorycode )
287 # Add enrolement fee on category change if required
288 $self->add_enrolment_fee_if_needed(1)
289 if C4::Context->preference('FeeOnChangePatronCategory');
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' );
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 (
308 !defined( $from_storage->{$key} )
309 && defined( $from_object->{$key} )
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} )
321 $info->{$key} = {
322 before => $from_storage->{$key},
323 after => $from_object->{$key}
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 }
341 # Final store
342 $self = $self->SUPER::store;
346 return $self;
349 =head3 delete
351 $patron->delete
353 Delete patron's holds, lists and finally the patron.
355 Lists owned by the borrower are deleted, but entries from the borrower to
356 other lists are kept.
358 =cut
360 sub delete {
361 my ($self) = @_;
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;
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 } );
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 } );
391 $self->SUPER::delete;
393 logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
396 return $self;
400 =head3 category
402 my $patron_category = $patron->category
404 Return the patron category for this patron
406 =cut
408 sub category {
409 my ( $self ) = @_;
410 return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
413 =head3 image
415 =cut
417 sub image {
418 my ( $self ) = @_;
420 return Koha::Patron::Images->find( $self->borrowernumber );
423 =head3 library
425 Returns a Koha::Library object representing the patron's home library.
427 =cut
429 sub library {
430 my ( $self ) = @_;
431 return Koha::Library->_new_from_dbic($self->_result->branchcode);
434 =head3 sms_provider
436 Returns a Koha::SMS::Provider object representing the patron's SMS provider.
438 =cut
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);
447 =head3 guarantor_relationships
449 Returns Koha::Patron::Relationships object for this patron's guarantors
451 Returns the set of relationships for the patrons that are guarantors for this patron.
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.
457 =cut
459 sub guarantor_relationships {
460 my ($self) = @_;
462 return Koha::Patron::Relationships->search( { guarantee_id => $self->id } );
465 =head3 guarantee_relationships
467 Returns Koha::Patron::Relationships object for this patron's guarantors
469 Returns the set of relationships for the patrons that are guarantees for this patron.
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.
475 =cut
477 sub guarantee_relationships {
478 my ($self) = @_;
480 return Koha::Patron::Relationships->search(
481 { guarantor_id => $self->id },
483 prefetch => 'guarantee',
484 order_by => { -asc => [ 'guarantee.surname', 'guarantee.firstname' ] },
489 =head3 relationships_debt
491 Returns the amount owed by the patron's guarantors *and* the other guarantees of those guarantors
493 =cut
495 sub relationships_debt {
496 my ($self, $params) = @_;
498 my $include_guarantors = $params->{include_guarantors};
499 my $only_this_guarantor = $params->{only_this_guarantor};
500 my $include_this_patron = $params->{include_this_patron};
502 my @guarantors;
503 if ( $only_this_guarantor ) {
504 @guarantors = $self->guarantee_relationships->count ? ( $self ) : ();
505 Koha::Exceptions::BadParameter->throw( { parameter => 'only_this_guarantor' } ) unless @guarantors;
506 } elsif ( $self->guarantor_relationships->count ) {
507 # I am a guarantee, just get all my guarantors
508 @guarantors = $self->guarantor_relationships->guarantors;
509 } else {
510 # I am a guarantor, I need to get all the guarantors of all my guarantees
511 @guarantors = map { $_->guarantor_relationships->guarantors } $self->guarantee_relationships->guarantees;
514 my $non_issues_charges = 0;
515 my $seen = $include_this_patron ? {} : { $self->id => 1 }; # For tracking members already added to the total
516 foreach my $guarantor (@guarantors) {
517 $non_issues_charges += $guarantor->account->non_issues_charges if $include_guarantors && !$seen->{ $guarantor->id };
519 # We've added what the guarantor owes, not added in that guarantor's guarantees as well
520 my @guarantees = map { $_->guarantee } $guarantor->guarantee_relationships();
521 my $guarantees_non_issues_charges = 0;
522 foreach my $guarantee (@guarantees) {
523 next if $seen->{ $guarantee->id };
524 $guarantees_non_issues_charges += $guarantee->account->non_issues_charges;
525 # Mark this guarantee as seen so we don't double count a guarantee linked to multiple guarantors
526 $seen->{ $guarantee->id } = 1;
529 $non_issues_charges += $guarantees_non_issues_charges;
530 $seen->{ $guarantor->id } = 1;
533 return $non_issues_charges;
536 =head3 housebound_profile
538 Returns the HouseboundProfile associated with this patron.
540 =cut
542 sub housebound_profile {
543 my ( $self ) = @_;
544 my $profile = $self->_result->housebound_profile;
545 return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
546 if ( $profile );
547 return;
550 =head3 housebound_role
552 Returns the HouseboundRole associated with this patron.
554 =cut
556 sub housebound_role {
557 my ( $self ) = @_;
559 my $role = $self->_result->housebound_role;
560 return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
561 return;
564 =head3 siblings
566 Returns the siblings of this patron.
568 =cut
570 sub siblings {
571 my ($self) = @_;
573 my @guarantors = $self->guarantor_relationships()->guarantors();
575 return unless @guarantors;
577 my @siblings =
578 map { $_->guarantee_relationships()->guarantees() } @guarantors;
580 return unless @siblings;
582 my %seen;
583 @siblings =
584 grep { !$seen{ $_->id }++ && ( $_->id != $self->id ) } @siblings;
586 return wantarray ? @siblings : Koha::Patrons->search( { borrowernumber => { -in => [ map { $_->id } @siblings ] } } );
589 =head3 merge_with
591 my $patron = Koha::Patrons->find($id);
592 $patron->merge_with( \@patron_ids );
594 This subroutine merges a list of patrons into the patron record. This is accomplished by finding
595 all related patron ids for the patrons to be merged in other tables and changing the ids to be that
596 of the keeper patron.
598 =cut
600 sub merge_with {
601 my ( $self, $patron_ids ) = @_;
603 my @patron_ids = @{ $patron_ids };
605 # Ensure the keeper isn't in the list of patrons to merge
606 @patron_ids = grep { $_ ne $self->id } @patron_ids;
608 my $schema = Koha::Database->new()->schema();
610 my $results;
612 $self->_result->result_source->schema->txn_do( sub {
613 foreach my $patron_id (@patron_ids) {
614 my $patron = Koha::Patrons->find( $patron_id );
616 next unless $patron;
618 # Unbless for safety, the patron will end up being deleted
619 $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
621 while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
622 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
623 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
624 $rs->update({ $field => $self->id });
627 $patron->move_to_deleted();
628 $patron->delete();
632 return $results;
637 =head3 wants_check_for_previous_checkout
639 $wants_check = $patron->wants_check_for_previous_checkout;
641 Return 1 if Koha needs to perform PrevIssue checking, else 0.
643 =cut
645 sub wants_check_for_previous_checkout {
646 my ( $self ) = @_;
647 my $syspref = C4::Context->preference("checkPrevCheckout");
649 # Simple cases
650 ## Hard syspref trumps all
651 return 1 if ($syspref eq 'hardyes');
652 return 0 if ($syspref eq 'hardno');
653 ## Now, patron pref trumps all
654 return 1 if ($self->checkprevcheckout eq 'yes');
655 return 0 if ($self->checkprevcheckout eq 'no');
657 # More complex: patron inherits -> determine category preference
658 my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
659 return 1 if ($checkPrevCheckoutByCat eq 'yes');
660 return 0 if ($checkPrevCheckoutByCat eq 'no');
662 # Finally: category preference is inherit, default to 0
663 if ($syspref eq 'softyes') {
664 return 1;
665 } else {
666 return 0;
670 =head3 do_check_for_previous_checkout
672 $do_check = $patron->do_check_for_previous_checkout($item);
674 Return 1 if the bib associated with $ITEM has previously been checked out to
675 $PATRON, 0 otherwise.
677 =cut
679 sub do_check_for_previous_checkout {
680 my ( $self, $item ) = @_;
682 my @item_nos;
683 my $biblio = Koha::Biblios->find( $item->{biblionumber} );
684 if ( $biblio->is_serial ) {
685 push @item_nos, $item->{itemnumber};
686 } else {
687 # Get all itemnumbers for given bibliographic record.
688 @item_nos = $biblio->items->get_column( 'itemnumber' );
691 # Create (old)issues search criteria
692 my $criteria = {
693 borrowernumber => $self->borrowernumber,
694 itemnumber => \@item_nos,
697 # Check current issues table
698 my $issues = Koha::Checkouts->search($criteria);
699 return 1 if $issues->count; # 0 || N
701 # Check old issues table
702 my $old_issues = Koha::Old::Checkouts->search($criteria);
703 return $old_issues->count; # 0 || N
706 =head3 is_debarred
708 my $debarment_expiration = $patron->is_debarred;
710 Returns the date a patron debarment will expire, or undef if the patron is not
711 debarred
713 =cut
715 sub is_debarred {
716 my ($self) = @_;
718 return unless $self->debarred;
719 return $self->debarred
720 if $self->debarred =~ '^9999'
721 or dt_from_string( $self->debarred ) > dt_from_string;
722 return;
725 =head3 is_expired
727 my $is_expired = $patron->is_expired;
729 Returns 1 if the patron is expired or 0;
731 =cut
733 sub is_expired {
734 my ($self) = @_;
735 return 0 unless $self->dateexpiry;
736 return 0 if $self->dateexpiry =~ '^9999';
737 return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
738 return 0;
741 =head3 is_going_to_expire
743 my $is_going_to_expire = $patron->is_going_to_expire;
745 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
747 =cut
749 sub is_going_to_expire {
750 my ($self) = @_;
752 my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
754 return 0 unless $delay;
755 return 0 unless $self->dateexpiry;
756 return 0 if $self->dateexpiry =~ '^9999';
757 return 1 if dt_from_string( $self->dateexpiry, undef, 'floating' )->subtract( days => $delay ) < dt_from_string(undef, undef, 'floating')->truncate( to => 'day' );
758 return 0;
761 =head3 set_password
763 $patron->set_password({ password => $plain_text_password [, skip_validation => 1 ] });
765 Set the patron's password.
767 =head4 Exceptions
769 The passed string is validated against the current password enforcement policy.
770 Validation can be skipped by passing the I<skip_validation> parameter.
772 Exceptions are thrown if the password is not good enough.
774 =over 4
776 =item Koha::Exceptions::Password::TooShort
778 =item Koha::Exceptions::Password::WhitespaceCharacters
780 =item Koha::Exceptions::Password::TooWeak
782 =item Koha::Exceptions::Password::Plugin (if a "check password" plugin is enabled)
784 =back
786 =cut
788 sub set_password {
789 my ( $self, $args ) = @_;
791 my $password = $args->{password};
793 unless ( $args->{skip_validation} ) {
794 my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password, $self->category );
796 if ( !$is_valid ) {
797 if ( $error eq 'too_short' ) {
798 my $min_length = $self->category->effective_min_password_length;
799 $min_length = 3 if not $min_length or $min_length < 3;
801 my $password_length = length($password);
802 Koha::Exceptions::Password::TooShort->throw(
803 length => $password_length, min_length => $min_length );
805 elsif ( $error eq 'has_whitespaces' ) {
806 Koha::Exceptions::Password::WhitespaceCharacters->throw();
808 elsif ( $error eq 'too_weak' ) {
809 Koha::Exceptions::Password::TooWeak->throw();
814 if ( C4::Context->config("enable_plugins") ) {
815 # Call any check_password plugins
816 my @plugins = Koha::Plugins->new()->GetPlugins({
817 method => 'check_password',
819 foreach my $plugin ( @plugins ) {
820 # This plugin hook will also be used by a plugin for the Norwegian national
821 # patron database. This is why we need to pass both the password and the
822 # borrowernumber to the plugin.
823 my $ret = $plugin->check_password(
825 password => $password,
826 borrowernumber => $self->borrowernumber
829 # This plugin hook will also be used by a plugin for the Norwegian national
830 # patron database. This is why we need to call the actual plugins and then
831 # check skip_validation afterwards.
832 if ( $ret->{'error'} == 1 && !$args->{skip_validation} ) {
833 Koha::Exceptions::Password::Plugin->throw();
838 my $digest = Koha::AuthUtils::hash_password($password);
840 # We do not want to call $self->store and retrieve password from DB
841 $self->password($digest);
842 $self->login_attempts(0);
843 $self->SUPER::store;
845 logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" )
846 if C4::Context->preference("BorrowersLog");
848 return $self;
852 =head3 renew_account
854 my $new_expiry_date = $patron->renew_account
856 Extending the subscription to the expiry date.
858 =cut
860 sub renew_account {
861 my ($self) = @_;
862 my $date;
863 if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
864 $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
865 } else {
866 $date =
867 C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
868 ? dt_from_string( $self->dateexpiry )
869 : dt_from_string;
871 my $expiry_date = $self->category->get_expiry_date($date);
873 $self->dateexpiry($expiry_date);
874 $self->date_renewed( dt_from_string() );
875 $self->store();
877 $self->add_enrolment_fee_if_needed(1);
879 logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
880 return dt_from_string( $expiry_date )->truncate( to => 'day' );
883 =head3 has_overdues
885 my $has_overdues = $patron->has_overdues;
887 Returns the number of patron's overdues
889 =cut
891 sub has_overdues {
892 my ($self) = @_;
893 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
894 return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
897 =head3 track_login
899 $patron->track_login;
900 $patron->track_login({ force => 1 });
902 Tracks a (successful) login attempt.
903 The preference TrackLastPatronActivity must be enabled. Or you
904 should pass the force parameter.
906 =cut
908 sub track_login {
909 my ( $self, $params ) = @_;
910 return if
911 !$params->{force} &&
912 !C4::Context->preference('TrackLastPatronActivity');
913 $self->lastseen( dt_from_string() )->store;
916 =head3 move_to_deleted
918 my $is_moved = $patron->move_to_deleted;
920 Move a patron to the deletedborrowers table.
921 This can be done before deleting a patron, to make sure the data are not completely deleted.
923 =cut
925 sub move_to_deleted {
926 my ($self) = @_;
927 my $patron_infos = $self->unblessed;
928 delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
929 return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
932 =head3 article_requests
934 my @requests = $borrower->article_requests();
935 my $requests = $borrower->article_requests();
937 Returns either a list of ArticleRequests objects,
938 or an ArtitleRequests object, depending on the
939 calling context.
941 =cut
943 sub article_requests {
944 my ( $self ) = @_;
946 $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
948 return $self->{_article_requests};
951 =head3 article_requests_current
953 my @requests = $patron->article_requests_current
955 Returns the article requests associated with this patron that are incomplete
957 =cut
959 sub article_requests_current {
960 my ( $self ) = @_;
962 $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
964 borrowernumber => $self->id(),
965 -or => [
966 { status => Koha::ArticleRequest::Status::Pending },
967 { status => Koha::ArticleRequest::Status::Processing }
972 return $self->{_article_requests_current};
975 =head3 article_requests_finished
977 my @requests = $biblio->article_requests_finished
979 Returns the article requests associated with this patron that are completed
981 =cut
983 sub article_requests_finished {
984 my ( $self, $borrower ) = @_;
986 $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
988 borrowernumber => $self->id(),
989 -or => [
990 { status => Koha::ArticleRequest::Status::Completed },
991 { status => Koha::ArticleRequest::Status::Canceled }
996 return $self->{_article_requests_finished};
999 =head3 add_enrolment_fee_if_needed
1001 my $enrolment_fee = $patron->add_enrolment_fee_if_needed($renewal);
1003 Add enrolment fee for a patron if needed.
1005 $renewal - boolean denoting whether this is an account renewal or not
1007 =cut
1009 sub add_enrolment_fee_if_needed {
1010 my ($self, $renewal) = @_;
1011 my $enrolment_fee = $self->category->enrolmentfee;
1012 if ( $enrolment_fee && $enrolment_fee > 0 ) {
1013 my $type = $renewal ? 'ACCOUNT_RENEW' : 'ACCOUNT';
1014 $self->account->add_debit(
1016 amount => $enrolment_fee,
1017 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
1018 interface => C4::Context->interface,
1019 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
1020 type => $type
1024 return $enrolment_fee || 0;
1027 =head3 checkouts
1029 my $checkouts = $patron->checkouts
1031 =cut
1033 sub checkouts {
1034 my ($self) = @_;
1035 my $checkouts = $self->_result->issues;
1036 return Koha::Checkouts->_new_from_dbic( $checkouts );
1039 =head3 pending_checkouts
1041 my $pending_checkouts = $patron->pending_checkouts
1043 This method will return the same as $self->checkouts, but with a prefetch on
1044 items, biblio and biblioitems.
1046 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
1048 It should not be used directly, prefer to access fields you need instead of
1049 retrieving all these fields in one go.
1051 =cut
1053 sub pending_checkouts {
1054 my( $self ) = @_;
1055 my $checkouts = $self->_result->issues->search(
1058 order_by => [
1059 { -desc => 'me.timestamp' },
1060 { -desc => 'issuedate' },
1061 { -desc => 'issue_id' }, # Sort by issue_id should be enough
1063 prefetch => { item => { biblio => 'biblioitems' } },
1066 return Koha::Checkouts->_new_from_dbic( $checkouts );
1069 =head3 old_checkouts
1071 my $old_checkouts = $patron->old_checkouts
1073 =cut
1075 sub old_checkouts {
1076 my ($self) = @_;
1077 my $old_checkouts = $self->_result->old_issues;
1078 return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
1081 =head3 get_overdues
1083 my $overdue_items = $patron->get_overdues
1085 Return the overdue items
1087 =cut
1089 sub get_overdues {
1090 my ($self) = @_;
1091 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
1092 return $self->checkouts->search(
1094 'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
1097 prefetch => { item => { biblio => 'biblioitems' } },
1102 =head3 get_routing_lists
1104 my @routinglists = $patron->get_routing_lists
1106 Returns the routing lists a patron is subscribed to.
1108 =cut
1110 sub get_routing_lists {
1111 my ($self) = @_;
1112 my $routing_list_rs = $self->_result->subscriptionroutinglists;
1113 return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
1116 =head3 get_age
1118 my $age = $patron->get_age
1120 Return the age of the patron
1122 =cut
1124 sub get_age {
1125 my ($self) = @_;
1126 my $today_str = dt_from_string->strftime("%Y-%m-%d");
1127 return unless $self->dateofbirth;
1128 my $dob_str = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
1130 my ( $dob_y, $dob_m, $dob_d ) = split /-/, $dob_str;
1131 my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
1133 my $age = $today_y - $dob_y;
1134 if ( $dob_m . $dob_d > $today_m . $today_d ) {
1135 $age--;
1138 return $age;
1141 =head3 is_valid_age
1143 my $is_valid = $patron->is_valid_age
1145 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
1147 =cut
1149 sub is_valid_age {
1150 my ($self) = @_;
1151 my $age = $self->get_age;
1153 my $patroncategory = $self->category;
1154 my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
1156 return (defined($age) && (($high && ($age > $high)) or ($age < $low))) ? 0 : 1;
1159 =head3 account
1161 my $account = $patron->account
1163 =cut
1165 sub account {
1166 my ($self) = @_;
1167 return Koha::Account->new( { patron_id => $self->borrowernumber } );
1170 =head3 holds
1172 my $holds = $patron->holds
1174 Return all the holds placed by this patron
1176 =cut
1178 sub holds {
1179 my ($self) = @_;
1180 my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1181 return Koha::Holds->_new_from_dbic($holds_rs);
1184 =head3 old_holds
1186 my $old_holds = $patron->old_holds
1188 Return all the historical holds for this patron
1190 =cut
1192 sub old_holds {
1193 my ($self) = @_;
1194 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1195 return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1198 =head3 return_claims
1200 my $return_claims = $patron->return_claims
1202 =cut
1204 sub return_claims {
1205 my ($self) = @_;
1206 my $return_claims = $self->_result->return_claims_borrowernumbers;
1207 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $return_claims );
1210 =head3 notice_email_address
1212 my $email = $patron->notice_email_address;
1214 Return the email address of patron used for notices.
1215 Returns the empty string if no email address.
1217 =cut
1219 sub notice_email_address{
1220 my ( $self ) = @_;
1222 my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1223 # if syspref is set to 'first valid' (value == OFF), look up email address
1224 if ( $which_address eq 'OFF' ) {
1225 return $self->first_valid_email_address;
1228 return $self->$which_address || '';
1231 =head3 first_valid_email_address
1233 my $first_valid_email_address = $patron->first_valid_email_address
1235 Return the first valid email address for a patron.
1236 For now, the order is defined as email, emailpro, B_email.
1237 Returns the empty string if the borrower has no email addresses.
1239 =cut
1241 sub first_valid_email_address {
1242 my ($self) = @_;
1244 return $self->email() || $self->emailpro() || $self->B_email() || q{};
1247 =head3 get_club_enrollments
1249 =cut
1251 sub get_club_enrollments {
1252 my ( $self, $return_scalar ) = @_;
1254 my $e = Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1256 return $e if $return_scalar;
1258 return wantarray ? $e->as_list : $e;
1261 =head3 get_enrollable_clubs
1263 =cut
1265 sub get_enrollable_clubs {
1266 my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
1268 my $params;
1269 $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1270 if $is_enrollable_from_opac;
1271 $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1273 $params->{borrower} = $self;
1275 my $e = Koha::Clubs->get_enrollable($params);
1277 return $e if $return_scalar;
1279 return wantarray ? $e->as_list : $e;
1282 =head3 account_locked
1284 my $is_locked = $patron->account_locked
1286 Return true if the patron has reached the maximum number of login attempts
1287 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1288 as an administrative lockout (independent of FailedLoginAttempts; see also
1289 Koha::Patron->lock).
1290 Otherwise return false.
1291 If the pref is not set (empty string, null or 0), the feature is considered as
1292 disabled.
1294 =cut
1296 sub account_locked {
1297 my ($self) = @_;
1298 my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1299 return 1 if $FailedLoginAttempts
1300 and $self->login_attempts
1301 and $self->login_attempts >= $FailedLoginAttempts;
1302 return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1303 return 0;
1306 =head3 can_see_patron_infos
1308 my $can_see = $patron->can_see_patron_infos( $patron );
1310 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1312 =cut
1314 sub can_see_patron_infos {
1315 my ( $self, $patron ) = @_;
1316 return unless $patron;
1317 return $self->can_see_patrons_from( $patron->library->branchcode );
1320 =head3 can_see_patrons_from
1322 my $can_see = $patron->can_see_patrons_from( $branchcode );
1324 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1326 =cut
1328 sub can_see_patrons_from {
1329 my ( $self, $branchcode ) = @_;
1330 my $can = 0;
1331 if ( $self->branchcode eq $branchcode ) {
1332 $can = 1;
1333 } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1334 $can = 1;
1335 } elsif ( my $library_groups = $self->library->library_groups ) {
1336 while ( my $library_group = $library_groups->next ) {
1337 if ( $library_group->parent->has_child( $branchcode ) ) {
1338 $can = 1;
1339 last;
1343 return $can;
1346 =head3 libraries_where_can_see_patrons
1348 my $libraries = $patron-libraries_where_can_see_patrons;
1350 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1351 The branchcodes are arbitrarily returned sorted.
1352 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1354 An empty array means no restriction, the patron can see patron's infos from any libraries.
1356 =cut
1358 sub libraries_where_can_see_patrons {
1359 my ( $self ) = @_;
1360 my $userenv = C4::Context->userenv;
1362 return () unless $userenv; # For tests, but userenv should be defined in tests...
1364 my @restricted_branchcodes;
1365 if (C4::Context::only_my_library) {
1366 push @restricted_branchcodes, $self->branchcode;
1368 else {
1369 unless (
1370 $self->has_permission(
1371 { borrowers => 'view_borrower_infos_from_any_libraries' }
1375 my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1376 if ( $library_groups->count )
1378 while ( my $library_group = $library_groups->next ) {
1379 my $parent = $library_group->parent;
1380 if ( $parent->has_child( $self->branchcode ) ) {
1381 push @restricted_branchcodes, $parent->children->get_column('branchcode');
1386 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1390 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1391 @restricted_branchcodes = uniq(@restricted_branchcodes);
1392 @restricted_branchcodes = sort(@restricted_branchcodes);
1393 return @restricted_branchcodes;
1396 =head3 has_permission
1398 my $permission = $patron->has_permission($required);
1400 See C4::Auth::haspermission for details of syntax for $required
1402 =cut
1404 sub has_permission {
1405 my ( $self, $flagsrequired ) = @_;
1406 return unless $self->userid;
1407 # TODO code from haspermission needs to be moved here!
1408 return C4::Auth::haspermission( $self->userid, $flagsrequired );
1411 =head3 is_superlibrarian
1413 my $is_superlibrarian = $patron->is_superlibrarian;
1415 Return true if the patron is a superlibrarian.
1417 =cut
1419 sub is_superlibrarian {
1420 my ($self) = @_;
1421 return $self->has_permission( { superlibrarian => 1 } ) ? 1 : 0;
1424 =head3 is_adult
1426 my $is_adult = $patron->is_adult
1428 Return true if the patron has a category with a type Adult (A) or Organization (I)
1430 =cut
1432 sub is_adult {
1433 my ( $self ) = @_;
1434 return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1437 =head3 is_child
1439 my $is_child = $patron->is_child
1441 Return true if the patron has a category with a type Child (C)
1443 =cut
1445 sub is_child {
1446 my( $self ) = @_;
1447 return $self->category->category_type eq 'C' ? 1 : 0;
1450 =head3 has_valid_userid
1452 my $patron = Koha::Patrons->find(42);
1453 $patron->userid( $new_userid );
1454 my $has_a_valid_userid = $patron->has_valid_userid
1456 my $patron = Koha::Patron->new( $params );
1457 my $has_a_valid_userid = $patron->has_valid_userid
1459 Return true if the current userid of this patron is valid/unique, otherwise false.
1461 Note that this should be done in $self->store instead and raise an exception if needed.
1463 =cut
1465 sub has_valid_userid {
1466 my ($self) = @_;
1468 return 0 unless $self->userid;
1470 return 0 if ( $self->userid eq C4::Context->config('user') ); # DB user
1472 my $already_exists = Koha::Patrons->search(
1474 userid => $self->userid,
1476 $self->in_storage
1477 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1478 : ()
1481 )->count;
1482 return $already_exists ? 0 : 1;
1485 =head3 generate_userid
1487 my $patron = Koha::Patron->new( $params );
1488 $patron->generate_userid
1490 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1492 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).
1494 =cut
1496 sub generate_userid {
1497 my ($self) = @_;
1498 my $offset = 0;
1499 my $firstname = $self->firstname // q{};
1500 my $surname = $self->surname // q{};
1501 #The script will "do" the following code and increment the $offset until the generated userid is unique
1502 do {
1503 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1504 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1505 my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1506 $userid = NFKD( $userid );
1507 $userid =~ s/\p{NonspacingMark}//g;
1508 $userid .= $offset unless $offset == 0;
1509 $self->userid( $userid );
1510 $offset++;
1511 } while (! $self->has_valid_userid );
1513 return $self;
1516 =head3 add_extended_attribute
1518 =cut
1520 sub add_extended_attribute {
1521 my ($self, $attribute) = @_;
1522 $attribute->{borrowernumber} = $self->borrowernumber;
1523 return Koha::Patron::Attribute->new($attribute)->store;
1526 =head3 extended_attributes
1528 Return object of Koha::Patron::Attributes type with all attributes set for this patron
1530 Or setter FIXME
1532 =cut
1534 sub extended_attributes {
1535 my ( $self, $attributes ) = @_;
1536 if ($attributes) { # setter
1537 my $schema = $self->_result->result_source->schema;
1538 $schema->txn_do(
1539 sub {
1540 # Remove the existing one
1541 $self->extended_attributes->filter_by_branch_limitations->delete;
1543 # Insert the new ones
1544 for my $attribute (@$attributes) {
1545 eval {
1546 $self->_result->create_related('borrower_attributes', $attribute);
1548 # FIXME We should:
1549 # 1 - Raise an exception
1550 # 2 - Execute in a transaction and don't save
1551 # or Insert anyway but display a message on the UI
1552 warn $@ if $@;
1558 my $rs = $self->_result->borrower_attributes;
1559 # We call search to use the filters in Koha::Patron::Attributes->search
1560 return Koha::Patron::Attributes->_new_from_dbic($rs)->search;
1563 =head3 lock
1565 Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
1567 Lock and optionally expire a patron account.
1568 Remove holds and article requests if remove flag set.
1569 In order to distinguish from locking by entering a wrong password, let's
1570 call this an administrative lockout.
1572 =cut
1574 sub lock {
1575 my ( $self, $params ) = @_;
1576 $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
1577 if( $params->{expire} ) {
1578 $self->dateexpiry( dt_from_string->subtract(days => 1) );
1580 $self->store;
1581 if( $params->{remove} ) {
1582 $self->holds->delete;
1583 $self->article_requests->delete;
1585 return $self;
1588 =head3 anonymize
1590 Koha::Patrons->find($id)->anonymize;
1592 Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
1593 are randomized, other personal data is cleared too.
1594 Patrons with issues are skipped.
1596 =cut
1598 sub anonymize {
1599 my ( $self ) = @_;
1600 if( $self->_result->issues->count ) {
1601 warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
1602 return;
1604 # Mandatory fields come from the corresponding pref, but email fields
1605 # are removed since scrambled email addresses only generate errors
1606 my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
1607 split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
1608 $mandatory->{userid} = 1; # needed since sub store does not clear field
1609 my @columns = $self->_result->result_source->columns;
1610 @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized/ } @columns;
1611 push @columns, 'dateofbirth'; # add this date back in
1612 foreach my $col (@columns) {
1613 $self->_anonymize_column($col, $mandatory->{lc $col} );
1615 $self->anonymized(1)->store;
1618 sub _anonymize_column {
1619 my ( $self, $col, $mandatory ) = @_;
1620 my $col_info = $self->_result->result_source->column_info($col);
1621 my $type = $col_info->{data_type};
1622 my $nullable = $col_info->{is_nullable};
1623 my $val;
1624 if( $type =~ /char|text/ ) {
1625 $val = $mandatory
1626 ? Koha::Token->new->generate({ pattern => '\w{10}' })
1627 : $nullable
1628 ? undef
1629 : q{};
1630 } elsif( $type =~ /integer|int$|float|dec|double/ ) {
1631 $val = $nullable ? undef : 0;
1632 } elsif( $type =~ /date|time/ ) {
1633 $val = $nullable ? undef : dt_from_string;
1635 $self->$col($val);
1638 =head3 add_guarantor
1640 my @relationships = $patron->add_guarantor(
1642 borrowernumber => $borrowernumber,
1643 relationships => $relationship,
1647 Adds a new guarantor to a patron.
1649 =cut
1651 sub add_guarantor {
1652 my ( $self, $params ) = @_;
1654 my $guarantor_id = $params->{guarantor_id};
1655 my $relationship = $params->{relationship};
1657 return Koha::Patron::Relationship->new(
1659 guarantee_id => $self->id,
1660 guarantor_id => $guarantor_id,
1661 relationship => $relationship
1663 )->store();
1666 =head3 get_extended_attribute
1668 my $attribute_value = $patron->get_extended_attribute( $code );
1670 Return the attribute for the code passed in parameter.
1672 It not exist it returns undef
1674 Note that this will not work for repeatable attribute types.
1676 Maybe you certainly not want to use this method, it is actually only used for SHOW_BARCODE
1677 (which should be a real patron's attribute (not extended)
1679 =cut
1681 sub get_extended_attribute {
1682 my ( $self, $code, $value ) = @_;
1683 my $rs = $self->_result->borrower_attributes;
1684 return unless $rs;
1685 my $attribute = $rs->search({ code => $code, ( $value ? ( attribute => $value ) : () ) });
1686 return unless $attribute->count;
1687 return $attribute->next;
1690 =head3 to_api
1692 my $json = $patron->to_api;
1694 Overloaded method that returns a JSON representation of the Koha::Patron object,
1695 suitable for API output.
1697 =cut
1699 sub to_api {
1700 my ( $self, $params ) = @_;
1702 my $json_patron = $self->SUPER::to_api( $params );
1704 $json_patron->{restricted} = ( $self->is_debarred )
1705 ? Mojo::JSON->true
1706 : Mojo::JSON->false;
1708 return $json_patron;
1711 =head3 to_api_mapping
1713 This method returns the mapping for representing a Koha::Patron object
1714 on the API.
1716 =cut
1718 sub to_api_mapping {
1719 return {
1720 borrowernotes => 'staff_notes',
1721 borrowernumber => 'patron_id',
1722 branchcode => 'library_id',
1723 categorycode => 'category_id',
1724 checkprevcheckout => 'check_previous_checkout',
1725 contactfirstname => undef, # Unused
1726 contactname => undef, # Unused
1727 contactnote => 'altaddress_notes',
1728 contacttitle => undef, # Unused
1729 dateenrolled => 'date_enrolled',
1730 dateexpiry => 'expiry_date',
1731 dateofbirth => 'date_of_birth',
1732 debarred => undef, # replaced by 'restricted'
1733 debarredcomment => undef, # calculated, API consumers will use /restrictions instead
1734 emailpro => 'secondary_email',
1735 flags => undef, # permissions manipulation handled in /permissions
1736 gonenoaddress => 'incorrect_address',
1737 guarantorid => 'guarantor_id',
1738 lastseen => 'last_seen',
1739 lost => 'patron_card_lost',
1740 opacnote => 'opac_notes',
1741 othernames => 'other_name',
1742 password => undef, # password manipulation handled in /password
1743 phonepro => 'secondary_phone',
1744 relationship => 'relationship_type',
1745 sex => 'gender',
1746 smsalertnumber => 'sms_number',
1747 sort1 => 'statistics_1',
1748 sort2 => 'statistics_2',
1749 autorenew_checkouts => 'autorenew_checkouts',
1750 streetnumber => 'street_number',
1751 streettype => 'street_type',
1752 zipcode => 'postal_code',
1753 B_address => 'altaddress_address',
1754 B_address2 => 'altaddress_address2',
1755 B_city => 'altaddress_city',
1756 B_country => 'altaddress_country',
1757 B_email => 'altaddress_email',
1758 B_phone => 'altaddress_phone',
1759 B_state => 'altaddress_state',
1760 B_streetnumber => 'altaddress_street_number',
1761 B_streettype => 'altaddress_street_type',
1762 B_zipcode => 'altaddress_postal_code',
1763 altcontactaddress1 => 'altcontact_address',
1764 altcontactaddress2 => 'altcontact_address2',
1765 altcontactaddress3 => 'altcontact_city',
1766 altcontactcountry => 'altcontact_country',
1767 altcontactfirstname => 'altcontact_firstname',
1768 altcontactphone => 'altcontact_phone',
1769 altcontactsurname => 'altcontact_surname',
1770 altcontactstate => 'altcontact_state',
1771 altcontactzipcode => 'altcontact_postal_code'
1775 =head2 Internal methods
1777 =head3 _type
1779 =cut
1781 sub _type {
1782 return 'Borrower';
1785 =head1 AUTHORS
1787 Kyle M Hall <kyle@bywatersolutions.com>
1788 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1789 Martin Renvoize <martin.renvoize@ptfs-europe.com>
1791 =cut