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 );
35 use Koha
::Exceptions
::Password
;
37 use Koha
::Old
::Checkouts
;
38 use Koha
::Patron
::Categories
;
39 use Koha
::Patron
::HouseboundProfile
;
40 use Koha
::Patron
::HouseboundRole
;
41 use Koha
::Patron
::Images
;
43 use Koha
::Virtualshelves
;
44 use Koha
::Club
::Enrollments
;
46 use Koha
::Subscription
::Routinglists
;
48 use base
qw(Koha::Object);
50 our $RESULTSET_PATRON_ID_MAPPING = {
51 Accountline
=> 'borrowernumber',
52 Aqbasketuser
=> 'borrowernumber',
53 Aqbudget
=> 'budget_owner_id',
54 Aqbudgetborrower
=> 'borrowernumber',
55 ArticleRequest
=> 'borrowernumber',
56 BorrowerAttribute
=> 'borrowernumber',
57 BorrowerDebarment
=> 'borrowernumber',
58 BorrowerFile
=> 'borrowernumber',
59 BorrowerModification
=> 'borrowernumber',
60 ClubEnrollment
=> 'borrowernumber',
61 Issue
=> 'borrowernumber',
62 ItemsLastBorrower
=> 'borrowernumber',
63 Linktracker
=> 'borrowernumber',
64 Message
=> 'borrowernumber',
65 MessageQueue
=> 'borrowernumber',
66 OldIssue
=> 'borrowernumber',
67 OldReserve
=> 'borrowernumber',
68 Rating
=> 'borrowernumber',
69 Reserve
=> 'borrowernumber',
70 Review
=> 'borrowernumber',
71 SearchHistory
=> 'userid',
72 Statistic
=> 'borrowernumber',
73 Suggestion
=> 'suggestedby',
74 TagAll
=> 'borrowernumber',
75 Virtualshelfcontent
=> 'borrowernumber',
76 Virtualshelfshare
=> 'borrowernumber',
77 Virtualshelve
=> 'owner',
82 Koha::Patron - Koha Patron Object class
95 my ( $class, $params ) = @_;
97 return $class->SUPER::new
($params);
100 =head3 fixup_cardnumber
102 Autogenerate next cardnumber from highest value found in database
106 sub fixup_cardnumber
{
108 my $max = Koha
::Patrons
->search({
109 cardnumber
=> {-regexp
=> '^-?[0-9]+$'}
111 select => \'CAST
(cardnumber AS SIGNED
)',
112 as => ['cast_cardnumber
']
113 })->_resultset->get_column('cast_cardnumber
')->max;
114 $self->cardnumber(($max || 0) +1);
117 =head3 trim_whitespace
119 trim whitespace from data which has some non-whitespace in it.
120 Could be moved to Koha::Object if need to be reused
124 sub trim_whitespaces {
127 my $schema = Koha::Database->new->schema;
128 my @columns = $schema->source($self->_type)->columns;
130 for my $column( @columns ) {
131 my $value = $self->$column;
132 if ( defined $value ) {
133 $value =~ s/^\s*|\s*$//g;
134 $self->$column($value);
140 =head3 plain_text_password
142 $patron->plain_text_password( $password );
144 stores a copy of the unencrypted password in the object
145 for use in code before encrypting for db
149 sub plain_text_password {
150 my ( $self, $password ) = @_;
152 $self->{_plain_text_password} = $password;
155 return $self->{_plain_text_password}
156 if $self->{_plain_text_password};
163 Patron specific store method to cleanup record
164 and do other necessary things before saving
172 $self->_result->result_source->schema->txn_do(
175 C4::Context->preference("autoMemberNum")
176 and ( not defined $self->cardnumber
177 or $self->cardnumber eq '' )
180 # Warning: The caller is responsible for locking the members table in write
181 # mode, to avoid database corruption.
182 # We are in a transaction but the table is not locked
183 $self->fixup_cardnumber;
186 unless( $self->category->in_storage ) {
187 Koha::Exceptions::Object::FKConstraint->throw(
188 broken_fk => 'categorycode
',
189 value => $self->categorycode,
193 $self->trim_whitespaces;
195 unless ( $self->in_storage ) { #AddMember
197 # Generate a valid userid/login if needed
198 $self->generate_userid
199 if not $self->userid or not $self->has_valid_userid;
201 # Add expiration date if it isn't already there
202 unless ( $self->dateexpiry ) {
203 $self->dateexpiry( $self->category->get_expiry_date );
206 # Add enrollment date if it isn't already there
207 unless ( $self->dateenrolled ) {
208 $self->dateenrolled(dt_from_string
);
211 # Set the privacy depending on the patron's category
212 my $default_privacy = $self->category->default_privacy || q{};
214 $default_privacy eq 'default' ?
1
215 : $default_privacy eq 'never' ?
2
216 : $default_privacy eq 'forever' ?
0
218 $self->privacy($default_privacy);
221 # Make a copy of the plain text password for later use
222 $self->plain_text_password( $self->password );
224 # Create a disabled account if no password provided
225 $self->password( $self->password
226 ? Koha
::AuthUtils
::hash_password
( $self->password )
229 $self->borrowernumber(undef);
231 $self = $self->SUPER::store
;
233 $self->add_enrolment_fee_if_needed;
235 logaction
( "MEMBERS", "CREATE", $self->borrowernumber, "" )
236 if C4
::Context
->preference("BorrowersLog");
240 my $self_from_storage = $self->get_from_storage;
241 # FIXME We should not deal with that here, callers have to do this job
242 # Moved from ModMember to prevent regressions
243 unless ( $self->userid ) {
244 my $stored_userid = $self_from_storage->userid;
245 $self->userid($stored_userid);
248 # Password must be updated using $self->set_password
249 $self->password($self_from_storage->password);
251 if ( C4
::Context
->preference('FeeOnChangePatronCategory')
252 and $self->category->categorycode ne
253 $self_from_storage->category->categorycode )
255 $self->add_enrolment_fee_if_needed;
259 if ( C4
::Context
->preference("BorrowersLog") ) {
261 my $from_storage = $self_from_storage->unblessed;
262 my $from_object = $self->unblessed;
263 my @skip_fields = (qw
/lastseen/);
264 for my $key ( keys %{$from_storage} ) {
265 next if any
{ /$key/ } @skip_fields;
268 !defined( $from_storage->{$key} )
269 && defined( $from_object->{$key} )
271 || ( defined( $from_storage->{$key} )
272 && !defined( $from_object->{$key} ) )
274 defined( $from_storage->{$key} )
275 && defined( $from_object->{$key} )
276 && ( $from_storage->{$key} ne
277 $from_object->{$key} )
282 before
=> $from_storage->{$key},
283 after
=> $from_object->{$key}
288 if ( defined($info) ) {
292 $self->borrowernumber,
295 { utf8
=> 1, pretty
=> 1, canonical
=> 1 }
302 $self = $self->SUPER::store
;
313 Delete patron's holds, lists and finally the patron.
315 Lists owned by the borrower are deleted, but entries from the borrower to
316 other lists are kept.
324 $self->_result->result_source->schema->txn_do(
326 # Delete Patron's holds
327 $self->holds->delete;
329 # Delete all lists and all shares of this borrower
330 # Consistent with the approach Koha uses on deleting individual lists
331 # Note that entries in virtualshelfcontents added by this borrower to
332 # lists of others will be handled by a table constraint: the borrower
333 # is set to NULL in those entries.
335 # We could handle the above deletes via a constraint too.
336 # But a new BZ report 11889 has been opened to discuss another approach.
337 # Instead of deleting we could also disown lists (based on a pref).
338 # In that way we could save shared and public lists.
339 # The current table constraints support that idea now.
340 # This pref should then govern the results of other routines/methods such as
341 # Koha::Virtualshelf->new->delete too.
342 # FIXME Could be $patron->get_lists
343 $_->delete for Koha
::Virtualshelves
->search( { owner
=> $self->borrowernumber } );
345 $deleted = $self->SUPER::delete;
347 logaction
( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4
::Context
->preference("BorrowersLog");
356 my $patron_category = $patron->category
358 Return the patron category for this patron
364 return Koha
::Patron
::Category
->_new_from_dbic( $self->_result->categorycode );
369 Returns a Koha::Patron object for this patron's guarantor
376 return unless $self->guarantorid();
378 return Koha
::Patrons
->find( $self->guarantorid() );
384 return scalar Koha
::Patron
::Images
->find( $self->borrowernumber );
389 return Koha
::Library
->_new_from_dbic($self->_result->branchcode);
394 Returns the guarantees (list of Koha::Patron) of this patron
401 return Koha
::Patrons
->search( { guarantorid
=> $self->borrowernumber }, { order_by
=> { -asc
=> ['surname','firstname'] } } );
404 =head3 housebound_profile
406 Returns the HouseboundProfile associated with this patron.
410 sub housebound_profile
{
412 my $profile = $self->_result->housebound_profile;
413 return Koha
::Patron
::HouseboundProfile
->_new_from_dbic($profile)
418 =head3 housebound_role
420 Returns the HouseboundRole associated with this patron.
424 sub housebound_role
{
427 my $role = $self->_result->housebound_role;
428 return Koha
::Patron
::HouseboundRole
->_new_from_dbic($role) if ( $role );
434 Returns the siblings of this patron.
441 my $guarantor = $self->guarantor;
443 return unless $guarantor;
445 return Koha
::Patrons
->search(
449 '=' => $guarantor->id,
452 '!=' => $self->borrowernumber,
460 my $patron = Koha::Patrons->find($id);
461 $patron->merge_with( \@patron_ids );
463 This subroutine merges a list of patrons into the patron record. This is accomplished by finding
464 all related patron ids for the patrons to be merged in other tables and changing the ids to be that
465 of the keeper patron.
470 my ( $self, $patron_ids ) = @_;
472 my @patron_ids = @
{ $patron_ids };
474 # Ensure the keeper isn't in the list of patrons to merge
475 @patron_ids = grep { $_ ne $self->id } @patron_ids;
477 my $schema = Koha
::Database
->new()->schema();
481 $self->_result->result_source->schema->txn_do( sub {
482 foreach my $patron_id (@patron_ids) {
483 my $patron = Koha
::Patrons
->find( $patron_id );
487 # Unbless for safety, the patron will end up being deleted
488 $results->{merged
}->{$patron_id}->{patron
} = $patron->unblessed;
490 while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
491 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
492 $results->{merged
}->{ $patron_id }->{updated
}->{$r} = $rs->count();
493 $rs->update({ $field => $self->id });
496 $patron->move_to_deleted();
506 =head3 wants_check_for_previous_checkout
508 $wants_check = $patron->wants_check_for_previous_checkout;
510 Return 1 if Koha needs to perform PrevIssue checking, else 0.
514 sub wants_check_for_previous_checkout
{
516 my $syspref = C4
::Context
->preference("checkPrevCheckout");
519 ## Hard syspref trumps all
520 return 1 if ($syspref eq 'hardyes');
521 return 0 if ($syspref eq 'hardno');
522 ## Now, patron pref trumps all
523 return 1 if ($self->checkprevcheckout eq 'yes');
524 return 0 if ($self->checkprevcheckout eq 'no');
526 # More complex: patron inherits -> determine category preference
527 my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
528 return 1 if ($checkPrevCheckoutByCat eq 'yes');
529 return 0 if ($checkPrevCheckoutByCat eq 'no');
531 # Finally: category preference is inherit, default to 0
532 if ($syspref eq 'softyes') {
539 =head3 do_check_for_previous_checkout
541 $do_check = $patron->do_check_for_previous_checkout($item);
543 Return 1 if the bib associated with $ITEM has previously been checked out to
544 $PATRON, 0 otherwise.
548 sub do_check_for_previous_checkout
{
549 my ( $self, $item ) = @_;
551 # Find all items for bib and extract item numbers.
552 my @items = Koha
::Items
->search({biblionumber
=> $item->{biblionumber
}});
554 foreach my $item (@items) {
555 push @item_nos, $item->itemnumber;
558 # Create (old)issues search criteria
560 borrowernumber
=> $self->borrowernumber,
561 itemnumber
=> \
@item_nos,
564 # Check current issues table
565 my $issues = Koha
::Checkouts
->search($criteria);
566 return 1 if $issues->count; # 0 || N
568 # Check old issues table
569 my $old_issues = Koha
::Old
::Checkouts
->search($criteria);
570 return $old_issues->count; # 0 || N
575 my $debarment_expiration = $patron->is_debarred;
577 Returns the date a patron debarment will expire, or undef if the patron is not
585 return unless $self->debarred;
586 return $self->debarred
587 if $self->debarred =~ '^9999'
588 or dt_from_string
( $self->debarred ) > dt_from_string
;
594 my $is_expired = $patron->is_expired;
596 Returns 1 if the patron is expired or 0;
602 return 0 unless $self->dateexpiry;
603 return 0 if $self->dateexpiry =~ '^9999';
604 return 1 if dt_from_string
( $self->dateexpiry ) < dt_from_string
->truncate( to
=> 'day' );
608 =head3 is_going_to_expire
610 my $is_going_to_expire = $patron->is_going_to_expire;
612 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
616 sub is_going_to_expire
{
619 my $delay = C4
::Context
->preference('NotifyBorrowerDeparture') || 0;
621 return 0 unless $delay;
622 return 0 unless $self->dateexpiry;
623 return 0 if $self->dateexpiry =~ '^9999';
624 return 1 if dt_from_string
( $self->dateexpiry )->subtract( days
=> $delay ) < dt_from_string
->truncate( to
=> 'day' );
630 $patron->set_password({ password => $plain_text_password [, skip_validation => 1 ] });
632 Set the patron's password.
636 The passed string is validated against the current password enforcement policy.
637 Validation can be skipped by passing the I<skip_validation> parameter.
639 Exceptions are thrown if the password is not good enough.
643 =item Koha::Exceptions::Password::TooShort
645 =item Koha::Exceptions::Password::WhitespaceCharacters
647 =item Koha::Exceptions::Password::TooWeak
654 my ( $self, $args ) = @_;
656 my $password = $args->{password
};
658 unless ( $args->{skip_validation
} ) {
659 my ( $is_valid, $error ) = Koha
::AuthUtils
::is_password_valid
( $password );
662 if ( $error eq 'too_short' ) {
663 my $min_length = C4
::Context
->preference('minPasswordLength');
664 $min_length = 3 if not $min_length or $min_length < 3;
666 my $password_length = length($password);
667 Koha
::Exceptions
::Password
::TooShort
->throw(
668 length => $password_length, min_length
=> $min_length );
670 elsif ( $error eq 'has_whitespaces' ) {
671 Koha
::Exceptions
::Password
::WhitespaceCharacters
->throw();
673 elsif ( $error eq 'too_weak' ) {
674 Koha
::Exceptions
::Password
::TooWeak
->throw();
679 my $digest = Koha
::AuthUtils
::hash_password
($password);
681 { password
=> $digest,
686 logaction
( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" )
687 if C4
::Context
->preference("BorrowersLog");
695 my $new_expiry_date = $patron->renew_account
697 Extending the subscription to the expiry date.
704 if ( C4
::Context
->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
705 $date = ( dt_from_string
gt dt_from_string
( $self->dateexpiry ) ) ? dt_from_string
: dt_from_string
( $self->dateexpiry );
708 C4
::Context
->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
709 ? dt_from_string
( $self->dateexpiry )
712 my $expiry_date = $self->category->get_expiry_date($date);
714 $self->dateexpiry($expiry_date);
715 $self->date_renewed( dt_from_string
() );
718 $self->add_enrolment_fee_if_needed;
720 logaction
( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4
::Context
->preference("BorrowersLog");
721 return dt_from_string
( $expiry_date )->truncate( to
=> 'day' );
726 my $has_overdues = $patron->has_overdues;
728 Returns the number of patron's overdues
734 my $dtf = Koha
::Database
->new->schema->storage->datetime_parser;
735 return $self->_result->issues->search({ date_due
=> { '<' => $dtf->format_datetime( dt_from_string
() ) } })->count;
740 $patron->track_login;
741 $patron->track_login({ force => 1 });
743 Tracks a (successful) login attempt.
744 The preference TrackLastPatronActivity must be enabled. Or you
745 should pass the force parameter.
750 my ( $self, $params ) = @_;
753 !C4
::Context
->preference('TrackLastPatronActivity');
754 $self->lastseen( dt_from_string
() )->store;
757 =head3 move_to_deleted
759 my $is_moved = $patron->move_to_deleted;
761 Move a patron to the deletedborrowers table.
762 This can be done before deleting a patron, to make sure the data are not completely deleted.
766 sub move_to_deleted
{
768 my $patron_infos = $self->unblessed;
769 delete $patron_infos->{updated_on
}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
770 return Koha
::Database
->new->schema->resultset('Deletedborrower')->create($patron_infos);
773 =head3 article_requests
775 my @requests = $borrower->article_requests();
776 my $requests = $borrower->article_requests();
778 Returns either a list of ArticleRequests objects,
779 or an ArtitleRequests object, depending on the
784 sub article_requests
{
787 $self->{_article_requests
} ||= Koha
::ArticleRequests
->search({ borrowernumber
=> $self->borrowernumber() });
789 return $self->{_article_requests
};
792 =head3 article_requests_current
794 my @requests = $patron->article_requests_current
796 Returns the article requests associated with this patron that are incomplete
800 sub article_requests_current
{
803 $self->{_article_requests_current
} ||= Koha
::ArticleRequests
->search(
805 borrowernumber
=> $self->id(),
807 { status
=> Koha
::ArticleRequest
::Status
::Pending
},
808 { status
=> Koha
::ArticleRequest
::Status
::Processing
}
813 return $self->{_article_requests_current
};
816 =head3 article_requests_finished
818 my @requests = $biblio->article_requests_finished
820 Returns the article requests associated with this patron that are completed
824 sub article_requests_finished
{
825 my ( $self, $borrower ) = @_;
827 $self->{_article_requests_finished
} ||= Koha
::ArticleRequests
->search(
829 borrowernumber
=> $self->id(),
831 { status
=> Koha
::ArticleRequest
::Status
::Completed
},
832 { status
=> Koha
::ArticleRequest
::Status
::Canceled
}
837 return $self->{_article_requests_finished
};
840 =head3 add_enrolment_fee_if_needed
842 my $enrolment_fee = $patron->add_enrolment_fee_if_needed;
844 Add enrolment fee for a patron if needed.
848 sub add_enrolment_fee_if_needed
{
850 my $enrolment_fee = $self->category->enrolmentfee;
851 if ( $enrolment_fee && $enrolment_fee > 0 ) {
852 # insert fee in patron debts
853 C4
::Accounts
::manualinvoice
( $self->borrowernumber, '', '', 'A', $enrolment_fee );
855 return $enrolment_fee || 0;
860 my $checkouts = $patron->checkouts
866 my $checkouts = $self->_result->issues;
867 return Koha
::Checkouts
->_new_from_dbic( $checkouts );
870 =head3 pending_checkouts
872 my $pending_checkouts = $patron->pending_checkouts
874 This method will return the same as $self->checkouts, but with a prefetch on
875 items, biblio and biblioitems.
877 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
879 It should not be used directly, prefer to access fields you need instead of
880 retrieving all these fields in one go.
885 sub pending_checkouts
{
887 my $checkouts = $self->_result->issues->search(
891 { -desc
=> 'me.timestamp' },
892 { -desc
=> 'issuedate' },
893 { -desc
=> 'issue_id' }, # Sort by issue_id should be enough
895 prefetch
=> { item
=> { biblio
=> 'biblioitems' } },
898 return Koha
::Checkouts
->_new_from_dbic( $checkouts );
903 my $old_checkouts = $patron->old_checkouts
909 my $old_checkouts = $self->_result->old_issues;
910 return Koha
::Old
::Checkouts
->_new_from_dbic( $old_checkouts );
915 my $overdue_items = $patron->get_overdues
917 Return the overdue items
923 my $dtf = Koha
::Database
->new->schema->storage->datetime_parser;
924 return $self->checkouts->search(
926 'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string
) },
929 prefetch
=> { item
=> { biblio
=> 'biblioitems' } },
934 =head3 get_routing_lists
936 my @routinglists = $patron->get_routing_lists
938 Returns the routing lists a patron is subscribed to.
942 sub get_routing_lists
{
944 my $routing_list_rs = $self->_result->subscriptionroutinglists;
945 return Koha
::Subscription
::Routinglists
->_new_from_dbic($routing_list_rs);
950 my $age = $patron->get_age
952 Return the age of the patron
958 my $today_str = dt_from_string
->strftime("%Y-%m-%d");
959 return unless $self->dateofbirth;
960 my $dob_str = dt_from_string
( $self->dateofbirth )->strftime("%Y-%m-%d");
962 my ( $dob_y, $dob_m, $dob_d ) = split /-/, $dob_str;
963 my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
965 my $age = $today_y - $dob_y;
966 if ( $dob_m . $dob_d > $today_m . $today_d ) {
975 my $account = $patron->account
981 return Koha
::Account
->new( { patron_id
=> $self->borrowernumber } );
986 my $holds = $patron->holds
988 Return all the holds placed by this patron
994 my $holds_rs = $self->_result->reserves->search( {}, { order_by
=> 'reservedate' } );
995 return Koha
::Holds
->_new_from_dbic($holds_rs);
1000 my $old_holds = $patron->old_holds
1002 Return all the historical holds for this patron
1008 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by
=> 'reservedate' } );
1009 return Koha
::Old
::Holds
->_new_from_dbic($old_holds_rs);
1012 =head3 notice_email_address
1014 my $email = $patron->notice_email_address;
1016 Return the email address of patron used for notices.
1017 Returns the empty string if no email address.
1021 sub notice_email_address
{
1024 my $which_address = C4
::Context
->preference("AutoEmailPrimaryAddress");
1025 # if syspref is set to 'first valid' (value == OFF), look up email address
1026 if ( $which_address eq 'OFF' ) {
1027 return $self->first_valid_email_address;
1030 return $self->$which_address || '';
1033 =head3 first_valid_email_address
1035 my $first_valid_email_address = $patron->first_valid_email_address
1037 Return the first valid email address for a patron.
1038 For now, the order is defined as email, emailpro, B_email.
1039 Returns the empty string if the borrower has no email addresses.
1043 sub first_valid_email_address
{
1046 return $self->email() || $self->emailpro() || $self->B_email() || q{};
1049 =head3 get_club_enrollments
1053 sub get_club_enrollments
{
1054 my ( $self, $return_scalar ) = @_;
1056 my $e = Koha
::Club
::Enrollments
->search( { borrowernumber
=> $self->borrowernumber(), date_canceled
=> undef } );
1058 return $e if $return_scalar;
1060 return wantarray ?
$e->as_list : $e;
1063 =head3 get_enrollable_clubs
1067 sub get_enrollable_clubs
{
1068 my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
1071 $params->{is_enrollable_from_opac
} = $is_enrollable_from_opac
1072 if $is_enrollable_from_opac;
1073 $params->{is_email_required
} = 0 unless $self->first_valid_email_address();
1075 $params->{borrower
} = $self;
1077 my $e = Koha
::Clubs
->get_enrollable($params);
1079 return $e if $return_scalar;
1081 return wantarray ?
$e->as_list : $e;
1084 =head3 account_locked
1086 my $is_locked = $patron->account_locked
1088 Return true if the patron has reach the maximum number of login attempts (see pref FailedLoginAttempts).
1089 Otherwise return false.
1090 If the pref is not set (empty string, null or 0), the feature is considered as disabled.
1094 sub account_locked
{
1096 my $FailedLoginAttempts = C4
::Context
->preference('FailedLoginAttempts');
1097 return ( $FailedLoginAttempts
1098 and $self->login_attempts
1099 and $self->login_attempts >= $FailedLoginAttempts )?
1 : 0;
1102 =head3 can_see_patron_infos
1104 my $can_see = $patron->can_see_patron_infos( $patron );
1106 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1110 sub can_see_patron_infos
{
1111 my ( $self, $patron ) = @_;
1112 return unless $patron;
1113 return $self->can_see_patrons_from( $patron->library->branchcode );
1116 =head3 can_see_patrons_from
1118 my $can_see = $patron->can_see_patrons_from( $branchcode );
1120 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1124 sub can_see_patrons_from
{
1125 my ( $self, $branchcode ) = @_;
1127 if ( $self->branchcode eq $branchcode ) {
1129 } elsif ( $self->has_permission( { borrowers
=> 'view_borrower_infos_from_any_libraries' } ) ) {
1131 } elsif ( my $library_groups = $self->library->library_groups ) {
1132 while ( my $library_group = $library_groups->next ) {
1133 if ( $library_group->parent->has_child( $branchcode ) ) {
1142 =head3 libraries_where_can_see_patrons
1144 my $libraries = $patron-libraries_where_can_see_patrons;
1146 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1147 The branchcodes are arbitrarily returned sorted.
1148 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1150 An empty array means no restriction, the patron can see patron's infos from any libraries.
1154 sub libraries_where_can_see_patrons
{
1156 my $userenv = C4
::Context
->userenv;
1158 return () unless $userenv; # For tests, but userenv should be defined in tests...
1160 my @restricted_branchcodes;
1161 if (C4
::Context
::only_my_library
) {
1162 push @restricted_branchcodes, $self->branchcode;
1166 $self->has_permission(
1167 { borrowers
=> 'view_borrower_infos_from_any_libraries' }
1171 my $library_groups = $self->library->library_groups({ ft_hide_patron_info
=> 1 });
1172 if ( $library_groups->count )
1174 while ( my $library_group = $library_groups->next ) {
1175 my $parent = $library_group->parent;
1176 if ( $parent->has_child( $self->branchcode ) ) {
1177 push @restricted_branchcodes, $parent->children->get_column('branchcode');
1182 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1186 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1187 @restricted_branchcodes = uniq
(@restricted_branchcodes);
1188 @restricted_branchcodes = sort(@restricted_branchcodes);
1189 return @restricted_branchcodes;
1192 sub has_permission
{
1193 my ( $self, $flagsrequired ) = @_;
1194 return unless $self->userid;
1195 # TODO code from haspermission needs to be moved here!
1196 return C4
::Auth
::haspermission
( $self->userid, $flagsrequired );
1201 my $is_adult = $patron->is_adult
1203 Return true if the patron has a category with a type Adult (A) or Organization (I)
1209 return $self->category->category_type =~ /^(A|I)$/ ?
1 : 0;
1214 my $is_child = $patron->is_child
1216 Return true if the patron has a category with a type Child (C)
1221 return $self->category->category_type eq 'C' ?
1 : 0;
1224 =head3 has_valid_userid
1226 my $patron = Koha::Patrons->find(42);
1227 $patron->userid( $new_userid );
1228 my $has_a_valid_userid = $patron->has_valid_userid
1230 my $patron = Koha::Patron->new( $params );
1231 my $has_a_valid_userid = $patron->has_valid_userid
1233 Return true if the current userid of this patron is valid/unique, otherwise false.
1235 Note that this should be done in $self->store instead and raise an exception if needed.
1239 sub has_valid_userid
{
1242 return 0 unless $self->userid;
1244 return 0 if ( $self->userid eq C4
::Context
->config('user') ); # DB user
1246 my $already_exists = Koha
::Patrons
->search(
1248 userid
=> $self->userid,
1251 ?
( borrowernumber
=> { '!=' => $self->borrowernumber } )
1256 return $already_exists ?
0 : 1;
1259 =head3 generate_userid
1261 my $patron = Koha::Patron->new( $params );
1262 $patron->generate_userid
1264 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1266 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).
1270 sub generate_userid
{
1273 my $firstname = $self->firstname // q{};
1274 my $surname = $self->surname // q{};
1275 #The script will "do" the following code and increment the $offset until the generated userid is unique
1277 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1278 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1279 my $userid = lc(($firstname)?
"$firstname.$surname" : $surname);
1280 $userid = unac_string
('utf-8',$userid);
1281 $userid .= $offset unless $offset == 0;
1282 $self->userid( $userid );
1284 } while (! $self->has_valid_userid );
1290 =head2 Internal methods
1302 Kyle M Hall <kyle@bywatersolutions.com>
1303 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>