3 # Copyright ByWater Solutions 2014
4 # Copyright PTFS Europe 2016
6 # This file is part of Koha.
8 # Koha is free software; you can redistribute it and/or modify it under the
9 # terms of the GNU General Public License as published by the Free Software
10 # Foundation; either version 3 of the License, or (at your option) any later
13 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
14 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License along
18 # with Koha; if not, write to the Free Software Foundation, Inc.,
19 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 use List::MoreUtils qw( any uniq );
25 use JSON qw( to_json );
26 use Text::Unaccent qw( unac_string );
33 use Koha::Club::Enrollments;
36 use Koha::Exceptions::Password;
38 use Koha::Old::Checkouts;
39 use Koha::Patron::Attributes;
40 use Koha::Patron::Categories;
41 use Koha::Patron::HouseboundProfile;
42 use Koha::Patron::HouseboundRole;
43 use Koha::Patron::Images;
44 use Koha::Patron::Relationships;
47 use Koha::Plugins::Handler;
48 use Koha::Subscription::Routinglists;
50 use Koha::Virtualshelves;
52 use base qw(Koha::Object);
54 use constant ADMINISTRATIVE_LOCKOUT => -1;
56 our $RESULTSET_PATRON_ID_MAPPING = {
57 Accountline => 'borrowernumber',
58 Aqbasketuser => 'borrowernumber',
59 Aqbudget => 'budget_owner_id',
60 Aqbudgetborrower => 'borrowernumber',
61 ArticleRequest => 'borrowernumber',
62 BorrowerAttribute => 'borrowernumber',
63 BorrowerDebarment => 'borrowernumber',
64 BorrowerFile => 'borrowernumber',
65 BorrowerModification => 'borrowernumber',
66 ClubEnrollment => 'borrowernumber',
67 Issue => 'borrowernumber',
68 ItemsLastBorrower => 'borrowernumber',
69 Linktracker => 'borrowernumber',
70 Message => 'borrowernumber',
71 MessageQueue => 'borrowernumber',
72 OldIssue => 'borrowernumber',
73 OldReserve => 'borrowernumber',
74 Rating => 'borrowernumber',
75 Reserve => 'borrowernumber',
76 Review => 'borrowernumber',
77 SearchHistory => 'userid',
78 Statistic => 'borrowernumber',
79 Suggestion => 'suggestedby',
80 TagAll => 'borrowernumber',
81 Virtualshelfcontent => 'borrowernumber',
82 Virtualshelfshare => 'borrowernumber',
83 Virtualshelve => 'owner',
88 Koha::Patron - Koha Patron Object class
99 my ( $class, $params ) = @_;
101 return $class->SUPER::new($params);
104 =head3 fixup_cardnumber
106 Autogenerate next cardnumber from highest value found in database
110 sub fixup_cardnumber {
112 my $max = Koha::Patrons->search({
113 cardnumber => {-regexp => '^-?[0-9]+$'}
115 select => \'CAST(cardnumber AS SIGNED)',
116 as => ['cast_cardnumber']
117 })->_resultset->get_column('cast_cardnumber')->max;
118 $self->cardnumber(($max || 0) +1);
121 =head3 trim_whitespace
123 trim whitespace from data which has some non-whitespace in it.
124 Could be moved to Koha::Object if need to be reused
128 sub trim_whitespaces {
131 my $schema = Koha::Database->new->schema;
132 my @columns = $schema->source($self->_type)->columns;
134 for my $column( @columns ) {
135 my $value = $self->$column;
136 if ( defined $value ) {
137 $value =~ s/^\s*|\s*$//g;
138 $self->$column($value);
144 =head3 plain_text_password
146 $patron->plain_text_password( $password );
148 stores a copy of the unencrypted password in the object
149 for use in code before encrypting for db
153 sub plain_text_password {
154 my ( $self, $password ) = @_;
156 $self->{_plain_text_password} = $password;
159 return $self->{_plain_text_password}
160 if $self->{_plain_text_password};
167 Patron specific store method to cleanup record
168 and do other necessary things before saving
176 $self->_result->result_source->schema->txn_do(
179 C4::Context->preference("autoMemberNum")
180 and ( not defined $self->cardnumber
181 or $self->cardnumber eq '' )
184 # Warning: The caller is responsible for locking the members table in write
185 # mode, to avoid database corruption.
186 # We are in a transaction but the table is not locked
187 $self->fixup_cardnumber;
190 unless( $self->category->in_storage ) {
191 Koha::Exceptions::Object::FKConstraint->throw(
192 broken_fk => 'categorycode',
193 value => $self->categorycode,
197 $self->trim_whitespaces;
199 # Set surname to uppercase if uppercasesurname is true
200 $self->surname( uc($self->surname) )
201 if C4::Context->preference("uppercasesurnames");
203 unless ( $self->in_storage ) { #AddMember
205 # Generate a valid userid/login if needed
206 $self->generate_userid
207 if not $self->userid or not $self->has_valid_userid;
209 # Add expiration date if it isn't already there
210 unless ( $self->dateexpiry ) {
211 $self->dateexpiry( $self->category->get_expiry_date );
214 # Add enrollment date if it isn't already there
215 unless ( $self->dateenrolled ) {
216 $self->dateenrolled(dt_from_string);
219 # Set the privacy depending on the patron's category
220 my $default_privacy = $self->category->default_privacy || q{};
222 $default_privacy eq 'default' ? 1
223 : $default_privacy eq 'never' ? 2
224 : $default_privacy eq 'forever' ? 0
226 $self->privacy($default_privacy);
228 # Make a copy of the plain text password for later use
229 $self->plain_text_password( $self->password );
231 logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
232 if C4::Context->preference("BorrowersLog");
234 if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
235 # Call any check_password plugins
236 my @plugins = Koha::Plugins->new()->GetPlugins({
237 method => 'check_password',
239 foreach my $plugin ( @plugins ) {
240 # This plugin hook will also be used by a plugin for the Norwegian national
241 # patron database. This is why we need to pass both the password and the
242 # borrowernumber to the plugin.
243 my $ret = Koha::Plugins::Handler->run({
244 class => ref $plugin,
245 method => 'check_password',
247 password => $self->plain_text_password,
248 borrowernumber => $self->borrowernumber,
251 if ( $ret->{'error'} == 1 ) {
252 Koha::Exceptions::Password::Plugin->throw();
257 # Create a disabled account if no password provided
258 $self->password( $self->password
259 ? Koha::AuthUtils::hash_password( $self->password )
262 $self->borrowernumber(undef);
264 $self = $self->SUPER::store;
266 $self->add_enrolment_fee_if_needed(0);
271 my $self_from_storage = $self->get_from_storage;
272 # FIXME We should not deal with that here, callers have to do this job
273 # Moved from ModMember to prevent regressions
274 unless ( $self->userid ) {
275 my $stored_userid = $self_from_storage->userid;
276 $self->userid($stored_userid);
279 # Password must be updated using $self->set_password
280 $self->password($self_from_storage->password);
282 if ( $self->category->categorycode ne
283 $self_from_storage->category->categorycode )
285 # Add enrolement fee on category change if required
286 $self->add_enrolment_fee_if_needed(1)
287 if C4::Context->preference('FeeOnChangePatronCategory');
289 # Clean up guarantors on category change if required
290 $self->guarantor_relationships->delete
291 if ( $self->category->category_type ne 'C'
292 && $self->category->category_type ne 'P' );
297 if ( C4::Context->preference("BorrowersLog") ) {
299 my $from_storage = $self_from_storage->unblessed;
300 my $from_object = $self->unblessed;
301 my @skip_fields = (qw/lastseen updated_on/);
302 for my $key ( keys %{$from_storage} ) {
303 next if any { /$key/ } @skip_fields;
306 !defined( $from_storage->{$key} )
307 && defined( $from_object->{$key} )
309 || ( defined( $from_storage->{$key} )
310 && !defined( $from_object->{$key} ) )
312 defined( $from_storage->{$key} )
313 && defined( $from_object->{$key} )
314 && ( $from_storage->{$key} ne
315 $from_object->{$key} )
320 before => $from_storage->{$key},
321 after => $from_object->{$key}
326 if ( defined($info) ) {
330 $self->borrowernumber,
333 { utf8 => 1, pretty => 1, canonical => 1 }
340 $self = $self->SUPER::store;
351 Delete patron's holds, lists and finally the patron.
353 Lists owned by the borrower are deleted, but entries from the borrower to
354 other lists are kept.
362 $self->_result->result_source->schema->txn_do(
364 # Cancel Patron's holds
365 my $holds = $self->holds;
366 while( my $hold = $holds->next ){
370 # Delete all lists and all shares of this borrower
371 # Consistent with the approach Koha uses on deleting individual lists
372 # Note that entries in virtualshelfcontents added by this borrower to
373 # lists of others will be handled by a table constraint: the borrower
374 # is set to NULL in those entries.
376 # We could handle the above deletes via a constraint too.
377 # But a new BZ report 11889 has been opened to discuss another approach.
378 # Instead of deleting we could also disown lists (based on a pref).
379 # In that way we could save shared and public lists.
380 # The current table constraints support that idea now.
381 # This pref should then govern the results of other routines/methods such as
382 # Koha::Virtualshelf->new->delete too.
383 # FIXME Could be $patron->get_lists
384 $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
386 $deleted = $self->SUPER::delete;
388 logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
397 my $patron_category = $patron->category
399 Return the patron category for this patron
405 return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
415 return scalar Koha::Patron::Images->find( $self->borrowernumber );
420 Returns a Koha::Library object representing the patron's home library.
426 return Koha::Library->_new_from_dbic($self->_result->branchcode);
429 =head3 guarantor_relationships
431 Returns Koha::Patron::Relationships object for this patron's guarantors
433 Returns the set of relationships for the patrons that are guarantors for this patron.
435 This is returned instead of a Koha::Patron object because the guarantor
436 may not exist as a patron in Koha. If this is true, the guarantors name
437 exists in the Koha::Patron::Relationship object and will have no guarantor_id.
441 sub guarantor_relationships {
444 return Koha::Patron::Relationships->search( { guarantee_id => $self->id } );
447 =head3 guarantee_relationships
449 Returns Koha::Patron::Relationships object for this patron's guarantors
451 Returns the set of relationships for the patrons that are guarantees for this patron.
453 The method returns Koha::Patron::Relationship objects for the sake
454 of consistency with the guantors method.
455 A guarantee by definition must exist as a patron in Koha.
459 sub guarantee_relationships {
462 return Koha::Patron::Relationships->search(
463 { guarantor_id => $self->id },
465 prefetch => 'guarantee',
466 order_by => { -asc => [ 'guarantee.surname', 'guarantee.firstname' ] },
471 =head3 housebound_profile
473 Returns the HouseboundProfile associated with this patron.
477 sub housebound_profile {
479 my $profile = $self->_result->housebound_profile;
480 return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
485 =head3 housebound_role
487 Returns the HouseboundRole associated with this patron.
491 sub housebound_role {
494 my $role = $self->_result->housebound_role;
495 return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
501 Returns the siblings of this patron.
508 my @guarantors = $self->guarantor_relationships()->guarantors();
510 return unless @guarantors;
513 map { $_->guarantee_relationships()->guarantees() } @guarantors;
515 return unless @siblings;
519 grep { !$seen{ $_->id }++ && ( $_->id != $self->id ) } @siblings;
521 return wantarray ? @siblings : Koha::Patrons->search( { borrowernumber => { -in => [ map { $_->id } @siblings ] } } );
526 my $patron = Koha::Patrons->find($id);
527 $patron->merge_with( \@patron_ids );
529 This subroutine merges a list of patrons into the patron record. This is accomplished by finding
530 all related patron ids for the patrons to be merged in other tables and changing the ids to be that
531 of the keeper patron.
536 my ( $self, $patron_ids ) = @_;
538 my @patron_ids = @{ $patron_ids };
540 # Ensure the keeper isn't in the list of patrons to merge
541 @patron_ids = grep { $_ ne $self->id } @patron_ids;
543 my $schema = Koha::Database->new()->schema();
547 $self->_result->result_source->schema->txn_do( sub {
548 foreach my $patron_id (@patron_ids) {
549 my $patron = Koha::Patrons->find( $patron_id );
553 # Unbless for safety, the patron will end up being deleted
554 $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
556 while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
557 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
558 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
559 $rs->update({ $field => $self->id });
562 $patron->move_to_deleted();
572 =head3 wants_check_for_previous_checkout
574 $wants_check = $patron->wants_check_for_previous_checkout;
576 Return 1 if Koha needs to perform PrevIssue checking, else 0.
580 sub wants_check_for_previous_checkout {
582 my $syspref = C4::Context->preference("checkPrevCheckout");
585 ## Hard syspref trumps all
586 return 1 if ($syspref eq 'hardyes');
587 return 0 if ($syspref eq 'hardno');
588 ## Now, patron pref trumps all
589 return 1 if ($self->checkprevcheckout eq 'yes');
590 return 0 if ($self->checkprevcheckout eq 'no');
592 # More complex: patron inherits -> determine category preference
593 my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
594 return 1 if ($checkPrevCheckoutByCat eq 'yes');
595 return 0 if ($checkPrevCheckoutByCat eq 'no');
597 # Finally: category preference is inherit, default to 0
598 if ($syspref eq 'softyes') {
605 =head3 do_check_for_previous_checkout
607 $do_check = $patron->do_check_for_previous_checkout($item);
609 Return 1 if the bib associated with $ITEM has previously been checked out to
610 $PATRON, 0 otherwise.
614 sub do_check_for_previous_checkout {
615 my ( $self, $item ) = @_;
618 my $biblio = Koha::Biblios->find( $item->{biblionumber} );
619 if ( $biblio->is_serial ) {
620 push @item_nos, $item->{itemnumber};
622 # Get all itemnumbers for given bibliographic record.
623 @item_nos = $biblio->items->get_column( 'itemnumber' );
626 # Create (old)issues search criteria
628 borrowernumber => $self->borrowernumber,
629 itemnumber => \@item_nos,
632 # Check current issues table
633 my $issues = Koha::Checkouts->search($criteria);
634 return 1 if $issues->count; # 0 || N
636 # Check old issues table
637 my $old_issues = Koha::Old::Checkouts->search($criteria);
638 return $old_issues->count; # 0 || N
643 my $debarment_expiration = $patron->is_debarred;
645 Returns the date a patron debarment will expire, or undef if the patron is not
653 return unless $self->debarred;
654 return $self->debarred
655 if $self->debarred =~ '^9999'
656 or dt_from_string( $self->debarred ) > dt_from_string;
662 my $is_expired = $patron->is_expired;
664 Returns 1 if the patron is expired or 0;
670 return 0 unless $self->dateexpiry;
671 return 0 if $self->dateexpiry =~ '^9999';
672 return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
676 =head3 is_going_to_expire
678 my $is_going_to_expire = $patron->is_going_to_expire;
680 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
684 sub is_going_to_expire {
687 my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
689 return 0 unless $delay;
690 return 0 unless $self->dateexpiry;
691 return 0 if $self->dateexpiry =~ '^9999';
692 return 1 if dt_from_string( $self->dateexpiry, undef, 'floating' )->subtract( days => $delay ) < dt_from_string(undef, undef, 'floating')->truncate( to => 'day' );
698 $patron->set_password({ password => $plain_text_password [, skip_validation => 1 ] });
700 Set the patron's password.
704 The passed string is validated against the current password enforcement policy.
705 Validation can be skipped by passing the I<skip_validation> parameter.
707 Exceptions are thrown if the password is not good enough.
711 =item Koha::Exceptions::Password::TooShort
713 =item Koha::Exceptions::Password::WhitespaceCharacters
715 =item Koha::Exceptions::Password::TooWeak
717 =item Koha::Exceptions::Password::Plugin (if a "check password" plugin is enabled)
724 my ( $self, $args ) = @_;
726 my $password = $args->{password};
728 unless ( $args->{skip_validation} ) {
729 my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password );
732 if ( $error eq 'too_short' ) {
733 my $min_length = C4::Context->preference('minPasswordLength');
734 $min_length = 3 if not $min_length or $min_length < 3;
736 my $password_length = length($password);
737 Koha::Exceptions::Password::TooShort->throw(
738 length => $password_length, min_length => $min_length );
740 elsif ( $error eq 'has_whitespaces' ) {
741 Koha::Exceptions::Password::WhitespaceCharacters->throw();
743 elsif ( $error eq 'too_weak' ) {
744 Koha::Exceptions::Password::TooWeak->throw();
749 if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
750 # Call any check_password plugins
751 my @plugins = Koha::Plugins->new()->GetPlugins({
752 method => 'check_password',
754 foreach my $plugin ( @plugins ) {
755 # This plugin hook will also be used by a plugin for the Norwegian national
756 # patron database. This is why we need to pass both the password and the
757 # borrowernumber to the plugin.
758 my $ret = Koha::Plugins::Handler->run({
759 class => ref $plugin,
760 method => 'check_password',
762 password => $password,
763 borrowernumber => $self->borrowernumber,
766 # This plugin hook will also be used by a plugin for the Norwegian national
767 # patron database. This is why we need to call the actual plugins and then
768 # check skip_validation afterwards.
769 if ( $ret->{'error'} == 1 && !$args->{skip_validation} ) {
770 Koha::Exceptions::Password::Plugin->throw();
775 my $digest = Koha::AuthUtils::hash_password($password);
777 { password => $digest,
782 logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" )
783 if C4::Context->preference("BorrowersLog");
791 my $new_expiry_date = $patron->renew_account
793 Extending the subscription to the expiry date.
800 if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
801 $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
804 C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
805 ? dt_from_string( $self->dateexpiry )
808 my $expiry_date = $self->category->get_expiry_date($date);
810 $self->dateexpiry($expiry_date);
811 $self->date_renewed( dt_from_string() );
814 $self->add_enrolment_fee_if_needed(1);
816 logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
817 return dt_from_string( $expiry_date )->truncate( to => 'day' );
822 my $has_overdues = $patron->has_overdues;
824 Returns the number of patron's overdues
830 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
831 return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
836 $patron->track_login;
837 $patron->track_login({ force => 1 });
839 Tracks a (successful) login attempt.
840 The preference TrackLastPatronActivity must be enabled. Or you
841 should pass the force parameter.
846 my ( $self, $params ) = @_;
849 !C4::Context->preference('TrackLastPatronActivity');
850 $self->lastseen( dt_from_string() )->store;
853 =head3 move_to_deleted
855 my $is_moved = $patron->move_to_deleted;
857 Move a patron to the deletedborrowers table.
858 This can be done before deleting a patron, to make sure the data are not completely deleted.
862 sub move_to_deleted {
864 my $patron_infos = $self->unblessed;
865 delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
866 return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
869 =head3 article_requests
871 my @requests = $borrower->article_requests();
872 my $requests = $borrower->article_requests();
874 Returns either a list of ArticleRequests objects,
875 or an ArtitleRequests object, depending on the
880 sub article_requests {
883 $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
885 return $self->{_article_requests};
888 =head3 article_requests_current
890 my @requests = $patron->article_requests_current
892 Returns the article requests associated with this patron that are incomplete
896 sub article_requests_current {
899 $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
901 borrowernumber => $self->id(),
903 { status => Koha::ArticleRequest::Status::Pending },
904 { status => Koha::ArticleRequest::Status::Processing }
909 return $self->{_article_requests_current};
912 =head3 article_requests_finished
914 my @requests = $biblio->article_requests_finished
916 Returns the article requests associated with this patron that are completed
920 sub article_requests_finished {
921 my ( $self, $borrower ) = @_;
923 $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
925 borrowernumber => $self->id(),
927 { status => Koha::ArticleRequest::Status::Completed },
928 { status => Koha::ArticleRequest::Status::Canceled }
933 return $self->{_article_requests_finished};
936 =head3 add_enrolment_fee_if_needed
938 my $enrolment_fee = $patron->add_enrolment_fee_if_needed($renewal);
940 Add enrolment fee for a patron if needed.
942 $renewal - boolean denoting whether this is an account renewal or not
946 sub add_enrolment_fee_if_needed {
947 my ($self, $renewal) = @_;
948 my $enrolment_fee = $self->category->enrolmentfee;
949 if ( $enrolment_fee && $enrolment_fee > 0 ) {
950 my $type = $renewal ? 'ACCOUNT_RENEW' : 'ACCOUNT';
951 $self->account->add_debit(
953 amount => $enrolment_fee,
954 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
955 interface => C4::Context->interface,
956 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
961 return $enrolment_fee || 0;
966 my $checkouts = $patron->checkouts
972 my $checkouts = $self->_result->issues;
973 return Koha::Checkouts->_new_from_dbic( $checkouts );
976 =head3 pending_checkouts
978 my $pending_checkouts = $patron->pending_checkouts
980 This method will return the same as $self->checkouts, but with a prefetch on
981 items, biblio and biblioitems.
983 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
985 It should not be used directly, prefer to access fields you need instead of
986 retrieving all these fields in one go.
990 sub pending_checkouts {
992 my $checkouts = $self->_result->issues->search(
996 { -desc => 'me.timestamp' },
997 { -desc => 'issuedate' },
998 { -desc => 'issue_id' }, # Sort by issue_id should be enough
1000 prefetch => { item => { biblio => 'biblioitems' } },
1003 return Koha::Checkouts->_new_from_dbic( $checkouts );
1006 =head3 old_checkouts
1008 my $old_checkouts = $patron->old_checkouts
1014 my $old_checkouts = $self->_result->old_issues;
1015 return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
1020 my $overdue_items = $patron->get_overdues
1022 Return the overdue items
1028 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
1029 return $self->checkouts->search(
1031 'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
1034 prefetch => { item => { biblio => 'biblioitems' } },
1039 =head3 get_routing_lists
1041 my @routinglists = $patron->get_routing_lists
1043 Returns the routing lists a patron is subscribed to.
1047 sub get_routing_lists {
1049 my $routing_list_rs = $self->_result->subscriptionroutinglists;
1050 return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
1055 my $age = $patron->get_age
1057 Return the age of the patron
1063 my $today_str = dt_from_string->strftime("%Y-%m-%d");
1064 return unless $self->dateofbirth;
1065 my $dob_str = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
1067 my ( $dob_y, $dob_m, $dob_d ) = split /-/, $dob_str;
1068 my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
1070 my $age = $today_y - $dob_y;
1071 if ( $dob_m . $dob_d > $today_m . $today_d ) {
1080 my $is_valid = $patron->is_valid_age
1082 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
1088 my $age = $self->get_age;
1090 my $patroncategory = $self->category;
1091 my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
1093 return (defined($age) && (($high && ($age > $high)) or ($age < $low))) ? 0 : 1;
1098 my $account = $patron->account
1104 return Koha::Account->new( { patron_id => $self->borrowernumber } );
1109 my $holds = $patron->holds
1111 Return all the holds placed by this patron
1117 my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1118 return Koha::Holds->_new_from_dbic($holds_rs);
1123 my $old_holds = $patron->old_holds
1125 Return all the historical holds for this patron
1131 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1132 return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1135 =head3 notice_email_address
1137 my $email = $patron->notice_email_address;
1139 Return the email address of patron used for notices.
1140 Returns the empty string if no email address.
1144 sub notice_email_address{
1147 my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1148 # if syspref is set to 'first valid' (value == OFF), look up email address
1149 if ( $which_address eq 'OFF' ) {
1150 return $self->first_valid_email_address;
1153 return $self->$which_address || '';
1156 =head3 first_valid_email_address
1158 my $first_valid_email_address = $patron->first_valid_email_address
1160 Return the first valid email address for a patron.
1161 For now, the order is defined as email, emailpro, B_email.
1162 Returns the empty string if the borrower has no email addresses.
1166 sub first_valid_email_address {
1169 return $self->email() || $self->emailpro() || $self->B_email() || q{};
1172 =head3 get_club_enrollments
1176 sub get_club_enrollments {
1177 my ( $self, $return_scalar ) = @_;
1179 my $e = Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1181 return $e if $return_scalar;
1183 return wantarray ? $e->as_list : $e;
1186 =head3 get_enrollable_clubs
1190 sub get_enrollable_clubs {
1191 my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
1194 $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1195 if $is_enrollable_from_opac;
1196 $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1198 $params->{borrower} = $self;
1200 my $e = Koha::Clubs->get_enrollable($params);
1202 return $e if $return_scalar;
1204 return wantarray ? $e->as_list : $e;
1207 =head3 account_locked
1209 my $is_locked = $patron->account_locked
1211 Return true if the patron has reached the maximum number of login attempts
1212 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1213 as an administrative lockout (independent of FailedLoginAttempts; see also
1214 Koha::Patron->lock).
1215 Otherwise return false.
1216 If the pref is not set (empty string, null or 0), the feature is considered as
1221 sub account_locked {
1223 my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1224 return 1 if $FailedLoginAttempts
1225 and $self->login_attempts
1226 and $self->login_attempts >= $FailedLoginAttempts;
1227 return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1231 =head3 can_see_patron_infos
1233 my $can_see = $patron->can_see_patron_infos( $patron );
1235 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1239 sub can_see_patron_infos {
1240 my ( $self, $patron ) = @_;
1241 return unless $patron;
1242 return $self->can_see_patrons_from( $patron->library->branchcode );
1245 =head3 can_see_patrons_from
1247 my $can_see = $patron->can_see_patrons_from( $branchcode );
1249 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1253 sub can_see_patrons_from {
1254 my ( $self, $branchcode ) = @_;
1256 if ( $self->branchcode eq $branchcode ) {
1258 } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1260 } elsif ( my $library_groups = $self->library->library_groups ) {
1261 while ( my $library_group = $library_groups->next ) {
1262 if ( $library_group->parent->has_child( $branchcode ) ) {
1271 =head3 libraries_where_can_see_patrons
1273 my $libraries = $patron-libraries_where_can_see_patrons;
1275 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1276 The branchcodes are arbitrarily returned sorted.
1277 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1279 An empty array means no restriction, the patron can see patron's infos from any libraries.
1283 sub libraries_where_can_see_patrons {
1285 my $userenv = C4::Context->userenv;
1287 return () unless $userenv; # For tests, but userenv should be defined in tests...
1289 my @restricted_branchcodes;
1290 if (C4::Context::only_my_library) {
1291 push @restricted_branchcodes, $self->branchcode;
1295 $self->has_permission(
1296 { borrowers => 'view_borrower_infos_from_any_libraries' }
1300 my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1301 if ( $library_groups->count )
1303 while ( my $library_group = $library_groups->next ) {
1304 my $parent = $library_group->parent;
1305 if ( $parent->has_child( $self->branchcode ) ) {
1306 push @restricted_branchcodes, $parent->children->get_column('branchcode');
1311 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1315 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1316 @restricted_branchcodes = uniq(@restricted_branchcodes);
1317 @restricted_branchcodes = sort(@restricted_branchcodes);
1318 return @restricted_branchcodes;
1321 sub has_permission {
1322 my ( $self, $flagsrequired ) = @_;
1323 return unless $self->userid;
1324 # TODO code from haspermission needs to be moved here!
1325 return C4::Auth::haspermission( $self->userid, $flagsrequired );
1330 my $is_adult = $patron->is_adult
1332 Return true if the patron has a category with a type Adult (A) or Organization (I)
1338 return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1343 my $is_child = $patron->is_child
1345 Return true if the patron has a category with a type Child (C)
1351 return $self->category->category_type eq 'C' ? 1 : 0;
1354 =head3 has_valid_userid
1356 my $patron = Koha::Patrons->find(42);
1357 $patron->userid( $new_userid );
1358 my $has_a_valid_userid = $patron->has_valid_userid
1360 my $patron = Koha::Patron->new( $params );
1361 my $has_a_valid_userid = $patron->has_valid_userid
1363 Return true if the current userid of this patron is valid/unique, otherwise false.
1365 Note that this should be done in $self->store instead and raise an exception if needed.
1369 sub has_valid_userid {
1372 return 0 unless $self->userid;
1374 return 0 if ( $self->userid eq C4::Context->config('user') ); # DB user
1376 my $already_exists = Koha::Patrons->search(
1378 userid => $self->userid,
1381 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1386 return $already_exists ? 0 : 1;
1389 =head3 generate_userid
1391 my $patron = Koha::Patron->new( $params );
1392 $patron->generate_userid
1394 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1396 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).
1400 sub generate_userid {
1403 my $firstname = $self->firstname // q{};
1404 my $surname = $self->surname // q{};
1405 #The script will "do" the following code and increment the $offset until the generated userid is unique
1407 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1408 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1409 my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1410 $userid = unac_string('utf-8',$userid);
1411 $userid .= $offset unless $offset == 0;
1412 $self->userid( $userid );
1414 } while (! $self->has_valid_userid );
1422 my $attributes = $patron->attributes
1424 Return object of Koha::Patron::Attributes type with all attributes set for this patron
1430 return Koha::Patron::Attributes->search({
1431 borrowernumber => $self->borrowernumber,
1432 branchcode => $self->branchcode,
1438 Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
1440 Lock and optionally expire a patron account.
1441 Remove holds and article requests if remove flag set.
1442 In order to distinguish from locking by entering a wrong password, let's
1443 call this an administrative lockout.
1448 my ( $self, $params ) = @_;
1449 $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
1450 if( $params->{expire} ) {
1451 $self->dateexpiry( dt_from_string->subtract(days => 1) );
1454 if( $params->{remove} ) {
1455 $self->holds->delete;
1456 $self->article_requests->delete;
1463 Koha::Patrons->find($id)->anonymize;
1465 Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
1466 are randomized, other personal data is cleared too.
1467 Patrons with issues are skipped.
1473 if( $self->_result->issues->count ) {
1474 warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
1477 # Mandatory fields come from the corresponding pref, but email fields
1478 # are removed since scrambled email addresses only generate errors
1479 my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
1480 split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
1481 $mandatory->{userid} = 1; # needed since sub store does not clear field
1482 my @columns = $self->_result->result_source->columns;
1483 @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized/ } @columns;
1484 push @columns, 'dateofbirth'; # add this date back in
1485 foreach my $col (@columns) {
1486 $self->_anonymize_column($col, $mandatory->{lc $col} );
1488 $self->anonymized(1)->store;
1491 sub _anonymize_column {
1492 my ( $self, $col, $mandatory ) = @_;
1493 my $col_info = $self->_result->result_source->column_info($col);
1494 my $type = $col_info->{data_type};
1495 my $nullable = $col_info->{is_nullable};
1497 if( $type =~ /char|text/ ) {
1499 ? Koha::Token->new->generate({ pattern => '\w{10}' })
1503 } elsif( $type =~ /integer|int$|float|dec|double/ ) {
1504 $val = $nullable ? undef : 0;
1505 } elsif( $type =~ /date|time/ ) {
1506 $val = $nullable ? undef : dt_from_string;
1511 =head3 add_guarantor
1513 my @relationships = $patron->add_guarantor(
1515 borrowernumber => $borrowernumber,
1516 relationships => $relationship,
1520 Adds a new guarantor to a patron.
1525 my ( $self, $params ) = @_;
1527 my $guarantor_id = $params->{guarantor_id};
1528 my $relationship = $params->{relationship};
1530 return Koha::Patron::Relationship->new(
1532 guarantee_id => $self->id,
1533 guarantor_id => $guarantor_id,
1534 relationship => $relationship
1541 my $json = $patron->to_api;
1543 Overloaded method that returns a JSON representation of the Koha::Patron object,
1544 suitable for API output.
1551 my $json_patron = $self->SUPER::to_api;
1553 $json_patron->{restricted} = ( $self->is_debarred )
1555 : Mojo::JSON->false;
1557 return $json_patron;
1560 =head3 to_api_mapping
1562 This method returns the mapping for representing a Koha::Patron object
1567 sub to_api_mapping {
1569 borrowernotes => 'staff_notes',
1570 borrowernumber => 'patron_id',
1571 branchcode => 'library_id',
1572 categorycode => 'category_id',
1573 checkprevcheckout => 'check_previous_checkout',
1574 contactfirstname => undef, # Unused
1575 contactname => undef, # Unused
1576 contactnote => 'altaddress_notes',
1577 contacttitle => undef, # Unused
1578 dateenrolled => 'date_enrolled',
1579 dateexpiry => 'expiry_date',
1580 dateofbirth => 'date_of_birth',
1581 debarred => undef, # replaced by 'restricted'
1582 debarredcomment => undef, # calculated, API consumers will use /restrictions instead
1583 emailpro => 'secondary_email',
1584 flags => undef, # permissions manipulation handled in /permissions
1585 gonenoaddress => 'incorrect_address',
1586 guarantorid => 'guarantor_id',
1587 lastseen => 'last_seen',
1588 lost => 'patron_card_lost',
1589 opacnote => 'opac_notes',
1590 othernames => 'other_name',
1591 password => undef, # password manipulation handled in /password
1592 phonepro => 'secondary_phone',
1593 relationship => 'relationship_type',
1595 smsalertnumber => 'sms_number',
1596 sort1 => 'statistics_1',
1597 sort2 => 'statistics_2',
1598 streetnumber => 'street_number',
1599 streettype => 'street_type',
1600 zipcode => 'postal_code',
1601 B_address => 'altaddress_address',
1602 B_address2 => 'altaddress_address2',
1603 B_city => 'altaddress_city',
1604 B_country => 'altaddress_country',
1605 B_email => 'altaddress_email',
1606 B_phone => 'altaddress_phone',
1607 B_state => 'altaddress_state',
1608 B_streetnumber => 'altaddress_street_number',
1609 B_streettype => 'altaddress_street_type',
1610 B_zipcode => 'altaddress_postal_code',
1611 altcontactaddress1 => 'altcontact_address',
1612 altcontactaddress2 => 'altcontact_address2',
1613 altcontactaddress3 => 'altcontact_city',
1614 altcontactcountry => 'altcontact_country',
1615 altcontactfirstname => 'altcontact_firstname',
1616 altcontactphone => 'altcontact_phone',
1617 altcontactsurname => 'altcontact_surname',
1618 altcontactstate => 'altcontact_state',
1619 altcontactzipcode => 'altcontact_postal_code'
1623 =head2 Internal methods
1635 Kyle M Hall <kyle@bywatersolutions.com>
1636 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1637 Martin Renvoize <martin.renvoize@ptfs-europe.com>