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 under the
9 # terms of the GNU General Public License as published by the Free Software
10 # Foundation; either version 3 of the License, or (at your option) any later
13 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
14 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License along
18 # with Koha; if not, write to the Free Software Foundation, Inc.,
19 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 use List
::MoreUtils
qw( any uniq );
25 use JSON
qw( to_json );
26 use Text
::Unaccent
qw( unac_string );
33 use Koha
::Club
::Enrollments
;
36 use Koha
::Exceptions
::Password
;
38 use Koha
::Old
::Checkouts
;
39 use Koha
::Patron
::Attributes
;
40 use Koha
::Patron
::Categories
;
41 use Koha
::Patron
::HouseboundProfile
;
42 use Koha
::Patron
::HouseboundRole
;
43 use Koha
::Patron
::Images
;
44 use Koha
::Patron
::Relationships
;
46 use Koha
::Subscription
::Routinglists
;
48 use Koha
::Virtualshelves
;
50 use base
qw(Koha::Object);
52 use constant ADMINISTRATIVE_LOCKOUT
=> -1;
54 our $RESULTSET_PATRON_ID_MAPPING = {
55 Accountline
=> 'borrowernumber',
56 Aqbasketuser
=> 'borrowernumber',
57 Aqbudget
=> 'budget_owner_id',
58 Aqbudgetborrower
=> 'borrowernumber',
59 ArticleRequest
=> 'borrowernumber',
60 BorrowerAttribute
=> 'borrowernumber',
61 BorrowerDebarment
=> 'borrowernumber',
62 BorrowerFile
=> 'borrowernumber',
63 BorrowerModification
=> 'borrowernumber',
64 ClubEnrollment
=> 'borrowernumber',
65 Issue
=> 'borrowernumber',
66 ItemsLastBorrower
=> 'borrowernumber',
67 Linktracker
=> 'borrowernumber',
68 Message
=> 'borrowernumber',
69 MessageQueue
=> 'borrowernumber',
70 OldIssue
=> 'borrowernumber',
71 OldReserve
=> 'borrowernumber',
72 Rating
=> 'borrowernumber',
73 Reserve
=> 'borrowernumber',
74 Review
=> 'borrowernumber',
75 SearchHistory
=> 'userid',
76 Statistic
=> 'borrowernumber',
77 Suggestion
=> 'suggestedby',
78 TagAll
=> 'borrowernumber',
79 Virtualshelfcontent
=> 'borrowernumber',
80 Virtualshelfshare
=> 'borrowernumber',
81 Virtualshelve
=> 'owner',
86 Koha::Patron - Koha Patron Object class
97 my ( $class, $params ) = @_;
99 return $class->SUPER::new
($params);
102 =head3 fixup_cardnumber
104 Autogenerate next cardnumber from highest value found in database
108 sub fixup_cardnumber
{
110 my $max = Koha
::Patrons
->search({
111 cardnumber
=> {-regexp
=> '^-?[0-9]+$'}
113 select => \'CAST
(cardnumber AS SIGNED
)',
114 as => ['cast_cardnumber
']
115 })->_resultset->get_column('cast_cardnumber
')->max;
116 $self->cardnumber(($max || 0) +1);
119 =head3 trim_whitespace
121 trim whitespace from data which has some non-whitespace in it.
122 Could be moved to Koha::Object if need to be reused
126 sub trim_whitespaces {
129 my $schema = Koha::Database->new->schema;
130 my @columns = $schema->source($self->_type)->columns;
132 for my $column( @columns ) {
133 my $value = $self->$column;
134 if ( defined $value ) {
135 $value =~ s/^\s*|\s*$//g;
136 $self->$column($value);
142 =head3 plain_text_password
144 $patron->plain_text_password( $password );
146 stores a copy of the unencrypted password in the object
147 for use in code before encrypting for db
151 sub plain_text_password {
152 my ( $self, $password ) = @_;
154 $self->{_plain_text_password} = $password;
157 return $self->{_plain_text_password}
158 if $self->{_plain_text_password};
165 Patron specific store method to cleanup record
166 and do other necessary things before saving
174 $self->_result->result_source->schema->txn_do(
177 C4::Context->preference("autoMemberNum")
178 and ( not defined $self->cardnumber
179 or $self->cardnumber eq '' )
182 # Warning: The caller is responsible for locking the members table in write
183 # mode, to avoid database corruption.
184 # We are in a transaction but the table is not locked
185 $self->fixup_cardnumber;
188 unless( $self->category->in_storage ) {
189 Koha::Exceptions::Object::FKConstraint->throw(
190 broken_fk => 'categorycode
',
191 value => $self->categorycode,
195 $self->trim_whitespaces;
197 # Set surname to uppercase if uppercasesurname is true
198 $self->surname( uc($self->surname) )
199 if C4::Context->preference("uppercasesurname");
201 unless ( $self->in_storage ) { #AddMember
203 # Generate a valid userid/login if needed
204 $self->generate_userid
205 if not $self->userid or not $self->has_valid_userid;
207 # Add expiration date if it isn't already there
208 unless ( $self->dateexpiry ) {
209 $self->dateexpiry( $self->category->get_expiry_date );
212 # Add enrollment date if it isn't already there
213 unless ( $self->dateenrolled ) {
214 $self->dateenrolled(dt_from_string
);
217 # Set the privacy depending on the patron's category
218 my $default_privacy = $self->category->default_privacy || q{};
220 $default_privacy eq 'default' ?
1
221 : $default_privacy eq 'never' ?
2
222 : $default_privacy eq 'forever' ?
0
224 $self->privacy($default_privacy);
227 # Make a copy of the plain text password for later use
228 $self->plain_text_password( $self->password );
230 # Create a disabled account if no password provided
231 $self->password( $self->password
232 ? Koha
::AuthUtils
::hash_password
( $self->password )
235 $self->borrowernumber(undef);
237 $self = $self->SUPER::store
;
239 $self->add_enrolment_fee_if_needed;
241 logaction
( "MEMBERS", "CREATE", $self->borrowernumber, "" )
242 if C4
::Context
->preference("BorrowersLog");
246 my $self_from_storage = $self->get_from_storage;
247 # FIXME We should not deal with that here, callers have to do this job
248 # Moved from ModMember to prevent regressions
249 unless ( $self->userid ) {
250 my $stored_userid = $self_from_storage->userid;
251 $self->userid($stored_userid);
254 # Password must be updated using $self->set_password
255 $self->password($self_from_storage->password);
257 if ( C4
::Context
->preference('FeeOnChangePatronCategory')
258 and $self->category->categorycode ne
259 $self_from_storage->category->categorycode )
261 $self->add_enrolment_fee_if_needed;
265 if ( C4
::Context
->preference("BorrowersLog") ) {
267 my $from_storage = $self_from_storage->unblessed;
268 my $from_object = $self->unblessed;
269 my @skip_fields = (qw
/lastseen updated_on/);
270 for my $key ( keys %{$from_storage} ) {
271 next if any
{ /$key/ } @skip_fields;
274 !defined( $from_storage->{$key} )
275 && defined( $from_object->{$key} )
277 || ( defined( $from_storage->{$key} )
278 && !defined( $from_object->{$key} ) )
280 defined( $from_storage->{$key} )
281 && defined( $from_object->{$key} )
282 && ( $from_storage->{$key} ne
283 $from_object->{$key} )
288 before
=> $from_storage->{$key},
289 after
=> $from_object->{$key}
294 if ( defined($info) ) {
298 $self->borrowernumber,
301 { utf8
=> 1, pretty
=> 1, canonical
=> 1 }
308 $self = $self->SUPER::store
;
319 Delete patron's holds, lists and finally the patron.
321 Lists owned by the borrower are deleted, but entries from the borrower to
322 other lists are kept.
330 $self->_result->result_source->schema->txn_do(
332 # Cancel Patron's holds
333 my $holds = $self->holds;
334 while( my $hold = $holds->next ){
338 # Delete all lists and all shares of this borrower
339 # Consistent with the approach Koha uses on deleting individual lists
340 # Note that entries in virtualshelfcontents added by this borrower to
341 # lists of others will be handled by a table constraint: the borrower
342 # is set to NULL in those entries.
344 # We could handle the above deletes via a constraint too.
345 # But a new BZ report 11889 has been opened to discuss another approach.
346 # Instead of deleting we could also disown lists (based on a pref).
347 # In that way we could save shared and public lists.
348 # The current table constraints support that idea now.
349 # This pref should then govern the results of other routines/methods such as
350 # Koha::Virtualshelf->new->delete too.
351 # FIXME Could be $patron->get_lists
352 $_->delete for Koha
::Virtualshelves
->search( { owner
=> $self->borrowernumber } );
354 $deleted = $self->SUPER::delete;
356 logaction
( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4
::Context
->preference("BorrowersLog");
365 my $patron_category = $patron->category
367 Return the patron category for this patron
373 return Koha
::Patron
::Category
->_new_from_dbic( $self->_result->categorycode );
383 return scalar Koha
::Patron
::Images
->find( $self->borrowernumber );
388 Returns a Koha::Library object representing the patron's home library.
394 return Koha
::Library
->_new_from_dbic($self->_result->branchcode);
397 =head3 guarantor_relationships
399 Returns Koha::Patron::Relationships object for this patron's guarantors
401 Returns the set of relationships for the patrons that are guarantors for this patron.
403 This is returned instead of a Koha::Patron object because the guarantor
404 may not exist as a patron in Koha. If this is true, the guarantors name
405 exists in the Koha::Patron::Relationship object and will have no guarantor_id.
409 sub guarantor_relationships
{
412 return Koha
::Patron
::Relationships
->search( { guarantee_id
=> $self->id } );
415 =head3 guarantee_relationships
417 Returns Koha::Patron::Relationships object for this patron's guarantors
419 Returns the set of relationships for the patrons that are guarantees for this patron.
421 The method returns Koha::Patron::Relationship objects for the sake
422 of consistency with the guantors method.
423 A guarantee by definition must exist as a patron in Koha.
427 sub guarantee_relationships
{
430 return Koha
::Patron
::Relationships
->search(
431 { guarantor_id
=> $self->id },
433 prefetch
=> 'guarantee',
434 order_by
=> { -asc
=> [ 'guarantee.surname', 'guarantee.firstname' ] },
439 =head3 housebound_profile
441 Returns the HouseboundProfile associated with this patron.
445 sub housebound_profile
{
447 my $profile = $self->_result->housebound_profile;
448 return Koha
::Patron
::HouseboundProfile
->_new_from_dbic($profile)
453 =head3 housebound_role
455 Returns the HouseboundRole associated with this patron.
459 sub housebound_role
{
462 my $role = $self->_result->housebound_role;
463 return Koha
::Patron
::HouseboundRole
->_new_from_dbic($role) if ( $role );
469 Returns the siblings of this patron.
476 my @guarantors = $self->guarantor_relationships()->guarantors();
478 return unless @guarantors;
481 map { $_->guarantee_relationships()->guarantees() } @guarantors;
483 return unless @siblings;
487 grep { !$seen{ $_->id }++ && ( $_->id != $self->id ) } @siblings;
489 return wantarray ?
@siblings : Koha
::Patrons
->search( { borrowernumber
=> { -in => [ map { $_->id } @siblings ] } } );
494 my $patron = Koha::Patrons->find($id);
495 $patron->merge_with( \@patron_ids );
497 This subroutine merges a list of patrons into the patron record. This is accomplished by finding
498 all related patron ids for the patrons to be merged in other tables and changing the ids to be that
499 of the keeper patron.
504 my ( $self, $patron_ids ) = @_;
506 my @patron_ids = @
{ $patron_ids };
508 # Ensure the keeper isn't in the list of patrons to merge
509 @patron_ids = grep { $_ ne $self->id } @patron_ids;
511 my $schema = Koha
::Database
->new()->schema();
515 $self->_result->result_source->schema->txn_do( sub {
516 foreach my $patron_id (@patron_ids) {
517 my $patron = Koha
::Patrons
->find( $patron_id );
521 # Unbless for safety, the patron will end up being deleted
522 $results->{merged
}->{$patron_id}->{patron
} = $patron->unblessed;
524 while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
525 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
526 $results->{merged
}->{ $patron_id }->{updated
}->{$r} = $rs->count();
527 $rs->update({ $field => $self->id });
530 $patron->move_to_deleted();
540 =head3 wants_check_for_previous_checkout
542 $wants_check = $patron->wants_check_for_previous_checkout;
544 Return 1 if Koha needs to perform PrevIssue checking, else 0.
548 sub wants_check_for_previous_checkout
{
550 my $syspref = C4
::Context
->preference("checkPrevCheckout");
553 ## Hard syspref trumps all
554 return 1 if ($syspref eq 'hardyes');
555 return 0 if ($syspref eq 'hardno');
556 ## Now, patron pref trumps all
557 return 1 if ($self->checkprevcheckout eq 'yes');
558 return 0 if ($self->checkprevcheckout eq 'no');
560 # More complex: patron inherits -> determine category preference
561 my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
562 return 1 if ($checkPrevCheckoutByCat eq 'yes');
563 return 0 if ($checkPrevCheckoutByCat eq 'no');
565 # Finally: category preference is inherit, default to 0
566 if ($syspref eq 'softyes') {
573 =head3 do_check_for_previous_checkout
575 $do_check = $patron->do_check_for_previous_checkout($item);
577 Return 1 if the bib associated with $ITEM has previously been checked out to
578 $PATRON, 0 otherwise.
582 sub do_check_for_previous_checkout
{
583 my ( $self, $item ) = @_;
586 my $biblio = Koha
::Biblios
->find( $item->{biblionumber
} );
587 if ( $biblio->is_serial ) {
588 push @item_nos, $item->{itemnumber
};
590 # Get all itemnumbers for given bibliographic record.
591 @item_nos = $biblio->items->get_column( 'itemnumber' );
594 # Create (old)issues search criteria
596 borrowernumber
=> $self->borrowernumber,
597 itemnumber
=> \
@item_nos,
600 # Check current issues table
601 my $issues = Koha
::Checkouts
->search($criteria);
602 return 1 if $issues->count; # 0 || N
604 # Check old issues table
605 my $old_issues = Koha
::Old
::Checkouts
->search($criteria);
606 return $old_issues->count; # 0 || N
611 my $debarment_expiration = $patron->is_debarred;
613 Returns the date a patron debarment will expire, or undef if the patron is not
621 return unless $self->debarred;
622 return $self->debarred
623 if $self->debarred =~ '^9999'
624 or dt_from_string
( $self->debarred ) > dt_from_string
;
630 my $is_expired = $patron->is_expired;
632 Returns 1 if the patron is expired or 0;
638 return 0 unless $self->dateexpiry;
639 return 0 if $self->dateexpiry =~ '^9999';
640 return 1 if dt_from_string
( $self->dateexpiry ) < dt_from_string
->truncate( to
=> 'day' );
644 =head3 is_going_to_expire
646 my $is_going_to_expire = $patron->is_going_to_expire;
648 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
652 sub is_going_to_expire
{
655 my $delay = C4
::Context
->preference('NotifyBorrowerDeparture') || 0;
657 return 0 unless $delay;
658 return 0 unless $self->dateexpiry;
659 return 0 if $self->dateexpiry =~ '^9999';
660 return 1 if dt_from_string
( $self->dateexpiry, undef, 'floating' )->subtract( days
=> $delay ) < dt_from_string
(undef, undef, 'floating')->truncate( to
=> 'day' );
666 $patron->set_password({ password => $plain_text_password [, skip_validation => 1 ] });
668 Set the patron's password.
672 The passed string is validated against the current password enforcement policy.
673 Validation can be skipped by passing the I<skip_validation> parameter.
675 Exceptions are thrown if the password is not good enough.
679 =item Koha::Exceptions::Password::TooShort
681 =item Koha::Exceptions::Password::WhitespaceCharacters
683 =item Koha::Exceptions::Password::TooWeak
690 my ( $self, $args ) = @_;
692 my $password = $args->{password
};
694 unless ( $args->{skip_validation
} ) {
695 my ( $is_valid, $error ) = Koha
::AuthUtils
::is_password_valid
( $password );
698 if ( $error eq 'too_short' ) {
699 my $min_length = C4
::Context
->preference('minPasswordLength');
700 $min_length = 3 if not $min_length or $min_length < 3;
702 my $password_length = length($password);
703 Koha
::Exceptions
::Password
::TooShort
->throw(
704 length => $password_length, min_length
=> $min_length );
706 elsif ( $error eq 'has_whitespaces' ) {
707 Koha
::Exceptions
::Password
::WhitespaceCharacters
->throw();
709 elsif ( $error eq 'too_weak' ) {
710 Koha
::Exceptions
::Password
::TooWeak
->throw();
715 my $digest = Koha
::AuthUtils
::hash_password
($password);
717 { password
=> $digest,
722 logaction
( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" )
723 if C4
::Context
->preference("BorrowersLog");
731 my $new_expiry_date = $patron->renew_account
733 Extending the subscription to the expiry date.
740 if ( C4
::Context
->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
741 $date = ( dt_from_string
gt dt_from_string
( $self->dateexpiry ) ) ? dt_from_string
: dt_from_string
( $self->dateexpiry );
744 C4
::Context
->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
745 ? dt_from_string
( $self->dateexpiry )
748 my $expiry_date = $self->category->get_expiry_date($date);
750 $self->dateexpiry($expiry_date);
751 $self->date_renewed( dt_from_string
() );
754 $self->add_enrolment_fee_if_needed;
756 logaction
( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4
::Context
->preference("BorrowersLog");
757 return dt_from_string
( $expiry_date )->truncate( to
=> 'day' );
762 my $has_overdues = $patron->has_overdues;
764 Returns the number of patron's overdues
770 my $dtf = Koha
::Database
->new->schema->storage->datetime_parser;
771 return $self->_result->issues->search({ date_due
=> { '<' => $dtf->format_datetime( dt_from_string
() ) } })->count;
776 $patron->track_login;
777 $patron->track_login({ force => 1 });
779 Tracks a (successful) login attempt.
780 The preference TrackLastPatronActivity must be enabled. Or you
781 should pass the force parameter.
786 my ( $self, $params ) = @_;
789 !C4
::Context
->preference('TrackLastPatronActivity');
790 $self->lastseen( dt_from_string
() )->store;
793 =head3 move_to_deleted
795 my $is_moved = $patron->move_to_deleted;
797 Move a patron to the deletedborrowers table.
798 This can be done before deleting a patron, to make sure the data are not completely deleted.
802 sub move_to_deleted
{
804 my $patron_infos = $self->unblessed;
805 delete $patron_infos->{updated_on
}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
806 return Koha
::Database
->new->schema->resultset('Deletedborrower')->create($patron_infos);
809 =head3 article_requests
811 my @requests = $borrower->article_requests();
812 my $requests = $borrower->article_requests();
814 Returns either a list of ArticleRequests objects,
815 or an ArtitleRequests object, depending on the
820 sub article_requests
{
823 $self->{_article_requests
} ||= Koha
::ArticleRequests
->search({ borrowernumber
=> $self->borrowernumber() });
825 return $self->{_article_requests
};
828 =head3 article_requests_current
830 my @requests = $patron->article_requests_current
832 Returns the article requests associated with this patron that are incomplete
836 sub article_requests_current
{
839 $self->{_article_requests_current
} ||= Koha
::ArticleRequests
->search(
841 borrowernumber
=> $self->id(),
843 { status
=> Koha
::ArticleRequest
::Status
::Pending
},
844 { status
=> Koha
::ArticleRequest
::Status
::Processing
}
849 return $self->{_article_requests_current
};
852 =head3 article_requests_finished
854 my @requests = $biblio->article_requests_finished
856 Returns the article requests associated with this patron that are completed
860 sub article_requests_finished
{
861 my ( $self, $borrower ) = @_;
863 $self->{_article_requests_finished
} ||= Koha
::ArticleRequests
->search(
865 borrowernumber
=> $self->id(),
867 { status
=> Koha
::ArticleRequest
::Status
::Completed
},
868 { status
=> Koha
::ArticleRequest
::Status
::Canceled
}
873 return $self->{_article_requests_finished
};
876 =head3 add_enrolment_fee_if_needed
878 my $enrolment_fee = $patron->add_enrolment_fee_if_needed;
880 Add enrolment fee for a patron if needed.
884 sub add_enrolment_fee_if_needed
{
886 my $enrolment_fee = $self->category->enrolmentfee;
887 if ( $enrolment_fee && $enrolment_fee > 0 ) {
888 $self->account->add_debit(
890 amount
=> $enrolment_fee,
891 user_id
=> C4
::Context
->userenv ? C4
::Context
->userenv->{'number'} : undef,
892 interface
=> C4
::Context
->interface,
893 library_id
=> C4
::Context
->userenv ? C4
::Context
->userenv->{'branch'} : undef,
898 return $enrolment_fee || 0;
903 my $checkouts = $patron->checkouts
909 my $checkouts = $self->_result->issues;
910 return Koha
::Checkouts
->_new_from_dbic( $checkouts );
913 =head3 pending_checkouts
915 my $pending_checkouts = $patron->pending_checkouts
917 This method will return the same as $self->checkouts, but with a prefetch on
918 items, biblio and biblioitems.
920 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
922 It should not be used directly, prefer to access fields you need instead of
923 retrieving all these fields in one go.
927 sub pending_checkouts
{
929 my $checkouts = $self->_result->issues->search(
933 { -desc
=> 'me.timestamp' },
934 { -desc
=> 'issuedate' },
935 { -desc
=> 'issue_id' }, # Sort by issue_id should be enough
937 prefetch
=> { item
=> { biblio
=> 'biblioitems' } },
940 return Koha
::Checkouts
->_new_from_dbic( $checkouts );
945 my $old_checkouts = $patron->old_checkouts
951 my $old_checkouts = $self->_result->old_issues;
952 return Koha
::Old
::Checkouts
->_new_from_dbic( $old_checkouts );
957 my $overdue_items = $patron->get_overdues
959 Return the overdue items
965 my $dtf = Koha
::Database
->new->schema->storage->datetime_parser;
966 return $self->checkouts->search(
968 'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string
) },
971 prefetch
=> { item
=> { biblio
=> 'biblioitems' } },
976 =head3 get_routing_lists
978 my @routinglists = $patron->get_routing_lists
980 Returns the routing lists a patron is subscribed to.
984 sub get_routing_lists
{
986 my $routing_list_rs = $self->_result->subscriptionroutinglists;
987 return Koha
::Subscription
::Routinglists
->_new_from_dbic($routing_list_rs);
992 my $age = $patron->get_age
994 Return the age of the patron
1000 my $today_str = dt_from_string
->strftime("%Y-%m-%d");
1001 return unless $self->dateofbirth;
1002 my $dob_str = dt_from_string
( $self->dateofbirth )->strftime("%Y-%m-%d");
1004 my ( $dob_y, $dob_m, $dob_d ) = split /-/, $dob_str;
1005 my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
1007 my $age = $today_y - $dob_y;
1008 if ( $dob_m . $dob_d > $today_m . $today_d ) {
1017 my $is_valid = $patron->is_valid_age
1019 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
1025 my $age = $self->get_age;
1027 my $patroncategory = $self->category;
1028 my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
1030 return (defined($age) && (($high && ($age > $high)) or ($age < $low))) ?
0 : 1;
1035 my $account = $patron->account
1041 return Koha
::Account
->new( { patron_id
=> $self->borrowernumber } );
1046 my $holds = $patron->holds
1048 Return all the holds placed by this patron
1054 my $holds_rs = $self->_result->reserves->search( {}, { order_by
=> 'reservedate' } );
1055 return Koha
::Holds
->_new_from_dbic($holds_rs);
1060 my $old_holds = $patron->old_holds
1062 Return all the historical holds for this patron
1068 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by
=> 'reservedate' } );
1069 return Koha
::Old
::Holds
->_new_from_dbic($old_holds_rs);
1072 =head3 notice_email_address
1074 my $email = $patron->notice_email_address;
1076 Return the email address of patron used for notices.
1077 Returns the empty string if no email address.
1081 sub notice_email_address
{
1084 my $which_address = C4
::Context
->preference("AutoEmailPrimaryAddress");
1085 # if syspref is set to 'first valid' (value == OFF), look up email address
1086 if ( $which_address eq 'OFF' ) {
1087 return $self->first_valid_email_address;
1090 return $self->$which_address || '';
1093 =head3 first_valid_email_address
1095 my $first_valid_email_address = $patron->first_valid_email_address
1097 Return the first valid email address for a patron.
1098 For now, the order is defined as email, emailpro, B_email.
1099 Returns the empty string if the borrower has no email addresses.
1103 sub first_valid_email_address
{
1106 return $self->email() || $self->emailpro() || $self->B_email() || q{};
1109 =head3 get_club_enrollments
1113 sub get_club_enrollments
{
1114 my ( $self, $return_scalar ) = @_;
1116 my $e = Koha
::Club
::Enrollments
->search( { borrowernumber
=> $self->borrowernumber(), date_canceled
=> undef } );
1118 return $e if $return_scalar;
1120 return wantarray ?
$e->as_list : $e;
1123 =head3 get_enrollable_clubs
1127 sub get_enrollable_clubs
{
1128 my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
1131 $params->{is_enrollable_from_opac
} = $is_enrollable_from_opac
1132 if $is_enrollable_from_opac;
1133 $params->{is_email_required
} = 0 unless $self->first_valid_email_address();
1135 $params->{borrower
} = $self;
1137 my $e = Koha
::Clubs
->get_enrollable($params);
1139 return $e if $return_scalar;
1141 return wantarray ?
$e->as_list : $e;
1144 =head3 account_locked
1146 my $is_locked = $patron->account_locked
1148 Return true if the patron has reached the maximum number of login attempts
1149 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1150 as an administrative lockout (independent of FailedLoginAttempts; see also
1151 Koha::Patron->lock).
1152 Otherwise return false.
1153 If the pref is not set (empty string, null or 0), the feature is considered as
1158 sub account_locked
{
1160 my $FailedLoginAttempts = C4
::Context
->preference('FailedLoginAttempts');
1161 return 1 if $FailedLoginAttempts
1162 and $self->login_attempts
1163 and $self->login_attempts >= $FailedLoginAttempts;
1164 return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1168 =head3 can_see_patron_infos
1170 my $can_see = $patron->can_see_patron_infos( $patron );
1172 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1176 sub can_see_patron_infos
{
1177 my ( $self, $patron ) = @_;
1178 return unless $patron;
1179 return $self->can_see_patrons_from( $patron->library->branchcode );
1182 =head3 can_see_patrons_from
1184 my $can_see = $patron->can_see_patrons_from( $branchcode );
1186 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1190 sub can_see_patrons_from
{
1191 my ( $self, $branchcode ) = @_;
1193 if ( $self->branchcode eq $branchcode ) {
1195 } elsif ( $self->has_permission( { borrowers
=> 'view_borrower_infos_from_any_libraries' } ) ) {
1197 } elsif ( my $library_groups = $self->library->library_groups ) {
1198 while ( my $library_group = $library_groups->next ) {
1199 if ( $library_group->parent->has_child( $branchcode ) ) {
1208 =head3 libraries_where_can_see_patrons
1210 my $libraries = $patron-libraries_where_can_see_patrons;
1212 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1213 The branchcodes are arbitrarily returned sorted.
1214 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1216 An empty array means no restriction, the patron can see patron's infos from any libraries.
1220 sub libraries_where_can_see_patrons
{
1222 my $userenv = C4
::Context
->userenv;
1224 return () unless $userenv; # For tests, but userenv should be defined in tests...
1226 my @restricted_branchcodes;
1227 if (C4
::Context
::only_my_library
) {
1228 push @restricted_branchcodes, $self->branchcode;
1232 $self->has_permission(
1233 { borrowers
=> 'view_borrower_infos_from_any_libraries' }
1237 my $library_groups = $self->library->library_groups({ ft_hide_patron_info
=> 1 });
1238 if ( $library_groups->count )
1240 while ( my $library_group = $library_groups->next ) {
1241 my $parent = $library_group->parent;
1242 if ( $parent->has_child( $self->branchcode ) ) {
1243 push @restricted_branchcodes, $parent->children->get_column('branchcode');
1248 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1252 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1253 @restricted_branchcodes = uniq
(@restricted_branchcodes);
1254 @restricted_branchcodes = sort(@restricted_branchcodes);
1255 return @restricted_branchcodes;
1258 sub has_permission
{
1259 my ( $self, $flagsrequired ) = @_;
1260 return unless $self->userid;
1261 # TODO code from haspermission needs to be moved here!
1262 return C4
::Auth
::haspermission
( $self->userid, $flagsrequired );
1267 my $is_adult = $patron->is_adult
1269 Return true if the patron has a category with a type Adult (A) or Organization (I)
1275 return $self->category->category_type =~ /^(A|I)$/ ?
1 : 0;
1280 my $is_child = $patron->is_child
1282 Return true if the patron has a category with a type Child (C)
1288 return $self->category->category_type eq 'C' ?
1 : 0;
1291 =head3 has_valid_userid
1293 my $patron = Koha::Patrons->find(42);
1294 $patron->userid( $new_userid );
1295 my $has_a_valid_userid = $patron->has_valid_userid
1297 my $patron = Koha::Patron->new( $params );
1298 my $has_a_valid_userid = $patron->has_valid_userid
1300 Return true if the current userid of this patron is valid/unique, otherwise false.
1302 Note that this should be done in $self->store instead and raise an exception if needed.
1306 sub has_valid_userid
{
1309 return 0 unless $self->userid;
1311 return 0 if ( $self->userid eq C4
::Context
->config('user') ); # DB user
1313 my $already_exists = Koha
::Patrons
->search(
1315 userid
=> $self->userid,
1318 ?
( borrowernumber
=> { '!=' => $self->borrowernumber } )
1323 return $already_exists ?
0 : 1;
1326 =head3 generate_userid
1328 my $patron = Koha::Patron->new( $params );
1329 $patron->generate_userid
1331 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1333 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).
1337 sub generate_userid
{
1340 my $firstname = $self->firstname // q{};
1341 my $surname = $self->surname // q{};
1342 #The script will "do" the following code and increment the $offset until the generated userid is unique
1344 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1345 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1346 my $userid = lc(($firstname)?
"$firstname.$surname" : $surname);
1347 $userid = unac_string
('utf-8',$userid);
1348 $userid .= $offset unless $offset == 0;
1349 $self->userid( $userid );
1351 } while (! $self->has_valid_userid );
1359 my $attributes = $patron->attributes
1361 Return object of Koha::Patron::Attributes type with all attributes set for this patron
1367 return Koha
::Patron
::Attributes
->search({
1368 borrowernumber
=> $self->borrowernumber,
1369 branchcode
=> $self->branchcode,
1375 Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
1377 Lock and optionally expire a patron account.
1378 Remove holds and article requests if remove flag set.
1379 In order to distinguish from locking by entering a wrong password, let's
1380 call this an administrative lockout.
1385 my ( $self, $params ) = @_;
1386 $self->login_attempts( ADMINISTRATIVE_LOCKOUT
);
1387 if( $params->{expire
} ) {
1388 $self->dateexpiry( dt_from_string
->subtract(days
=> 1) );
1391 if( $params->{remove
} ) {
1392 $self->holds->delete;
1393 $self->article_requests->delete;
1400 Koha::Patrons->find($id)->anonymize;
1402 Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
1403 are randomized, other personal data is cleared too.
1404 Patrons with issues are skipped.
1410 if( $self->_result->issues->count ) {
1411 warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
1414 # Mandatory fields come from the corresponding pref, but email fields
1415 # are removed since scrambled email addresses only generate errors
1416 my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
1417 split /\s*\|\s*/, C4
::Context
->preference('BorrowerMandatoryField') };
1418 $mandatory->{userid
} = 1; # needed since sub store does not clear field
1419 my @columns = $self->_result->result_source->columns;
1420 @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized/ } @columns;
1421 push @columns, 'dateofbirth'; # add this date back in
1422 foreach my $col (@columns) {
1423 $self->_anonymize_column($col, $mandatory->{lc $col} );
1425 $self->anonymized(1)->store;
1428 sub _anonymize_column
{
1429 my ( $self, $col, $mandatory ) = @_;
1430 my $col_info = $self->_result->result_source->column_info($col);
1431 my $type = $col_info->{data_type
};
1432 my $nullable = $col_info->{is_nullable
};
1434 if( $type =~ /char|text/ ) {
1436 ? Koha
::Token
->new->generate({ pattern
=> '\w{10}' })
1440 } elsif( $type =~ /integer|int$|float|dec|double/ ) {
1441 $val = $nullable ?
undef : 0;
1442 } elsif( $type =~ /date|time/ ) {
1443 $val = $nullable ?
undef : dt_from_string
;
1448 =head3 add_guarantor
1450 my @relationships = $patron->add_guarantor(
1452 borrowernumber => $borrowernumber,
1453 relationships => $relationship,
1457 Adds a new guarantor to a patron.
1462 my ( $self, $params ) = @_;
1464 my $guarantor_id = $params->{guarantor_id
};
1465 my $relationship = $params->{relationship
};
1467 return Koha
::Patron
::Relationship
->new(
1469 guarantee_id
=> $self->id,
1470 guarantor_id
=> $guarantor_id,
1471 relationship
=> $relationship
1476 =head2 Internal methods
1488 Kyle M Hall <kyle@bywatersolutions.com>
1489 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1490 Martin Renvoize <martin.renvoize@ptfs-europe.com>