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
90 Delete patron's holds, lists and finally the patron.
92 Lists owned by the borrower are deleted, but entries from the borrower to
101 $self->_result->result_source->schema->txn_do(
103 # Delete Patron's holds
104 $self->holds->delete;
106 # Delete all lists and all shares of this borrower
107 # Consistent with the approach Koha uses on deleting individual lists
108 # Note that entries in virtualshelfcontents added by this borrower to
109 # lists of others will be handled by a table constraint: the borrower
110 # is set to NULL in those entries.
112 # We could handle the above deletes via a constraint too.
113 # But a new BZ report 11889 has been opened to discuss another approach.
114 # Instead of deleting we could also disown lists (based on a pref).
115 # In that way we could save shared and public lists.
116 # The current table constraints support that idea now.
117 # This pref should then govern the results of other routines/methods such as
118 # Koha::Virtualshelf->new->delete too.
119 # FIXME Could be $patron->get_lists
120 $_->delete for Koha
::Virtualshelves
->search( { owner
=> $self->borrowernumber } );
122 $deleted = $self->SUPER::delete;
124 logaction
( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4
::Context
->preference("BorrowersLog");
133 my $patron_category = $patron->category
135 Return the patron category for this patron
141 return Koha
::Patron
::Category
->_new_from_dbic( $self->_result->categorycode );
146 Returns a Koha::Patron object for this patron's guarantor
153 return unless $self->guarantorid();
155 return Koha
::Patrons
->find( $self->guarantorid() );
161 return scalar Koha
::Patron
::Images
->find( $self->borrowernumber );
166 return Koha
::Library
->_new_from_dbic($self->_result->branchcode);
171 Returns the guarantees (list of Koha::Patron) of this patron
178 return Koha
::Patrons
->search( { guarantorid
=> $self->borrowernumber } );
181 =head3 housebound_profile
183 Returns the HouseboundProfile associated with this patron.
187 sub housebound_profile
{
189 my $profile = $self->_result->housebound_profile;
190 return Koha
::Patron
::HouseboundProfile
->_new_from_dbic($profile)
195 =head3 housebound_role
197 Returns the HouseboundRole associated with this patron.
201 sub housebound_role
{
204 my $role = $self->_result->housebound_role;
205 return Koha
::Patron
::HouseboundRole
->_new_from_dbic($role) if ( $role );
211 Returns the siblings of this patron.
218 my $guarantor = $self->guarantor;
220 return unless $guarantor;
222 return Koha
::Patrons
->search(
226 '=' => $guarantor->id,
229 '!=' => $self->borrowernumber,
237 my $patron = Koha::Patrons->find($id);
238 $patron->merge_with( \@patron_ids );
240 This subroutine merges a list of patrons into the patron record. This is accomplished by finding
241 all related patron ids for the patrons to be merged in other tables and changing the ids to be that
242 of the keeper patron.
247 my ( $self, $patron_ids ) = @_;
249 my @patron_ids = @
{ $patron_ids };
251 # Ensure the keeper isn't in the list of patrons to merge
252 @patron_ids = grep { $_ ne $self->id } @patron_ids;
254 my $schema = Koha
::Database
->new()->schema();
258 $self->_result->result_source->schema->txn_do( sub {
259 foreach my $patron_id (@patron_ids) {
260 my $patron = Koha
::Patrons
->find( $patron_id );
264 # Unbless for safety, the patron will end up being deleted
265 $results->{merged
}->{$patron_id}->{patron
} = $patron->unblessed;
267 while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
268 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
269 $results->{merged
}->{ $patron_id }->{updated
}->{$r} = $rs->count();
270 $rs->update({ $field => $self->id });
273 $patron->move_to_deleted();
283 =head3 wants_check_for_previous_checkout
285 $wants_check = $patron->wants_check_for_previous_checkout;
287 Return 1 if Koha needs to perform PrevIssue checking, else 0.
291 sub wants_check_for_previous_checkout
{
293 my $syspref = C4
::Context
->preference("checkPrevCheckout");
296 ## Hard syspref trumps all
297 return 1 if ($syspref eq 'hardyes');
298 return 0 if ($syspref eq 'hardno');
299 ## Now, patron pref trumps all
300 return 1 if ($self->checkprevcheckout eq 'yes');
301 return 0 if ($self->checkprevcheckout eq 'no');
303 # More complex: patron inherits -> determine category preference
304 my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
305 return 1 if ($checkPrevCheckoutByCat eq 'yes');
306 return 0 if ($checkPrevCheckoutByCat eq 'no');
308 # Finally: category preference is inherit, default to 0
309 if ($syspref eq 'softyes') {
316 =head3 do_check_for_previous_checkout
318 $do_check = $patron->do_check_for_previous_checkout($item);
320 Return 1 if the bib associated with $ITEM has previously been checked out to
321 $PATRON, 0 otherwise.
325 sub do_check_for_previous_checkout
{
326 my ( $self, $item ) = @_;
328 # Find all items for bib and extract item numbers.
329 my @items = Koha
::Items
->search({biblionumber
=> $item->{biblionumber
}});
331 foreach my $item (@items) {
332 push @item_nos, $item->itemnumber;
335 # Create (old)issues search criteria
337 borrowernumber
=> $self->borrowernumber,
338 itemnumber
=> \
@item_nos,
341 # Check current issues table
342 my $issues = Koha
::Checkouts
->search($criteria);
343 return 1 if $issues->count; # 0 || N
345 # Check old issues table
346 my $old_issues = Koha
::Old
::Checkouts
->search($criteria);
347 return $old_issues->count; # 0 || N
352 my $debarment_expiration = $patron->is_debarred;
354 Returns the date a patron debarment will expire, or undef if the patron is not
362 return unless $self->debarred;
363 return $self->debarred
364 if $self->debarred =~ '^9999'
365 or dt_from_string
( $self->debarred ) > dt_from_string
;
371 my $is_expired = $patron->is_expired;
373 Returns 1 if the patron is expired or 0;
379 return 0 unless $self->dateexpiry;
380 return 0 if $self->dateexpiry =~ '^9999';
381 return 1 if dt_from_string
( $self->dateexpiry ) < dt_from_string
->truncate( to
=> 'day' );
385 =head3 is_going_to_expire
387 my $is_going_to_expire = $patron->is_going_to_expire;
389 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
393 sub is_going_to_expire
{
396 my $delay = C4
::Context
->preference('NotifyBorrowerDeparture') || 0;
398 return 0 unless $delay;
399 return 0 unless $self->dateexpiry;
400 return 0 if $self->dateexpiry =~ '^9999';
401 return 1 if dt_from_string
( $self->dateexpiry )->subtract( days
=> $delay ) < dt_from_string
->truncate( to
=> 'day' );
405 =head3 update_password
407 my $updated = $patron->update_password( $userid, $password );
409 Update the userid and the password of a patron.
410 If the userid already exists, returns and let DBIx::Class warns
411 This will add an entry to action_logs if BorrowersLog is set.
415 sub update_password
{
416 my ( $self, $userid, $password ) = @_;
417 eval { $self->userid($userid)->store; };
418 return if $@
; # Make sure the userid is not already in used by another patron
421 password
=> $password,
425 logaction
( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" ) if C4
::Context
->preference("BorrowersLog");
431 my $new_expiry_date = $patron->renew_account
433 Extending the subscription to the expiry date.
440 if ( C4
::Context
->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
441 $date = ( dt_from_string
gt dt_from_string
( $self->dateexpiry ) ) ? dt_from_string
: dt_from_string
( $self->dateexpiry );
444 C4
::Context
->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
445 ? dt_from_string
( $self->dateexpiry )
448 my $expiry_date = $self->category->get_expiry_date($date);
450 $self->dateexpiry($expiry_date);
451 $self->date_renewed( dt_from_string
() );
454 $self->add_enrolment_fee_if_needed;
456 logaction
( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4
::Context
->preference("BorrowersLog");
457 return dt_from_string
( $expiry_date )->truncate( to
=> 'day' );
462 my $has_overdues = $patron->has_overdues;
464 Returns the number of patron's overdues
470 my $dtf = Koha
::Database
->new->schema->storage->datetime_parser;
471 return $self->_result->issues->search({ date_due
=> { '<' => $dtf->format_datetime( dt_from_string
() ) } })->count;
476 $patron->track_login;
477 $patron->track_login({ force => 1 });
479 Tracks a (successful) login attempt.
480 The preference TrackLastPatronActivity must be enabled. Or you
481 should pass the force parameter.
486 my ( $self, $params ) = @_;
489 !C4
::Context
->preference('TrackLastPatronActivity');
490 $self->lastseen( dt_from_string
() )->store;
493 =head3 move_to_deleted
495 my $is_moved = $patron->move_to_deleted;
497 Move a patron to the deletedborrowers table.
498 This can be done before deleting a patron, to make sure the data are not completely deleted.
502 sub move_to_deleted
{
504 my $patron_infos = $self->unblessed;
505 delete $patron_infos->{updated_on
}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
506 return Koha
::Database
->new->schema->resultset('Deletedborrower')->create($patron_infos);
509 =head3 article_requests
511 my @requests = $borrower->article_requests();
512 my $requests = $borrower->article_requests();
514 Returns either a list of ArticleRequests objects,
515 or an ArtitleRequests object, depending on the
520 sub article_requests
{
523 $self->{_article_requests
} ||= Koha
::ArticleRequests
->search({ borrowernumber
=> $self->borrowernumber() });
525 return $self->{_article_requests
};
528 =head3 article_requests_current
530 my @requests = $patron->article_requests_current
532 Returns the article requests associated with this patron that are incomplete
536 sub article_requests_current
{
539 $self->{_article_requests_current
} ||= Koha
::ArticleRequests
->search(
541 borrowernumber
=> $self->id(),
543 { status
=> Koha
::ArticleRequest
::Status
::Pending
},
544 { status
=> Koha
::ArticleRequest
::Status
::Processing
}
549 return $self->{_article_requests_current
};
552 =head3 article_requests_finished
554 my @requests = $biblio->article_requests_finished
556 Returns the article requests associated with this patron that are completed
560 sub article_requests_finished
{
561 my ( $self, $borrower ) = @_;
563 $self->{_article_requests_finished
} ||= Koha
::ArticleRequests
->search(
565 borrowernumber
=> $self->id(),
567 { status
=> Koha
::ArticleRequest
::Status
::Completed
},
568 { status
=> Koha
::ArticleRequest
::Status
::Canceled
}
573 return $self->{_article_requests_finished
};
576 =head3 add_enrolment_fee_if_needed
578 my $enrolment_fee = $patron->add_enrolment_fee_if_needed;
580 Add enrolment fee for a patron if needed.
584 sub add_enrolment_fee_if_needed
{
586 my $enrolment_fee = $self->category->enrolmentfee;
587 if ( $enrolment_fee && $enrolment_fee > 0 ) {
588 # insert fee in patron debts
589 C4
::Accounts
::manualinvoice
( $self->borrowernumber, '', '', 'A', $enrolment_fee );
591 return $enrolment_fee || 0;
596 my $checkouts = $patron->checkouts
602 my $checkouts = $self->_result->issues;
603 return Koha
::Checkouts
->_new_from_dbic( $checkouts );
606 =head3 pending_checkouts
608 my $pending_checkouts = $patron->pending_checkouts
610 This method will return the same as $self->checkouts, but with a prefetch on
611 items, biblio and biblioitems.
613 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
615 It should not be used directly, prefer to access fields you need instead of
616 retrieving all these fields in one go.
621 sub pending_checkouts
{
623 my $checkouts = $self->_result->issues->search(
627 { -desc
=> 'me.timestamp' },
628 { -desc
=> 'issuedate' },
629 { -desc
=> 'issue_id' }, # Sort by issue_id should be enough
631 prefetch
=> { item
=> { biblio
=> 'biblioitems' } },
634 return Koha
::Checkouts
->_new_from_dbic( $checkouts );
639 my $old_checkouts = $patron->old_checkouts
645 my $old_checkouts = $self->_result->old_issues;
646 return Koha
::Old
::Checkouts
->_new_from_dbic( $old_checkouts );
651 my $overdue_items = $patron->get_overdues
653 Return the overdue items
659 my $dtf = Koha
::Database
->new->schema->storage->datetime_parser;
660 return $self->checkouts->search(
662 'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string
) },
665 prefetch
=> { item
=> { biblio
=> 'biblioitems' } },
670 =head3 get_routing_lists
672 my @routinglists = $patron->get_routing_lists
674 Returns the routing lists a patron is subscribed to.
678 sub get_routing_lists
{
680 my $routing_list_rs = $self->_result->subscriptionroutinglists;
681 return Koha
::Subscription
::Routinglists
->_new_from_dbic($routing_list_rs);
686 my $age = $patron->get_age
688 Return the age of the patron
694 my $today_str = dt_from_string
->strftime("%Y-%m-%d");
695 return unless $self->dateofbirth;
696 my $dob_str = dt_from_string
( $self->dateofbirth )->strftime("%Y-%m-%d");
698 my ( $dob_y, $dob_m, $dob_d ) = split /-/, $dob_str;
699 my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
701 my $age = $today_y - $dob_y;
702 if ( $dob_m . $dob_d > $today_m . $today_d ) {
711 my $account = $patron->account
717 return Koha
::Account
->new( { patron_id
=> $self->borrowernumber } );
722 my $holds = $patron->holds
724 Return all the holds placed by this patron
730 my $holds_rs = $self->_result->reserves->search( {}, { order_by
=> 'reservedate' } );
731 return Koha
::Holds
->_new_from_dbic($holds_rs);
736 my $old_holds = $patron->old_holds
738 Return all the historical holds for this patron
744 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by
=> 'reservedate' } );
745 return Koha
::Old
::Holds
->_new_from_dbic($old_holds_rs);
748 =head3 notice_email_address
750 my $email = $patron->notice_email_address;
752 Return the email address of patron used for notices.
753 Returns the empty string if no email address.
757 sub notice_email_address
{
760 my $which_address = C4
::Context
->preference("AutoEmailPrimaryAddress");
761 # if syspref is set to 'first valid' (value == OFF), look up email address
762 if ( $which_address eq 'OFF' ) {
763 return $self->first_valid_email_address;
766 return $self->$which_address || '';
769 =head3 first_valid_email_address
771 my $first_valid_email_address = $patron->first_valid_email_address
773 Return the first valid email address for a patron.
774 For now, the order is defined as email, emailpro, B_email.
775 Returns the empty string if the borrower has no email addresses.
779 sub first_valid_email_address
{
782 return $self->email() || $self->emailpro() || $self->B_email() || q{};
785 =head3 get_club_enrollments
789 sub get_club_enrollments
{
790 my ( $self, $return_scalar ) = @_;
792 my $e = Koha
::Club
::Enrollments
->search( { borrowernumber
=> $self->borrowernumber(), date_canceled
=> undef } );
794 return $e if $return_scalar;
796 return wantarray ?
$e->as_list : $e;
799 =head3 get_enrollable_clubs
803 sub get_enrollable_clubs
{
804 my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
807 $params->{is_enrollable_from_opac
} = $is_enrollable_from_opac
808 if $is_enrollable_from_opac;
809 $params->{is_email_required
} = 0 unless $self->first_valid_email_address();
811 $params->{borrower
} = $self;
813 my $e = Koha
::Clubs
->get_enrollable($params);
815 return $e if $return_scalar;
817 return wantarray ?
$e->as_list : $e;
820 =head3 account_locked
822 my $is_locked = $patron->account_locked
824 Return true if the patron has reach the maximum number of login attempts (see pref FailedLoginAttempts).
825 Otherwise return false.
826 If the pref is not set (empty string, null or 0), the feature is considered as disabled.
832 my $FailedLoginAttempts = C4
::Context
->preference('FailedLoginAttempts');
833 return ( $FailedLoginAttempts
834 and $self->login_attempts
835 and $self->login_attempts >= $FailedLoginAttempts )?
1 : 0;
838 =head3 can_see_patron_infos
840 my $can_see = $patron->can_see_patron_infos( $patron );
842 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
846 sub can_see_patron_infos
{
847 my ( $self, $patron ) = @_;
848 return $self->can_see_patrons_from( $patron->library->branchcode );
851 =head3 can_see_patrons_from
853 my $can_see = $patron->can_see_patrons_from( $branchcode );
855 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
859 sub can_see_patrons_from
{
860 my ( $self, $branchcode ) = @_;
862 if ( $self->branchcode eq $branchcode ) {
864 } elsif ( $self->has_permission( { borrowers
=> 'view_borrower_infos_from_any_libraries' } ) ) {
866 } elsif ( my $library_groups = $self->library->library_groups ) {
867 while ( my $library_group = $library_groups->next ) {
868 if ( $library_group->parent->has_child( $branchcode ) ) {
877 =head3 libraries_where_can_see_patrons
879 my $libraries = $patron-libraries_where_can_see_patrons;
881 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
882 The branchcodes are arbitrarily returned sorted.
883 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
885 An empty array means no restriction, the patron can see patron's infos from any libraries.
889 sub libraries_where_can_see_patrons
{
891 my $userenv = C4
::Context
->userenv;
893 return () unless $userenv; # For tests, but userenv should be defined in tests...
895 my @restricted_branchcodes;
896 if (C4
::Context
::only_my_library
) {
897 push @restricted_branchcodes, $self->branchcode;
901 $self->has_permission(
902 { borrowers
=> 'view_borrower_infos_from_any_libraries' }
906 my $library_groups = $self->library->library_groups({ ft_hide_patron_info
=> 1 });
907 if ( $library_groups->count )
909 while ( my $library_group = $library_groups->next ) {
910 my $parent = $library_group->parent;
911 if ( $parent->has_child( $self->branchcode ) ) {
912 push @restricted_branchcodes, $parent->children->get_column('branchcode');
917 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
921 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
922 @restricted_branchcodes = uniq
(@restricted_branchcodes);
923 @restricted_branchcodes = sort(@restricted_branchcodes);
924 return @restricted_branchcodes;
928 my ( $self, $flagsrequired ) = @_;
929 return unless $self->userid;
930 # TODO code from haspermission needs to be moved here!
931 return C4
::Auth
::haspermission
( $self->userid, $flagsrequired );
936 my $is_adult = $patron->is_adult
938 Return true if the patron has a category with a type Adult (A) or Organization (I)
944 return $self->category->category_type =~ /^(A|I)$/ ?
1 : 0;
949 my $is_child = $patron->is_child
951 Return true if the patron has a category with a type Child (C)
956 return $self->category->category_type eq 'C' ?
1 : 0;
959 =head3 has_valid_userid
961 my $patron = Koha::Patrons->find(42);
962 $patron->userid( $new_userid );
963 my $has_a_valid_userid = $patron->has_valid_userid
965 my $patron = Koha::Patron->new( $params );
966 my $has_a_valid_userid = $patron->has_valid_userid
968 Return true if the current userid of this patron is valid/unique, otherwise false.
970 Note that this should be done in $self->store instead and raise an exception if needed.
974 sub has_valid_userid
{
977 return 0 unless $self->userid;
979 return 0 if ( $self->userid eq C4
::Context
->config('user') ); # DB user
981 my $already_exists = Koha
::Patrons
->search(
983 userid
=> $self->userid,
986 ?
( borrowernumber
=> { '!=' => $self->borrowernumber } )
991 return $already_exists ?
0 : 1;
994 =head3 generate_userid
996 my $patron = Koha::Patron->new( $params );
997 my $userid = $patron->generate_userid
999 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1001 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).
1003 # Note: Should we set $self->userid with the generated value?
1004 # Certainly yes, but we AddMember and ModMember will be rewritten
1008 sub generate_userid
{
1012 my $existing_userid = $self->userid;
1013 my $firstname = $self->firstname // q{};
1014 my $surname = $self->surname // q{};
1015 #The script will "do" the following code and increment the $offset until the generated userid is unique
1017 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1018 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1019 $userid = lc(($firstname)?
"$firstname.$surname" : $surname);
1020 $userid = unac_string
('utf-8',$userid);
1021 $userid .= $offset unless $offset == 0;
1022 $self->userid( $userid );
1024 } while (! $self->has_valid_userid );
1026 # Resetting to the previous value as the callers do not expect
1027 # this method to modify the userid attribute
1028 # This will be done later (move of AddMember and ModMember)
1029 $self->userid( $existing_userid );
1035 =head2 Internal methods
1047 Kyle M Hall <kyle@bywatersolutions.com>
1048 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>