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);
110 $self->_result->result_source->schema->txn_do(
113 C4::Context->preference("autoMemberNum")
114 and ( not defined $self->cardnumber
115 or $self->cardnumber eq '' )
118 # Warning: The caller is responsible for locking the members table in write
119 # mode, to avoid database corruption.
120 # We are in a transaction but the table is not locked
121 $self->fixup_cardnumber;
133 Delete patron's holds, lists and finally the patron.
135 Lists owned by the borrower are deleted, but entries from the borrower to
136 other lists are kept.
144 $self->_result->result_source->schema->txn_do(
146 # Delete Patron's holds
147 $self->holds->delete;
149 # Delete all lists and all shares of this borrower
150 # Consistent with the approach Koha uses on deleting individual lists
151 # Note that entries in virtualshelfcontents added by this borrower to
152 # lists of others will be handled by a table constraint: the borrower
153 # is set to NULL in those entries.
155 # We could handle the above deletes via a constraint too.
156 # But a new BZ report 11889 has been opened to discuss another approach.
157 # Instead of deleting we could also disown lists (based on a pref).
158 # In that way we could save shared and public lists.
159 # The current table constraints support that idea now.
160 # This pref should then govern the results of other routines/methods such as
161 # Koha::Virtualshelf->new->delete too.
162 # FIXME Could be $patron->get_lists
163 $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
165 $deleted = $self->SUPER::delete;
167 logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
176 my $patron_category = $patron->category
178 Return the patron category for this patron
184 return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
189 Returns a Koha::Patron object for this patron's guarantor
196 return unless $self->guarantorid();
198 return Koha::Patrons->find( $self->guarantorid() );
204 return scalar Koha::Patron::Images->find( $self->borrowernumber );
209 return Koha::Library->_new_from_dbic($self->_result->branchcode);
214 Returns the guarantees (list of Koha::Patron) of this patron
221 return Koha::Patrons->search( { guarantorid => $self->borrowernumber } );
224 =head3 housebound_profile
226 Returns the HouseboundProfile associated with this patron.
230 sub housebound_profile {
232 my $profile = $self->_result->housebound_profile;
233 return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
238 =head3 housebound_role
240 Returns the HouseboundRole associated with this patron.
244 sub housebound_role {
247 my $role = $self->_result->housebound_role;
248 return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
254 Returns the siblings of this patron.
261 my $guarantor = $self->guarantor;
263 return unless $guarantor;
265 return Koha::Patrons->search(
269 '=' => $guarantor->id,
272 '!=' => $self->borrowernumber,
280 my $patron = Koha::Patrons->find($id);
281 $patron->merge_with( \@patron_ids );
283 This subroutine merges a list of patrons into the patron record. This is accomplished by finding
284 all related patron ids for the patrons to be merged in other tables and changing the ids to be that
285 of the keeper patron.
290 my ( $self, $patron_ids ) = @_;
292 my @patron_ids = @{ $patron_ids };
294 # Ensure the keeper isn't in the list of patrons to merge
295 @patron_ids = grep { $_ ne $self->id } @patron_ids;
297 my $schema = Koha::Database->new()->schema();
301 $self->_result->result_source->schema->txn_do( sub {
302 foreach my $patron_id (@patron_ids) {
303 my $patron = Koha::Patrons->find( $patron_id );
307 # Unbless for safety, the patron will end up being deleted
308 $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
310 while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
311 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
312 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
313 $rs->update({ $field => $self->id });
316 $patron->move_to_deleted();
326 =head3 wants_check_for_previous_checkout
328 $wants_check = $patron->wants_check_for_previous_checkout;
330 Return 1 if Koha needs to perform PrevIssue checking, else 0.
334 sub wants_check_for_previous_checkout {
336 my $syspref = C4::Context->preference("checkPrevCheckout");
339 ## Hard syspref trumps all
340 return 1 if ($syspref eq 'hardyes');
341 return 0 if ($syspref eq 'hardno');
342 ## Now, patron pref trumps all
343 return 1 if ($self->checkprevcheckout eq 'yes');
344 return 0 if ($self->checkprevcheckout eq 'no');
346 # More complex: patron inherits -> determine category preference
347 my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
348 return 1 if ($checkPrevCheckoutByCat eq 'yes');
349 return 0 if ($checkPrevCheckoutByCat eq 'no');
351 # Finally: category preference is inherit, default to 0
352 if ($syspref eq 'softyes') {
359 =head3 do_check_for_previous_checkout
361 $do_check = $patron->do_check_for_previous_checkout($item);
363 Return 1 if the bib associated with $ITEM has previously been checked out to
364 $PATRON, 0 otherwise.
368 sub do_check_for_previous_checkout {
369 my ( $self, $item ) = @_;
371 # Find all items for bib and extract item numbers.
372 my @items = Koha::Items->search({biblionumber => $item->{biblionumber}});
374 foreach my $item (@items) {
375 push @item_nos, $item->itemnumber;
378 # Create (old)issues search criteria
380 borrowernumber => $self->borrowernumber,
381 itemnumber => \@item_nos,
384 # Check current issues table
385 my $issues = Koha::Checkouts->search($criteria);
386 return 1 if $issues->count; # 0 || N
388 # Check old issues table
389 my $old_issues = Koha::Old::Checkouts->search($criteria);
390 return $old_issues->count; # 0 || N
395 my $debarment_expiration = $patron->is_debarred;
397 Returns the date a patron debarment will expire, or undef if the patron is not
405 return unless $self->debarred;
406 return $self->debarred
407 if $self->debarred =~ '^9999'
408 or dt_from_string( $self->debarred ) > dt_from_string;
414 my $is_expired = $patron->is_expired;
416 Returns 1 if the patron is expired or 0;
422 return 0 unless $self->dateexpiry;
423 return 0 if $self->dateexpiry =~ '^9999';
424 return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
428 =head3 is_going_to_expire
430 my $is_going_to_expire = $patron->is_going_to_expire;
432 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
436 sub is_going_to_expire {
439 my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
441 return 0 unless $delay;
442 return 0 unless $self->dateexpiry;
443 return 0 if $self->dateexpiry =~ '^9999';
444 return 1 if dt_from_string( $self->dateexpiry )->subtract( days => $delay ) < dt_from_string->truncate( to => 'day' );
448 =head3 update_password
450 my $updated = $patron->update_password( $userid, $password );
452 Update the userid and the password of a patron.
453 If the userid already exists, returns and let DBIx::Class warns
454 This will add an entry to action_logs if BorrowersLog is set.
458 sub update_password {
459 my ( $self, $userid, $password ) = @_;
460 eval { $self->userid($userid)->store; };
461 return if $@; # Make sure the userid is not already in used by another patron
464 password => $password,
468 logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
474 my $new_expiry_date = $patron->renew_account
476 Extending the subscription to the expiry date.
483 if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
484 $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
487 C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
488 ? dt_from_string( $self->dateexpiry )
491 my $expiry_date = $self->category->get_expiry_date($date);
493 $self->dateexpiry($expiry_date);
494 $self->date_renewed( dt_from_string() );
497 $self->add_enrolment_fee_if_needed;
499 logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
500 return dt_from_string( $expiry_date )->truncate( to => 'day' );
505 my $has_overdues = $patron->has_overdues;
507 Returns the number of patron's overdues
513 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
514 return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
519 $patron->track_login;
520 $patron->track_login({ force => 1 });
522 Tracks a (successful) login attempt.
523 The preference TrackLastPatronActivity must be enabled. Or you
524 should pass the force parameter.
529 my ( $self, $params ) = @_;
532 !C4::Context->preference('TrackLastPatronActivity');
533 $self->lastseen( dt_from_string() )->store;
536 =head3 move_to_deleted
538 my $is_moved = $patron->move_to_deleted;
540 Move a patron to the deletedborrowers table.
541 This can be done before deleting a patron, to make sure the data are not completely deleted.
545 sub move_to_deleted {
547 my $patron_infos = $self->unblessed;
548 delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
549 return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
552 =head3 article_requests
554 my @requests = $borrower->article_requests();
555 my $requests = $borrower->article_requests();
557 Returns either a list of ArticleRequests objects,
558 or an ArtitleRequests object, depending on the
563 sub article_requests {
566 $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
568 return $self->{_article_requests};
571 =head3 article_requests_current
573 my @requests = $patron->article_requests_current
575 Returns the article requests associated with this patron that are incomplete
579 sub article_requests_current {
582 $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
584 borrowernumber => $self->id(),
586 { status => Koha::ArticleRequest::Status::Pending },
587 { status => Koha::ArticleRequest::Status::Processing }
592 return $self->{_article_requests_current};
595 =head3 article_requests_finished
597 my @requests = $biblio->article_requests_finished
599 Returns the article requests associated with this patron that are completed
603 sub article_requests_finished {
604 my ( $self, $borrower ) = @_;
606 $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
608 borrowernumber => $self->id(),
610 { status => Koha::ArticleRequest::Status::Completed },
611 { status => Koha::ArticleRequest::Status::Canceled }
616 return $self->{_article_requests_finished};
619 =head3 add_enrolment_fee_if_needed
621 my $enrolment_fee = $patron->add_enrolment_fee_if_needed;
623 Add enrolment fee for a patron if needed.
627 sub add_enrolment_fee_if_needed {
629 my $enrolment_fee = $self->category->enrolmentfee;
630 if ( $enrolment_fee && $enrolment_fee > 0 ) {
631 # insert fee in patron debts
632 C4::Accounts::manualinvoice( $self->borrowernumber, '', '', 'A', $enrolment_fee );
634 return $enrolment_fee || 0;
639 my $checkouts = $patron->checkouts
645 my $checkouts = $self->_result->issues;
646 return Koha::Checkouts->_new_from_dbic( $checkouts );
649 =head3 pending_checkouts
651 my $pending_checkouts = $patron->pending_checkouts
653 This method will return the same as $self->checkouts, but with a prefetch on
654 items, biblio and biblioitems.
656 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
658 It should not be used directly, prefer to access fields you need instead of
659 retrieving all these fields in one go.
664 sub pending_checkouts {
666 my $checkouts = $self->_result->issues->search(
670 { -desc => 'me.timestamp' },
671 { -desc => 'issuedate' },
672 { -desc => 'issue_id' }, # Sort by issue_id should be enough
674 prefetch => { item => { biblio => 'biblioitems' } },
677 return Koha::Checkouts->_new_from_dbic( $checkouts );
682 my $old_checkouts = $patron->old_checkouts
688 my $old_checkouts = $self->_result->old_issues;
689 return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
694 my $overdue_items = $patron->get_overdues
696 Return the overdue items
702 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
703 return $self->checkouts->search(
705 'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
708 prefetch => { item => { biblio => 'biblioitems' } },
713 =head3 get_routing_lists
715 my @routinglists = $patron->get_routing_lists
717 Returns the routing lists a patron is subscribed to.
721 sub get_routing_lists {
723 my $routing_list_rs = $self->_result->subscriptionroutinglists;
724 return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
729 my $age = $patron->get_age
731 Return the age of the patron
737 my $today_str = dt_from_string->strftime("%Y-%m-%d");
738 return unless $self->dateofbirth;
739 my $dob_str = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
741 my ( $dob_y, $dob_m, $dob_d ) = split /-/, $dob_str;
742 my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
744 my $age = $today_y - $dob_y;
745 if ( $dob_m . $dob_d > $today_m . $today_d ) {
754 my $account = $patron->account
760 return Koha::Account->new( { patron_id => $self->borrowernumber } );
765 my $holds = $patron->holds
767 Return all the holds placed by this patron
773 my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
774 return Koha::Holds->_new_from_dbic($holds_rs);
779 my $old_holds = $patron->old_holds
781 Return all the historical holds for this patron
787 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
788 return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
791 =head3 notice_email_address
793 my $email = $patron->notice_email_address;
795 Return the email address of patron used for notices.
796 Returns the empty string if no email address.
800 sub notice_email_address{
803 my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
804 # if syspref is set to 'first valid' (value == OFF), look up email address
805 if ( $which_address eq 'OFF' ) {
806 return $self->first_valid_email_address;
809 return $self->$which_address || '';
812 =head3 first_valid_email_address
814 my $first_valid_email_address = $patron->first_valid_email_address
816 Return the first valid email address for a patron.
817 For now, the order is defined as email, emailpro, B_email.
818 Returns the empty string if the borrower has no email addresses.
822 sub first_valid_email_address {
825 return $self->email() || $self->emailpro() || $self->B_email() || q{};
828 =head3 get_club_enrollments
832 sub get_club_enrollments {
833 my ( $self, $return_scalar ) = @_;
835 my $e = Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
837 return $e if $return_scalar;
839 return wantarray ? $e->as_list : $e;
842 =head3 get_enrollable_clubs
846 sub get_enrollable_clubs {
847 my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
850 $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
851 if $is_enrollable_from_opac;
852 $params->{is_email_required} = 0 unless $self->first_valid_email_address();
854 $params->{borrower} = $self;
856 my $e = Koha::Clubs->get_enrollable($params);
858 return $e if $return_scalar;
860 return wantarray ? $e->as_list : $e;
863 =head3 account_locked
865 my $is_locked = $patron->account_locked
867 Return true if the patron has reach the maximum number of login attempts (see pref FailedLoginAttempts).
868 Otherwise return false.
869 If the pref is not set (empty string, null or 0), the feature is considered as disabled.
875 my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
876 return ( $FailedLoginAttempts
877 and $self->login_attempts
878 and $self->login_attempts >= $FailedLoginAttempts )? 1 : 0;
881 =head3 can_see_patron_infos
883 my $can_see = $patron->can_see_patron_infos( $patron );
885 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
889 sub can_see_patron_infos {
890 my ( $self, $patron ) = @_;
891 return $self->can_see_patrons_from( $patron->library->branchcode );
894 =head3 can_see_patrons_from
896 my $can_see = $patron->can_see_patrons_from( $branchcode );
898 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
902 sub can_see_patrons_from {
903 my ( $self, $branchcode ) = @_;
905 if ( $self->branchcode eq $branchcode ) {
907 } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
909 } elsif ( my $library_groups = $self->library->library_groups ) {
910 while ( my $library_group = $library_groups->next ) {
911 if ( $library_group->parent->has_child( $branchcode ) ) {
920 =head3 libraries_where_can_see_patrons
922 my $libraries = $patron-libraries_where_can_see_patrons;
924 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
925 The branchcodes are arbitrarily returned sorted.
926 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
928 An empty array means no restriction, the patron can see patron's infos from any libraries.
932 sub libraries_where_can_see_patrons {
934 my $userenv = C4::Context->userenv;
936 return () unless $userenv; # For tests, but userenv should be defined in tests...
938 my @restricted_branchcodes;
939 if (C4::Context::only_my_library) {
940 push @restricted_branchcodes, $self->branchcode;
944 $self->has_permission(
945 { borrowers => 'view_borrower_infos_from_any_libraries' }
949 my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
950 if ( $library_groups->count )
952 while ( my $library_group = $library_groups->next ) {
953 my $parent = $library_group->parent;
954 if ( $parent->has_child( $self->branchcode ) ) {
955 push @restricted_branchcodes, $parent->children->get_column('branchcode');
960 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
964 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
965 @restricted_branchcodes = uniq(@restricted_branchcodes);
966 @restricted_branchcodes = sort(@restricted_branchcodes);
967 return @restricted_branchcodes;
971 my ( $self, $flagsrequired ) = @_;
972 return unless $self->userid;
973 # TODO code from haspermission needs to be moved here!
974 return C4::Auth::haspermission( $self->userid, $flagsrequired );
979 my $is_adult = $patron->is_adult
981 Return true if the patron has a category with a type Adult (A) or Organization (I)
987 return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
992 my $is_child = $patron->is_child
994 Return true if the patron has a category with a type Child (C)
999 return $self->category->category_type eq 'C' ? 1 : 0;
1002 =head3 has_valid_userid
1004 my $patron = Koha::Patrons->find(42);
1005 $patron->userid( $new_userid );
1006 my $has_a_valid_userid = $patron->has_valid_userid
1008 my $patron = Koha::Patron->new( $params );
1009 my $has_a_valid_userid = $patron->has_valid_userid
1011 Return true if the current userid of this patron is valid/unique, otherwise false.
1013 Note that this should be done in $self->store instead and raise an exception if needed.
1017 sub has_valid_userid {
1020 return 0 unless $self->userid;
1022 return 0 if ( $self->userid eq C4::Context->config('user') ); # DB user
1024 my $already_exists = Koha::Patrons->search(
1026 userid => $self->userid,
1029 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1034 return $already_exists ? 0 : 1;
1037 =head3 generate_userid
1039 my $patron = Koha::Patron->new( $params );
1040 my $userid = $patron->generate_userid
1042 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1044 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).
1046 # Note: Should we set $self->userid with the generated value?
1047 # Certainly yes, but we AddMember and ModMember will be rewritten
1051 sub generate_userid {
1055 my $existing_userid = $self->userid;
1056 my $firstname = $self->firstname // q{};
1057 my $surname = $self->surname // q{};
1058 #The script will "do" the following code and increment the $offset until the generated userid is unique
1060 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1061 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1062 $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1063 $userid = unac_string('utf-8',$userid);
1064 $userid .= $offset unless $offset == 0;
1065 $self->userid( $userid );
1067 } while (! $self->has_valid_userid );
1069 # Resetting to the previous value as the callers do not expect
1070 # this method to modify the userid attribute
1071 # This will be done later (move of AddMember and ModMember)
1072 $self->userid( $existing_userid );
1078 =head2 Internal methods
1090 Kyle M Hall <kyle@bywatersolutions.com>
1091 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>