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( uniq );
25 use Text::Unaccent qw( unac_string );
33 use Koha::Old::Checkouts;
34 use Koha::Patron::Categories;
35 use Koha::Patron::HouseboundProfile;
36 use Koha::Patron::HouseboundRole;
37 use Koha::Patron::Images;
39 use Koha::Virtualshelves;
40 use Koha::Club::Enrollments;
42 use Koha::Subscription::Routinglists;
44 use base qw(Koha::Object);
46 our $RESULTSET_PATRON_ID_MAPPING = {
47 Accountline => 'borrowernumber',
48 Aqbasketuser => 'borrowernumber',
49 Aqbudget => 'budget_owner_id',
50 Aqbudgetborrower => 'borrowernumber',
51 ArticleRequest => 'borrowernumber',
52 BorrowerAttribute => 'borrowernumber',
53 BorrowerDebarment => 'borrowernumber',
54 BorrowerFile => 'borrowernumber',
55 BorrowerModification => 'borrowernumber',
56 ClubEnrollment => 'borrowernumber',
57 Issue => 'borrowernumber',
58 ItemsLastBorrower => 'borrowernumber',
59 Linktracker => 'borrowernumber',
60 Message => 'borrowernumber',
61 MessageQueue => 'borrowernumber',
62 OldIssue => 'borrowernumber',
63 OldReserve => 'borrowernumber',
64 Rating => 'borrowernumber',
65 Reserve => 'borrowernumber',
66 Review => 'borrowernumber',
67 SearchHistory => 'userid',
68 Statistic => 'borrowernumber',
69 Suggestion => 'suggestedby',
70 TagAll => 'borrowernumber',
71 Virtualshelfcontent => 'borrowernumber',
72 Virtualshelfshare => 'borrowernumber',
73 Virtualshelve => 'owner',
78 Koha::Patron - Koha Patron Object class
91 my ( $class, $params ) = @_;
93 return $class->SUPER::new($params);
96 sub fixup_cardnumber {
98 my $max = Koha::Patrons->search({
99 cardnumber => {-regexp => '^-?[0-9]+$'}
101 select => \'CAST(cardnumber AS SIGNED)',
102 as => ['cast_cardnumber']
103 })->_resultset->get_column('cast_cardnumber')->max;
104 $self->cardnumber($max+1);
107 # trim whitespace from data which has some non-whitespace in it.
108 # Could be moved to Koha::Object if need to be reused
109 sub trim_whitespaces {
112 my $schema = Koha::Database->new->schema;
113 my @columns = $schema->source('Borrowers')->columns;
115 for my $column( @columns ) {
116 my $value = $self->$column;
117 if ( defined $value ) {
118 $value =~ s/^\s*|\s*$//g;
119 $self->$column($value);
128 $self->_result->result_source->schema->txn_do(
131 C4::Context->preference("autoMemberNum")
132 and ( not defined $self->cardnumber
133 or $self->cardnumber eq '' )
136 # Warning: The caller is responsible for locking the members table in write
137 # mode, to avoid database corruption.
138 # We are in a transaction but the table is not locked
139 $self->fixup_cardnumber;
151 Delete patron's holds, lists and finally the patron.
153 Lists owned by the borrower are deleted, but entries from the borrower to
154 other lists are kept.
162 $self->_result->result_source->schema->txn_do(
164 # Delete Patron's holds
165 $self->holds->delete;
167 # Delete all lists and all shares of this borrower
168 # Consistent with the approach Koha uses on deleting individual lists
169 # Note that entries in virtualshelfcontents added by this borrower to
170 # lists of others will be handled by a table constraint: the borrower
171 # is set to NULL in those entries.
173 # We could handle the above deletes via a constraint too.
174 # But a new BZ report 11889 has been opened to discuss another approach.
175 # Instead of deleting we could also disown lists (based on a pref).
176 # In that way we could save shared and public lists.
177 # The current table constraints support that idea now.
178 # This pref should then govern the results of other routines/methods such as
179 # Koha::Virtualshelf->new->delete too.
180 # FIXME Could be $patron->get_lists
181 $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
183 $deleted = $self->SUPER::delete;
185 logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
194 my $patron_category = $patron->category
196 Return the patron category for this patron
202 return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
207 Returns a Koha::Patron object for this patron's guarantor
214 return unless $self->guarantorid();
216 return Koha::Patrons->find( $self->guarantorid() );
222 return scalar Koha::Patron::Images->find( $self->borrowernumber );
227 return Koha::Library->_new_from_dbic($self->_result->branchcode);
232 Returns the guarantees (list of Koha::Patron) of this patron
239 return Koha::Patrons->search( { guarantorid => $self->borrowernumber } );
242 =head3 housebound_profile
244 Returns the HouseboundProfile associated with this patron.
248 sub housebound_profile {
250 my $profile = $self->_result->housebound_profile;
251 return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
256 =head3 housebound_role
258 Returns the HouseboundRole associated with this patron.
262 sub housebound_role {
265 my $role = $self->_result->housebound_role;
266 return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
272 Returns the siblings of this patron.
279 my $guarantor = $self->guarantor;
281 return unless $guarantor;
283 return Koha::Patrons->search(
287 '=' => $guarantor->id,
290 '!=' => $self->borrowernumber,
298 my $patron = Koha::Patrons->find($id);
299 $patron->merge_with( \@patron_ids );
301 This subroutine merges a list of patrons into the patron record. This is accomplished by finding
302 all related patron ids for the patrons to be merged in other tables and changing the ids to be that
303 of the keeper patron.
308 my ( $self, $patron_ids ) = @_;
310 my @patron_ids = @{ $patron_ids };
312 # Ensure the keeper isn't in the list of patrons to merge
313 @patron_ids = grep { $_ ne $self->id } @patron_ids;
315 my $schema = Koha::Database->new()->schema();
319 $self->_result->result_source->schema->txn_do( sub {
320 foreach my $patron_id (@patron_ids) {
321 my $patron = Koha::Patrons->find( $patron_id );
325 # Unbless for safety, the patron will end up being deleted
326 $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
328 while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
329 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
330 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
331 $rs->update({ $field => $self->id });
334 $patron->move_to_deleted();
344 =head3 wants_check_for_previous_checkout
346 $wants_check = $patron->wants_check_for_previous_checkout;
348 Return 1 if Koha needs to perform PrevIssue checking, else 0.
352 sub wants_check_for_previous_checkout {
354 my $syspref = C4::Context->preference("checkPrevCheckout");
357 ## Hard syspref trumps all
358 return 1 if ($syspref eq 'hardyes');
359 return 0 if ($syspref eq 'hardno');
360 ## Now, patron pref trumps all
361 return 1 if ($self->checkprevcheckout eq 'yes');
362 return 0 if ($self->checkprevcheckout eq 'no');
364 # More complex: patron inherits -> determine category preference
365 my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
366 return 1 if ($checkPrevCheckoutByCat eq 'yes');
367 return 0 if ($checkPrevCheckoutByCat eq 'no');
369 # Finally: category preference is inherit, default to 0
370 if ($syspref eq 'softyes') {
377 =head3 do_check_for_previous_checkout
379 $do_check = $patron->do_check_for_previous_checkout($item);
381 Return 1 if the bib associated with $ITEM has previously been checked out to
382 $PATRON, 0 otherwise.
386 sub do_check_for_previous_checkout {
387 my ( $self, $item ) = @_;
389 # Find all items for bib and extract item numbers.
390 my @items = Koha::Items->search({biblionumber => $item->{biblionumber}});
392 foreach my $item (@items) {
393 push @item_nos, $item->itemnumber;
396 # Create (old)issues search criteria
398 borrowernumber => $self->borrowernumber,
399 itemnumber => \@item_nos,
402 # Check current issues table
403 my $issues = Koha::Checkouts->search($criteria);
404 return 1 if $issues->count; # 0 || N
406 # Check old issues table
407 my $old_issues = Koha::Old::Checkouts->search($criteria);
408 return $old_issues->count; # 0 || N
413 my $debarment_expiration = $patron->is_debarred;
415 Returns the date a patron debarment will expire, or undef if the patron is not
423 return unless $self->debarred;
424 return $self->debarred
425 if $self->debarred =~ '^9999'
426 or dt_from_string( $self->debarred ) > dt_from_string;
432 my $is_expired = $patron->is_expired;
434 Returns 1 if the patron is expired or 0;
440 return 0 unless $self->dateexpiry;
441 return 0 if $self->dateexpiry =~ '^9999';
442 return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
446 =head3 is_going_to_expire
448 my $is_going_to_expire = $patron->is_going_to_expire;
450 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
454 sub is_going_to_expire {
457 my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
459 return 0 unless $delay;
460 return 0 unless $self->dateexpiry;
461 return 0 if $self->dateexpiry =~ '^9999';
462 return 1 if dt_from_string( $self->dateexpiry )->subtract( days => $delay ) < dt_from_string->truncate( to => 'day' );
466 =head3 update_password
468 my $updated = $patron->update_password( $userid, $password );
470 Update the userid and the password of a patron.
471 If the userid already exists, returns and let DBIx::Class warns
472 This will add an entry to action_logs if BorrowersLog is set.
476 sub update_password {
477 my ( $self, $userid, $password ) = @_;
478 eval { $self->userid($userid)->store; };
479 return if $@; # Make sure the userid is not already in used by another patron
482 password => $password,
486 logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
492 my $new_expiry_date = $patron->renew_account
494 Extending the subscription to the expiry date.
501 if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
502 $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
505 C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
506 ? dt_from_string( $self->dateexpiry )
509 my $expiry_date = $self->category->get_expiry_date($date);
511 $self->dateexpiry($expiry_date);
512 $self->date_renewed( dt_from_string() );
515 $self->add_enrolment_fee_if_needed;
517 logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
518 return dt_from_string( $expiry_date )->truncate( to => 'day' );
523 my $has_overdues = $patron->has_overdues;
525 Returns the number of patron's overdues
531 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
532 return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
537 $patron->track_login;
538 $patron->track_login({ force => 1 });
540 Tracks a (successful) login attempt.
541 The preference TrackLastPatronActivity must be enabled. Or you
542 should pass the force parameter.
547 my ( $self, $params ) = @_;
550 !C4::Context->preference('TrackLastPatronActivity');
551 $self->lastseen( dt_from_string() )->store;
554 =head3 move_to_deleted
556 my $is_moved = $patron->move_to_deleted;
558 Move a patron to the deletedborrowers table.
559 This can be done before deleting a patron, to make sure the data are not completely deleted.
563 sub move_to_deleted {
565 my $patron_infos = $self->unblessed;
566 delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
567 return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
570 =head3 article_requests
572 my @requests = $borrower->article_requests();
573 my $requests = $borrower->article_requests();
575 Returns either a list of ArticleRequests objects,
576 or an ArtitleRequests object, depending on the
581 sub article_requests {
584 $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
586 return $self->{_article_requests};
589 =head3 article_requests_current
591 my @requests = $patron->article_requests_current
593 Returns the article requests associated with this patron that are incomplete
597 sub article_requests_current {
600 $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
602 borrowernumber => $self->id(),
604 { status => Koha::ArticleRequest::Status::Pending },
605 { status => Koha::ArticleRequest::Status::Processing }
610 return $self->{_article_requests_current};
613 =head3 article_requests_finished
615 my @requests = $biblio->article_requests_finished
617 Returns the article requests associated with this patron that are completed
621 sub article_requests_finished {
622 my ( $self, $borrower ) = @_;
624 $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
626 borrowernumber => $self->id(),
628 { status => Koha::ArticleRequest::Status::Completed },
629 { status => Koha::ArticleRequest::Status::Canceled }
634 return $self->{_article_requests_finished};
637 =head3 add_enrolment_fee_if_needed
639 my $enrolment_fee = $patron->add_enrolment_fee_if_needed;
641 Add enrolment fee for a patron if needed.
645 sub add_enrolment_fee_if_needed {
647 my $enrolment_fee = $self->category->enrolmentfee;
648 if ( $enrolment_fee && $enrolment_fee > 0 ) {
649 # insert fee in patron debts
650 C4::Accounts::manualinvoice( $self->borrowernumber, '', '', 'A', $enrolment_fee );
652 return $enrolment_fee || 0;
657 my $checkouts = $patron->checkouts
663 my $checkouts = $self->_result->issues;
664 return Koha::Checkouts->_new_from_dbic( $checkouts );
667 =head3 pending_checkouts
669 my $pending_checkouts = $patron->pending_checkouts
671 This method will return the same as $self->checkouts, but with a prefetch on
672 items, biblio and biblioitems.
674 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
676 It should not be used directly, prefer to access fields you need instead of
677 retrieving all these fields in one go.
682 sub pending_checkouts {
684 my $checkouts = $self->_result->issues->search(
688 { -desc => 'me.timestamp' },
689 { -desc => 'issuedate' },
690 { -desc => 'issue_id' }, # Sort by issue_id should be enough
692 prefetch => { item => { biblio => 'biblioitems' } },
695 return Koha::Checkouts->_new_from_dbic( $checkouts );
700 my $old_checkouts = $patron->old_checkouts
706 my $old_checkouts = $self->_result->old_issues;
707 return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
712 my $overdue_items = $patron->get_overdues
714 Return the overdue items
720 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
721 return $self->checkouts->search(
723 'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
726 prefetch => { item => { biblio => 'biblioitems' } },
731 =head3 get_routing_lists
733 my @routinglists = $patron->get_routing_lists
735 Returns the routing lists a patron is subscribed to.
739 sub get_routing_lists {
741 my $routing_list_rs = $self->_result->subscriptionroutinglists;
742 return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
747 my $age = $patron->get_age
749 Return the age of the patron
755 my $today_str = dt_from_string->strftime("%Y-%m-%d");
756 return unless $self->dateofbirth;
757 my $dob_str = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
759 my ( $dob_y, $dob_m, $dob_d ) = split /-/, $dob_str;
760 my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
762 my $age = $today_y - $dob_y;
763 if ( $dob_m . $dob_d > $today_m . $today_d ) {
772 my $account = $patron->account
778 return Koha::Account->new( { patron_id => $self->borrowernumber } );
783 my $holds = $patron->holds
785 Return all the holds placed by this patron
791 my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
792 return Koha::Holds->_new_from_dbic($holds_rs);
797 my $old_holds = $patron->old_holds
799 Return all the historical holds for this patron
805 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
806 return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
809 =head3 notice_email_address
811 my $email = $patron->notice_email_address;
813 Return the email address of patron used for notices.
814 Returns the empty string if no email address.
818 sub notice_email_address{
821 my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
822 # if syspref is set to 'first valid' (value == OFF), look up email address
823 if ( $which_address eq 'OFF' ) {
824 return $self->first_valid_email_address;
827 return $self->$which_address || '';
830 =head3 first_valid_email_address
832 my $first_valid_email_address = $patron->first_valid_email_address
834 Return the first valid email address for a patron.
835 For now, the order is defined as email, emailpro, B_email.
836 Returns the empty string if the borrower has no email addresses.
840 sub first_valid_email_address {
843 return $self->email() || $self->emailpro() || $self->B_email() || q{};
846 =head3 get_club_enrollments
850 sub get_club_enrollments {
851 my ( $self, $return_scalar ) = @_;
853 my $e = Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
855 return $e if $return_scalar;
857 return wantarray ? $e->as_list : $e;
860 =head3 get_enrollable_clubs
864 sub get_enrollable_clubs {
865 my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
868 $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
869 if $is_enrollable_from_opac;
870 $params->{is_email_required} = 0 unless $self->first_valid_email_address();
872 $params->{borrower} = $self;
874 my $e = Koha::Clubs->get_enrollable($params);
876 return $e if $return_scalar;
878 return wantarray ? $e->as_list : $e;
881 =head3 account_locked
883 my $is_locked = $patron->account_locked
885 Return true if the patron has reach the maximum number of login attempts (see pref FailedLoginAttempts).
886 Otherwise return false.
887 If the pref is not set (empty string, null or 0), the feature is considered as disabled.
893 my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
894 return ( $FailedLoginAttempts
895 and $self->login_attempts
896 and $self->login_attempts >= $FailedLoginAttempts )? 1 : 0;
899 =head3 can_see_patron_infos
901 my $can_see = $patron->can_see_patron_infos( $patron );
903 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
907 sub can_see_patron_infos {
908 my ( $self, $patron ) = @_;
909 return $self->can_see_patrons_from( $patron->library->branchcode );
912 =head3 can_see_patrons_from
914 my $can_see = $patron->can_see_patrons_from( $branchcode );
916 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
920 sub can_see_patrons_from {
921 my ( $self, $branchcode ) = @_;
923 if ( $self->branchcode eq $branchcode ) {
925 } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
927 } elsif ( my $library_groups = $self->library->library_groups ) {
928 while ( my $library_group = $library_groups->next ) {
929 if ( $library_group->parent->has_child( $branchcode ) ) {
938 =head3 libraries_where_can_see_patrons
940 my $libraries = $patron-libraries_where_can_see_patrons;
942 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
943 The branchcodes are arbitrarily returned sorted.
944 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
946 An empty array means no restriction, the patron can see patron's infos from any libraries.
950 sub libraries_where_can_see_patrons {
952 my $userenv = C4::Context->userenv;
954 return () unless $userenv; # For tests, but userenv should be defined in tests...
956 my @restricted_branchcodes;
957 if (C4::Context::only_my_library) {
958 push @restricted_branchcodes, $self->branchcode;
962 $self->has_permission(
963 { borrowers => 'view_borrower_infos_from_any_libraries' }
967 my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
968 if ( $library_groups->count )
970 while ( my $library_group = $library_groups->next ) {
971 my $parent = $library_group->parent;
972 if ( $parent->has_child( $self->branchcode ) ) {
973 push @restricted_branchcodes, $parent->children->get_column('branchcode');
978 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
982 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
983 @restricted_branchcodes = uniq(@restricted_branchcodes);
984 @restricted_branchcodes = sort(@restricted_branchcodes);
985 return @restricted_branchcodes;
989 my ( $self, $flagsrequired ) = @_;
990 return unless $self->userid;
991 # TODO code from haspermission needs to be moved here!
992 return C4::Auth::haspermission( $self->userid, $flagsrequired );
997 my $is_adult = $patron->is_adult
999 Return true if the patron has a category with a type Adult (A) or Organization (I)
1005 return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1010 my $is_child = $patron->is_child
1012 Return true if the patron has a category with a type Child (C)
1017 return $self->category->category_type eq 'C' ? 1 : 0;
1020 =head3 has_valid_userid
1022 my $patron = Koha::Patrons->find(42);
1023 $patron->userid( $new_userid );
1024 my $has_a_valid_userid = $patron->has_valid_userid
1026 my $patron = Koha::Patron->new( $params );
1027 my $has_a_valid_userid = $patron->has_valid_userid
1029 Return true if the current userid of this patron is valid/unique, otherwise false.
1031 Note that this should be done in $self->store instead and raise an exception if needed.
1035 sub has_valid_userid {
1038 return 0 unless $self->userid;
1040 return 0 if ( $self->userid eq C4::Context->config('user') ); # DB user
1042 my $already_exists = Koha::Patrons->search(
1044 userid => $self->userid,
1047 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1052 return $already_exists ? 0 : 1;
1055 =head3 generate_userid
1057 my $patron = Koha::Patron->new( $params );
1058 my $userid = $patron->generate_userid
1060 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1062 Return the generate 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).
1064 # Note: Should we set $self->userid with the generated value?
1065 # Certainly yes, but we AddMember and ModMember will be rewritten
1069 sub generate_userid {
1073 my $existing_userid = $self->userid;
1074 my $firstname = $self->firstname // q{};
1075 my $surname = $self->surname // q{};
1076 #The script will "do" the following code and increment the $offset until the generated userid is unique
1078 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1079 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1080 $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1081 $userid = unac_string('utf-8',$userid);
1082 $userid .= $offset unless $offset == 0;
1083 $self->userid( $userid );
1085 } while (! $self->has_valid_userid );
1087 # Resetting to the previous value as the callers do not expect
1088 # this method to modify the userid attribute
1089 # This will be done later (move of AddMember and ModMember)
1090 $self->userid( $existing_userid );
1096 =head2 Internal methods
1108 Kyle M Hall <kyle@bywatersolutions.com>
1109 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>