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 JSON qw( to_json );
26 use Module::Load::Conditional qw( can_load );
27 use Text::Unaccent qw( unac_string );
36 use Koha::Old::Checkouts;
37 use Koha::Patron::Categories;
38 use Koha::Patron::HouseboundProfile;
39 use Koha::Patron::HouseboundRole;
40 use Koha::Patron::Images;
42 use Koha::Virtualshelves;
43 use Koha::Club::Enrollments;
45 use Koha::Subscription::Routinglists;
47 if ( ! can_load( modules => { 'Koha::NorwegianPatronDB' => undef } ) ) {
48 warn "Unable to load Koha::NorwegianPatronDB";
51 use base qw(Koha::Object);
53 our $RESULTSET_PATRON_ID_MAPPING = {
54 Accountline => 'borrowernumber',
55 Aqbasketuser => 'borrowernumber',
56 Aqbudget => 'budget_owner_id',
57 Aqbudgetborrower => 'borrowernumber',
58 ArticleRequest => 'borrowernumber',
59 BorrowerAttribute => 'borrowernumber',
60 BorrowerDebarment => 'borrowernumber',
61 BorrowerFile => 'borrowernumber',
62 BorrowerModification => 'borrowernumber',
63 ClubEnrollment => 'borrowernumber',
64 Issue => 'borrowernumber',
65 ItemsLastBorrower => 'borrowernumber',
66 Linktracker => 'borrowernumber',
67 Message => 'borrowernumber',
68 MessageQueue => 'borrowernumber',
69 OldIssue => 'borrowernumber',
70 OldReserve => 'borrowernumber',
71 Rating => 'borrowernumber',
72 Reserve => 'borrowernumber',
73 Review => 'borrowernumber',
74 SearchHistory => 'userid',
75 Statistic => 'borrowernumber',
76 Suggestion => 'suggestedby',
77 TagAll => 'borrowernumber',
78 Virtualshelfcontent => 'borrowernumber',
79 Virtualshelfshare => 'borrowernumber',
80 Virtualshelve => 'owner',
85 Koha::Patron - Koha Patron Object class
98 my ( $class, $params ) = @_;
100 return $class->SUPER::new($params);
103 =head3 fixup_cardnumber
105 Autogenerate next cardnumber from highest value found in database
109 sub fixup_cardnumber {
111 my $max = Koha::Patrons->search({
112 cardnumber => {-regexp => '^-?[0-9]+$'}
114 select => \'CAST(cardnumber AS SIGNED)',
115 as => ['cast_cardnumber']
116 })->_resultset->get_column('cast_cardnumber')->max;
117 $self->cardnumber(($max || 0) +1);
120 =head3 trim_whitespace
122 trim whitespace from data which has some non-whitespace in it.
123 Could be moved to Koha::Object if need to be reused
127 sub trim_whitespaces {
130 my $schema = Koha::Database->new->schema;
131 my @columns = $schema->source($self->_type)->columns;
133 for my $column( @columns ) {
134 my $value = $self->$column;
135 if ( defined $value ) {
136 $value =~ s/^\s*|\s*$//g;
137 $self->$column($value);
143 =head3 plain_text_password
145 $patron->plain_text_password( $password );
147 stores a copy of the unencrypted password in the object
148 for use in code before encrypting for db
152 sub plain_text_password {
153 my ( $self, $password ) = @_;
155 $self->{_plain_text_password} = $password;
158 return $self->{_plain_text_password}
159 if $self->{_plain_text_password};
166 Patron specific store method to cleanup record
167 and do other necessary things before saving
175 $self->_result->result_source->schema->txn_do(
178 C4::Context->preference("autoMemberNum")
179 and ( not defined $self->cardnumber
180 or $self->cardnumber eq '' )
183 # Warning: The caller is responsible for locking the members table in write
184 # mode, to avoid database corruption.
185 # We are in a transaction but the table is not locked
186 $self->fixup_cardnumber;
189 unless( $self->category->in_storage ) {
190 Koha::Exceptions::Object::FKConstraint->throw(
191 broken_fk => 'categorycode',
192 value => $self->categorycode,
196 $self->trim_whitespaces;
198 # We don't want invalid dates in the db (mysql has a bad habit of inserting 0000-00-00)
199 $self->dateofbirth(undef) unless $self->dateofbirth;
200 $self->debarred(undef) unless $self->debarred;
202 # Set default values if not set
203 $self->sms_provider_id(undef) unless $self->sms_provider_id;
204 $self->guarantorid(undef) unless $self->guarantorid;
206 unless ( $self->in_storage ) { #AddMember
208 # Generate a valid userid/login if needed
209 $self->generate_userid
210 if not $self->userid or not $self->has_valid_userid;
212 # Add expiration date if it isn't already there
213 unless ( $self->dateexpiry ) {
214 $self->dateexpiry( $self->category->get_expiry_date );
217 # Add enrollment date if it isn't already there
218 unless ( $self->dateenrolled ) {
219 $self->dateenrolled(dt_from_string);
222 # Set the privacy depending on the patron's category
223 my $default_privacy = $self->category->default_privacy || q{};
225 $default_privacy eq 'default' ? 1
226 : $default_privacy eq 'never' ? 2
227 : $default_privacy eq 'forever' ? 0
229 $self->privacy($default_privacy);
231 unless ( defined $self->privacy_guarantor_checkouts ) {
232 $self->privacy_guarantor_checkouts(0);
235 # Make a copy of the plain text password for later use
236 $self->plain_text_password( $self->password );
238 # Create a disabled account if no password provided
239 $self->password( $self->password
240 ? Koha::AuthUtils::hash_password( $self->password )
243 $self->borrowernumber(undef);
245 $self = $self->SUPER::store;
247 # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
248 # cronjob will use for syncing with NL
249 if ( C4::Context->preference('NorwegianPatronDBEnable')
250 && C4::Context->preference('NorwegianPatronDBEnable') == 1 )
252 Koha::Database->new->schema->resultset('BorrowerSync')
255 'borrowernumber' => $self->borrowernumber,
256 'synctype' => 'norwegianpatrondb',
258 'syncstatus' => 'new',
260 Koha::NorwegianPatronDB::NLEncryptPIN($self->plain_text_password),
265 $self->add_enrolment_fee_if_needed;
267 logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
268 if C4::Context->preference("BorrowersLog");
272 # Come from ModMember, but should not be possible (?)
273 $self->dateenrolled(undef) unless $self->dateenrolled;
274 $self->dateexpiry(undef) unless $self->dateexpiry;
277 my $self_from_storage = $self->get_from_storage;
278 # FIXME We should not deal with that here, callers have to do this job
279 # Moved from ModMember to prevent regressions
280 unless ( $self->userid ) {
281 my $stored_userid = $self_from_storage->userid;
282 $self->userid($stored_userid);
285 # Password must be updated using $self->update_password
286 $self->password($self_from_storage->password);
288 if ( C4::Context->preference('FeeOnChangePatronCategory')
289 and $self->category->categorycode ne
290 $self_from_storage->category->categorycode )
292 $self->add_enrolment_fee_if_needed;
295 # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
296 # cronjob will use for syncing with NL
297 if ( C4::Context->preference('NorwegianPatronDBEnable')
298 && C4::Context->preference('NorwegianPatronDBEnable') == 1 )
300 my $borrowersync = Koha::Database->new->schema->resultset('BorrowerSync')->find({
301 'synctype' => 'norwegianpatrondb',
302 'borrowernumber' => $self->borrowernumber,
304 # Do not set to "edited" if syncstatus is "new". We need to sync as new before
305 # we can sync as changed. And the "new sync" will pick up all changes since
306 # the patron was created anyway.
307 if ( $borrowersync->syncstatus ne 'new' && $borrowersync->syncstatus ne 'delete' ) {
308 $borrowersync->update( { 'syncstatus' => 'edited' } );
310 # Set the value of 'sync'
311 # FIXME THIS IS BROKEN # $borrowersync->update( { 'sync' => $data{'sync'} } );
313 # Try to do the live sync
314 Koha::NorwegianPatronDB::NLSync({ 'borrowernumber' => $self->borrowernumber });
317 my $borrowers_log = C4::Context->preference("BorrowersLog");
318 my $previous_cardnumber = $self_from_storage->cardnumber;
320 && ( !defined $previous_cardnumber
321 || $previous_cardnumber ne $self->cardnumber )
327 $self->borrowernumber,
330 cardnumber_replaced => {
331 previous_cardnumber => $previous_cardnumber,
332 new_cardnumber => $self->cardnumber,
335 { utf8 => 1, pretty => 1 }
340 logaction( "MEMBERS", "MODIFY", $self->borrowernumber,
341 "UPDATE (executed w/ arg: " . $self->borrowernumber . ")" )
344 $self = $self->SUPER::store;
355 Delete patron's holds, lists and finally the patron.
357 Lists owned by the borrower are deleted, but entries from the borrower to
358 other lists are kept.
366 $self->_result->result_source->schema->txn_do(
368 # Delete Patron's holds
369 $self->holds->delete;
371 # Delete all lists and all shares of this borrower
372 # Consistent with the approach Koha uses on deleting individual lists
373 # Note that entries in virtualshelfcontents added by this borrower to
374 # lists of others will be handled by a table constraint: the borrower
375 # is set to NULL in those entries.
377 # We could handle the above deletes via a constraint too.
378 # But a new BZ report 11889 has been opened to discuss another approach.
379 # Instead of deleting we could also disown lists (based on a pref).
380 # In that way we could save shared and public lists.
381 # The current table constraints support that idea now.
382 # This pref should then govern the results of other routines/methods such as
383 # Koha::Virtualshelf->new->delete too.
384 # FIXME Could be $patron->get_lists
385 $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
387 $deleted = $self->SUPER::delete;
389 logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
398 my $patron_category = $patron->category
400 Return the patron category for this patron
406 return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
411 Returns a Koha::Patron object for this patron's guarantor
418 return unless $self->guarantorid();
420 return Koha::Patrons->find( $self->guarantorid() );
426 return scalar Koha::Patron::Images->find( $self->borrowernumber );
431 return Koha::Library->_new_from_dbic($self->_result->branchcode);
436 Returns the guarantees (list of Koha::Patron) of this patron
443 return Koha::Patrons->search( { guarantorid => $self->borrowernumber } );
446 =head3 housebound_profile
448 Returns the HouseboundProfile associated with this patron.
452 sub housebound_profile {
454 my $profile = $self->_result->housebound_profile;
455 return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
460 =head3 housebound_role
462 Returns the HouseboundRole associated with this patron.
466 sub housebound_role {
469 my $role = $self->_result->housebound_role;
470 return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
476 Returns the siblings of this patron.
483 my $guarantor = $self->guarantor;
485 return unless $guarantor;
487 return Koha::Patrons->search(
491 '=' => $guarantor->id,
494 '!=' => $self->borrowernumber,
502 my $patron = Koha::Patrons->find($id);
503 $patron->merge_with( \@patron_ids );
505 This subroutine merges a list of patrons into the patron record. This is accomplished by finding
506 all related patron ids for the patrons to be merged in other tables and changing the ids to be that
507 of the keeper patron.
512 my ( $self, $patron_ids ) = @_;
514 my @patron_ids = @{ $patron_ids };
516 # Ensure the keeper isn't in the list of patrons to merge
517 @patron_ids = grep { $_ ne $self->id } @patron_ids;
519 my $schema = Koha::Database->new()->schema();
523 $self->_result->result_source->schema->txn_do( sub {
524 foreach my $patron_id (@patron_ids) {
525 my $patron = Koha::Patrons->find( $patron_id );
529 # Unbless for safety, the patron will end up being deleted
530 $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
532 while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
533 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
534 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
535 $rs->update({ $field => $self->id });
538 $patron->move_to_deleted();
548 =head3 wants_check_for_previous_checkout
550 $wants_check = $patron->wants_check_for_previous_checkout;
552 Return 1 if Koha needs to perform PrevIssue checking, else 0.
556 sub wants_check_for_previous_checkout {
558 my $syspref = C4::Context->preference("checkPrevCheckout");
561 ## Hard syspref trumps all
562 return 1 if ($syspref eq 'hardyes');
563 return 0 if ($syspref eq 'hardno');
564 ## Now, patron pref trumps all
565 return 1 if ($self->checkprevcheckout eq 'yes');
566 return 0 if ($self->checkprevcheckout eq 'no');
568 # More complex: patron inherits -> determine category preference
569 my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
570 return 1 if ($checkPrevCheckoutByCat eq 'yes');
571 return 0 if ($checkPrevCheckoutByCat eq 'no');
573 # Finally: category preference is inherit, default to 0
574 if ($syspref eq 'softyes') {
581 =head3 do_check_for_previous_checkout
583 $do_check = $patron->do_check_for_previous_checkout($item);
585 Return 1 if the bib associated with $ITEM has previously been checked out to
586 $PATRON, 0 otherwise.
590 sub do_check_for_previous_checkout {
591 my ( $self, $item ) = @_;
593 # Find all items for bib and extract item numbers.
594 my @items = Koha::Items->search({biblionumber => $item->{biblionumber}});
596 foreach my $item (@items) {
597 push @item_nos, $item->itemnumber;
600 # Create (old)issues search criteria
602 borrowernumber => $self->borrowernumber,
603 itemnumber => \@item_nos,
606 # Check current issues table
607 my $issues = Koha::Checkouts->search($criteria);
608 return 1 if $issues->count; # 0 || N
610 # Check old issues table
611 my $old_issues = Koha::Old::Checkouts->search($criteria);
612 return $old_issues->count; # 0 || N
617 my $debarment_expiration = $patron->is_debarred;
619 Returns the date a patron debarment will expire, or undef if the patron is not
627 return unless $self->debarred;
628 return $self->debarred
629 if $self->debarred =~ '^9999'
630 or dt_from_string( $self->debarred ) > dt_from_string;
636 my $is_expired = $patron->is_expired;
638 Returns 1 if the patron is expired or 0;
644 return 0 unless $self->dateexpiry;
645 return 0 if $self->dateexpiry =~ '^9999';
646 return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
650 =head3 is_going_to_expire
652 my $is_going_to_expire = $patron->is_going_to_expire;
654 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
658 sub is_going_to_expire {
661 my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
663 return 0 unless $delay;
664 return 0 unless $self->dateexpiry;
665 return 0 if $self->dateexpiry =~ '^9999';
666 return 1 if dt_from_string( $self->dateexpiry )->subtract( days => $delay ) < dt_from_string->truncate( to => 'day' );
670 =head3 update_password
672 my $updated = $patron->update_password( $userid, $password );
674 Update the userid and the password of a patron.
675 If the userid already exists, returns and let DBIx::Class warns
676 This will add an entry to action_logs if BorrowersLog is set.
680 sub update_password {
681 my ( $self, $userid, $password ) = @_;
682 eval { $self->userid($userid)->store; };
683 return if $@; # Make sure the userid is not already in used by another patron
685 return 0 if $password eq '****' or $password eq '';
687 if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
688 # Update the hashed PIN in borrower_sync.hashed_pin, before Koha hashes it
689 Koha::NorwegianPatronDB::NLUpdateHashedPIN( $self->borrowernumber, $password );
692 my $digest = Koha::AuthUtils::hash_password($password);
700 logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
706 my $new_expiry_date = $patron->renew_account
708 Extending the subscription to the expiry date.
715 if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
716 $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
719 C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
720 ? dt_from_string( $self->dateexpiry )
723 my $expiry_date = $self->category->get_expiry_date($date);
725 $self->dateexpiry($expiry_date);
726 $self->date_renewed( dt_from_string() );
729 $self->add_enrolment_fee_if_needed;
731 logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
732 return dt_from_string( $expiry_date )->truncate( to => 'day' );
737 my $has_overdues = $patron->has_overdues;
739 Returns the number of patron's overdues
745 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
746 return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
751 $patron->track_login;
752 $patron->track_login({ force => 1 });
754 Tracks a (successful) login attempt.
755 The preference TrackLastPatronActivity must be enabled. Or you
756 should pass the force parameter.
761 my ( $self, $params ) = @_;
764 !C4::Context->preference('TrackLastPatronActivity');
765 $self->lastseen( dt_from_string() )->store;
768 =head3 move_to_deleted
770 my $is_moved = $patron->move_to_deleted;
772 Move a patron to the deletedborrowers table.
773 This can be done before deleting a patron, to make sure the data are not completely deleted.
777 sub move_to_deleted {
779 my $patron_infos = $self->unblessed;
780 delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
781 return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
784 =head3 article_requests
786 my @requests = $borrower->article_requests();
787 my $requests = $borrower->article_requests();
789 Returns either a list of ArticleRequests objects,
790 or an ArtitleRequests object, depending on the
795 sub article_requests {
798 $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
800 return $self->{_article_requests};
803 =head3 article_requests_current
805 my @requests = $patron->article_requests_current
807 Returns the article requests associated with this patron that are incomplete
811 sub article_requests_current {
814 $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
816 borrowernumber => $self->id(),
818 { status => Koha::ArticleRequest::Status::Pending },
819 { status => Koha::ArticleRequest::Status::Processing }
824 return $self->{_article_requests_current};
827 =head3 article_requests_finished
829 my @requests = $biblio->article_requests_finished
831 Returns the article requests associated with this patron that are completed
835 sub article_requests_finished {
836 my ( $self, $borrower ) = @_;
838 $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
840 borrowernumber => $self->id(),
842 { status => Koha::ArticleRequest::Status::Completed },
843 { status => Koha::ArticleRequest::Status::Canceled }
848 return $self->{_article_requests_finished};
851 =head3 add_enrolment_fee_if_needed
853 my $enrolment_fee = $patron->add_enrolment_fee_if_needed;
855 Add enrolment fee for a patron if needed.
859 sub add_enrolment_fee_if_needed {
861 my $enrolment_fee = $self->category->enrolmentfee;
862 if ( $enrolment_fee && $enrolment_fee > 0 ) {
863 # insert fee in patron debts
864 C4::Accounts::manualinvoice( $self->borrowernumber, '', '', 'A', $enrolment_fee );
866 return $enrolment_fee || 0;
871 my $checkouts = $patron->checkouts
877 my $checkouts = $self->_result->issues;
878 return Koha::Checkouts->_new_from_dbic( $checkouts );
881 =head3 pending_checkouts
883 my $pending_checkouts = $patron->pending_checkouts
885 This method will return the same as $self->checkouts, but with a prefetch on
886 items, biblio and biblioitems.
888 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
890 It should not be used directly, prefer to access fields you need instead of
891 retrieving all these fields in one go.
896 sub pending_checkouts {
898 my $checkouts = $self->_result->issues->search(
902 { -desc => 'me.timestamp' },
903 { -desc => 'issuedate' },
904 { -desc => 'issue_id' }, # Sort by issue_id should be enough
906 prefetch => { item => { biblio => 'biblioitems' } },
909 return Koha::Checkouts->_new_from_dbic( $checkouts );
914 my $old_checkouts = $patron->old_checkouts
920 my $old_checkouts = $self->_result->old_issues;
921 return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
926 my $overdue_items = $patron->get_overdues
928 Return the overdue items
934 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
935 return $self->checkouts->search(
937 'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
940 prefetch => { item => { biblio => 'biblioitems' } },
945 =head3 get_routing_lists
947 my @routinglists = $patron->get_routing_lists
949 Returns the routing lists a patron is subscribed to.
953 sub get_routing_lists {
955 my $routing_list_rs = $self->_result->subscriptionroutinglists;
956 return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
961 my $age = $patron->get_age
963 Return the age of the patron
969 my $today_str = dt_from_string->strftime("%Y-%m-%d");
970 return unless $self->dateofbirth;
971 my $dob_str = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
973 my ( $dob_y, $dob_m, $dob_d ) = split /-/, $dob_str;
974 my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
976 my $age = $today_y - $dob_y;
977 if ( $dob_m . $dob_d > $today_m . $today_d ) {
986 my $account = $patron->account
992 return Koha::Account->new( { patron_id => $self->borrowernumber } );
997 my $holds = $patron->holds
999 Return all the holds placed by this patron
1005 my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1006 return Koha::Holds->_new_from_dbic($holds_rs);
1011 my $old_holds = $patron->old_holds
1013 Return all the historical holds for this patron
1019 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1020 return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1023 =head3 notice_email_address
1025 my $email = $patron->notice_email_address;
1027 Return the email address of patron used for notices.
1028 Returns the empty string if no email address.
1032 sub notice_email_address{
1035 my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1036 # if syspref is set to 'first valid' (value == OFF), look up email address
1037 if ( $which_address eq 'OFF' ) {
1038 return $self->first_valid_email_address;
1041 return $self->$which_address || '';
1044 =head3 first_valid_email_address
1046 my $first_valid_email_address = $patron->first_valid_email_address
1048 Return the first valid email address for a patron.
1049 For now, the order is defined as email, emailpro, B_email.
1050 Returns the empty string if the borrower has no email addresses.
1054 sub first_valid_email_address {
1057 return $self->email() || $self->emailpro() || $self->B_email() || q{};
1060 =head3 get_club_enrollments
1064 sub get_club_enrollments {
1065 my ( $self, $return_scalar ) = @_;
1067 my $e = Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1069 return $e if $return_scalar;
1071 return wantarray ? $e->as_list : $e;
1074 =head3 get_enrollable_clubs
1078 sub get_enrollable_clubs {
1079 my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
1082 $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1083 if $is_enrollable_from_opac;
1084 $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1086 $params->{borrower} = $self;
1088 my $e = Koha::Clubs->get_enrollable($params);
1090 return $e if $return_scalar;
1092 return wantarray ? $e->as_list : $e;
1095 =head3 account_locked
1097 my $is_locked = $patron->account_locked
1099 Return true if the patron has reach the maximum number of login attempts (see pref FailedLoginAttempts).
1100 Otherwise return false.
1101 If the pref is not set (empty string, null or 0), the feature is considered as disabled.
1105 sub account_locked {
1107 my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1108 return ( $FailedLoginAttempts
1109 and $self->login_attempts
1110 and $self->login_attempts >= $FailedLoginAttempts )? 1 : 0;
1113 =head3 can_see_patron_infos
1115 my $can_see = $patron->can_see_patron_infos( $patron );
1117 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1121 sub can_see_patron_infos {
1122 my ( $self, $patron ) = @_;
1123 return $self->can_see_patrons_from( $patron->library->branchcode );
1126 =head3 can_see_patrons_from
1128 my $can_see = $patron->can_see_patrons_from( $branchcode );
1130 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1134 sub can_see_patrons_from {
1135 my ( $self, $branchcode ) = @_;
1137 if ( $self->branchcode eq $branchcode ) {
1139 } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1141 } elsif ( my $library_groups = $self->library->library_groups ) {
1142 while ( my $library_group = $library_groups->next ) {
1143 if ( $library_group->parent->has_child( $branchcode ) ) {
1152 =head3 libraries_where_can_see_patrons
1154 my $libraries = $patron-libraries_where_can_see_patrons;
1156 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1157 The branchcodes are arbitrarily returned sorted.
1158 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1160 An empty array means no restriction, the patron can see patron's infos from any libraries.
1164 sub libraries_where_can_see_patrons {
1166 my $userenv = C4::Context->userenv;
1168 return () unless $userenv; # For tests, but userenv should be defined in tests...
1170 my @restricted_branchcodes;
1171 if (C4::Context::only_my_library) {
1172 push @restricted_branchcodes, $self->branchcode;
1176 $self->has_permission(
1177 { borrowers => 'view_borrower_infos_from_any_libraries' }
1181 my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1182 if ( $library_groups->count )
1184 while ( my $library_group = $library_groups->next ) {
1185 my $parent = $library_group->parent;
1186 if ( $parent->has_child( $self->branchcode ) ) {
1187 push @restricted_branchcodes, $parent->children->get_column('branchcode');
1192 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1196 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1197 @restricted_branchcodes = uniq(@restricted_branchcodes);
1198 @restricted_branchcodes = sort(@restricted_branchcodes);
1199 return @restricted_branchcodes;
1202 sub has_permission {
1203 my ( $self, $flagsrequired ) = @_;
1204 return unless $self->userid;
1205 # TODO code from haspermission needs to be moved here!
1206 return C4::Auth::haspermission( $self->userid, $flagsrequired );
1211 my $is_adult = $patron->is_adult
1213 Return true if the patron has a category with a type Adult (A) or Organization (I)
1219 return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1224 my $is_child = $patron->is_child
1226 Return true if the patron has a category with a type Child (C)
1231 return $self->category->category_type eq 'C' ? 1 : 0;
1234 =head3 has_valid_userid
1236 my $patron = Koha::Patrons->find(42);
1237 $patron->userid( $new_userid );
1238 my $has_a_valid_userid = $patron->has_valid_userid
1240 my $patron = Koha::Patron->new( $params );
1241 my $has_a_valid_userid = $patron->has_valid_userid
1243 Return true if the current userid of this patron is valid/unique, otherwise false.
1245 Note that this should be done in $self->store instead and raise an exception if needed.
1249 sub has_valid_userid {
1252 return 0 unless $self->userid;
1254 return 0 if ( $self->userid eq C4::Context->config('user') ); # DB user
1256 my $already_exists = Koha::Patrons->search(
1258 userid => $self->userid,
1261 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1266 return $already_exists ? 0 : 1;
1269 =head3 generate_userid
1271 my $patron = Koha::Patron->new( $params );
1272 $patron->generate_userid
1274 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1276 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).
1280 sub generate_userid {
1283 my $firstname = $self->firstname // q{};
1284 my $surname = $self->surname // q{};
1285 #The script will "do" the following code and increment the $offset until the generated userid is unique
1287 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1288 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1289 my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1290 $userid = unac_string('utf-8',$userid);
1291 $userid .= $offset unless $offset == 0;
1292 $self->userid( $userid );
1294 } while (! $self->has_valid_userid );
1300 =head2 Internal methods
1312 Kyle M Hall <kyle@bywatersolutions.com>
1313 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>