Bug 21801: Make paycollect.pl pass library_id when calling ->pay
[koha.git] / Koha / Patron.pm
blobb1d9b14620c4637a00b609a2cdb3dcdbdc80e00c
1 package Koha::Patron;
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
11 # version.
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.
21 use Modern::Perl;
23 use Carp;
24 use List::MoreUtils qw( any uniq );
25 use JSON qw( to_json );
26 use Text::Unaccent qw( unac_string );
28 use C4::Context;
29 use C4::Log;
30 use Koha::AuthUtils;
31 use Koha::Checkouts;
32 use Koha::Database;
33 use Koha::DateUtils;
34 use Koha::Exceptions::Password;
35 use Koha::Holds;
36 use Koha::Old::Checkouts;
37 use Koha::Patron::Attributes;
38 use Koha::Patron::Categories;
39 use Koha::Patron::HouseboundProfile;
40 use Koha::Patron::HouseboundRole;
41 use Koha::Patron::Images;
42 use Koha::Patrons;
43 use Koha::Virtualshelves;
44 use Koha::Club::Enrollments;
45 use Koha::Account;
46 use Koha::Subscription::Routinglists;
47 use Koha::Token;
49 use base qw(Koha::Object);
51 use constant ADMINISTRATIVE_LOCKOUT => -1;
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',
83 =head1 NAME
85 Koha::Patron - Koha Patron Object class
87 =head1 API
89 =head2 Class Methods
91 =head3 new
93 =cut
95 sub new {
96 my ( $class, $params ) = @_;
98 return $class->SUPER::new($params);
101 =head3 fixup_cardnumber
103 Autogenerate next cardnumber from highest value found in database
105 =cut
107 sub fixup_cardnumber {
108 my ( $self ) = @_;
109 my $max = Koha::Patrons->search({
110 cardnumber => {-regexp => '^-?[0-9]+$'}
111 }, {
112 select => \'CAST(cardnumber AS SIGNED)',
113 as => ['cast_cardnumber']
114 })->_resultset->get_column('cast_cardnumber')->max;
115 $self->cardnumber(($max || 0) +1);
118 =head3 trim_whitespace
120 trim whitespace from data which has some non-whitespace in it.
121 Could be moved to Koha::Object if need to be reused
123 =cut
125 sub trim_whitespaces {
126 my( $self ) = @_;
128 my $schema = Koha::Database->new->schema;
129 my @columns = $schema->source($self->_type)->columns;
131 for my $column( @columns ) {
132 my $value = $self->$column;
133 if ( defined $value ) {
134 $value =~ s/^\s*|\s*$//g;
135 $self->$column($value);
138 return $self;
141 =head3 plain_text_password
143 $patron->plain_text_password( $password );
145 stores a copy of the unencrypted password in the object
146 for use in code before encrypting for db
148 =cut
150 sub plain_text_password {
151 my ( $self, $password ) = @_;
152 if ( $password ) {
153 $self->{_plain_text_password} = $password;
154 return $self;
156 return $self->{_plain_text_password}
157 if $self->{_plain_text_password};
159 return;
162 =head3 store
164 Patron specific store method to cleanup record
165 and do other necessary things before saving
166 to db
168 =cut
170 sub store {
171 my ($self) = @_;
173 $self->_result->result_source->schema->txn_do(
174 sub {
175 if (
176 C4::Context->preference("autoMemberNum")
177 and ( not defined $self->cardnumber
178 or $self->cardnumber eq '' )
181 # Warning: The caller is responsible for locking the members table in write
182 # mode, to avoid database corruption.
183 # We are in a transaction but the table is not locked
184 $self->fixup_cardnumber;
187 unless( $self->category->in_storage ) {
188 Koha::Exceptions::Object::FKConstraint->throw(
189 broken_fk => 'categorycode',
190 value => $self->categorycode,
194 $self->trim_whitespaces;
196 unless ( $self->in_storage ) { #AddMember
198 # Generate a valid userid/login if needed
199 $self->generate_userid
200 if not $self->userid or not $self->has_valid_userid;
202 # Add expiration date if it isn't already there
203 unless ( $self->dateexpiry ) {
204 $self->dateexpiry( $self->category->get_expiry_date );
207 # Add enrollment date if it isn't already there
208 unless ( $self->dateenrolled ) {
209 $self->dateenrolled(dt_from_string);
212 # Set the privacy depending on the patron's category
213 my $default_privacy = $self->category->default_privacy || q{};
214 $default_privacy =
215 $default_privacy eq 'default' ? 1
216 : $default_privacy eq 'never' ? 2
217 : $default_privacy eq 'forever' ? 0
218 : undef;
219 $self->privacy($default_privacy);
222 # Make a copy of the plain text password for later use
223 $self->plain_text_password( $self->password );
225 # Create a disabled account if no password provided
226 $self->password( $self->password
227 ? Koha::AuthUtils::hash_password( $self->password )
228 : '!' );
230 $self->borrowernumber(undef);
232 $self = $self->SUPER::store;
234 $self->add_enrolment_fee_if_needed;
236 logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
237 if C4::Context->preference("BorrowersLog");
239 else { #ModMember
241 my $self_from_storage = $self->get_from_storage;
242 # FIXME We should not deal with that here, callers have to do this job
243 # Moved from ModMember to prevent regressions
244 unless ( $self->userid ) {
245 my $stored_userid = $self_from_storage->userid;
246 $self->userid($stored_userid);
249 # Password must be updated using $self->set_password
250 $self->password($self_from_storage->password);
252 if ( C4::Context->preference('FeeOnChangePatronCategory')
253 and $self->category->categorycode ne
254 $self_from_storage->category->categorycode )
256 $self->add_enrolment_fee_if_needed;
259 # Actionlogs
260 if ( C4::Context->preference("BorrowersLog") ) {
261 my $info;
262 my $from_storage = $self_from_storage->unblessed;
263 my $from_object = $self->unblessed;
264 my @skip_fields = (qw/lastseen/);
265 for my $key ( keys %{$from_storage} ) {
266 next if any { /$key/ } @skip_fields;
267 if (
269 !defined( $from_storage->{$key} )
270 && defined( $from_object->{$key} )
272 || ( defined( $from_storage->{$key} )
273 && !defined( $from_object->{$key} ) )
274 || (
275 defined( $from_storage->{$key} )
276 && defined( $from_object->{$key} )
277 && ( $from_storage->{$key} ne
278 $from_object->{$key} )
282 $info->{$key} = {
283 before => $from_storage->{$key},
284 after => $from_object->{$key}
289 if ( defined($info) ) {
290 logaction(
291 "MEMBERS",
292 "MODIFY",
293 $self->borrowernumber,
294 to_json(
295 $info,
296 { utf8 => 1, pretty => 1, canonical => 1 }
302 # Final store
303 $self = $self->SUPER::store;
307 return $self;
310 =head3 delete
312 $patron->delete
314 Delete patron's holds, lists and finally the patron.
316 Lists owned by the borrower are deleted, but entries from the borrower to
317 other lists are kept.
319 =cut
321 sub delete {
322 my ($self) = @_;
324 my $deleted;
325 $self->_result->result_source->schema->txn_do(
326 sub {
327 # Delete Patron's holds
328 $self->holds->delete;
330 # Delete all lists and all shares of this borrower
331 # Consistent with the approach Koha uses on deleting individual lists
332 # Note that entries in virtualshelfcontents added by this borrower to
333 # lists of others will be handled by a table constraint: the borrower
334 # is set to NULL in those entries.
335 # NOTE:
336 # We could handle the above deletes via a constraint too.
337 # But a new BZ report 11889 has been opened to discuss another approach.
338 # Instead of deleting we could also disown lists (based on a pref).
339 # In that way we could save shared and public lists.
340 # The current table constraints support that idea now.
341 # This pref should then govern the results of other routines/methods such as
342 # Koha::Virtualshelf->new->delete too.
343 # FIXME Could be $patron->get_lists
344 $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
346 $deleted = $self->SUPER::delete;
348 logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
351 return $deleted;
355 =head3 category
357 my $patron_category = $patron->category
359 Return the patron category for this patron
361 =cut
363 sub category {
364 my ( $self ) = @_;
365 return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
368 =head3 guarantor
370 Returns a Koha::Patron object for this patron's guarantor
372 =cut
374 sub guarantor {
375 my ( $self ) = @_;
377 return unless $self->guarantorid();
379 return Koha::Patrons->find( $self->guarantorid() );
382 sub image {
383 my ( $self ) = @_;
385 return scalar Koha::Patron::Images->find( $self->borrowernumber );
388 sub library {
389 my ( $self ) = @_;
390 return Koha::Library->_new_from_dbic($self->_result->branchcode);
393 =head3 guarantees
395 Returns the guarantees (list of Koha::Patron) of this patron
397 =cut
399 sub guarantees {
400 my ( $self ) = @_;
402 return Koha::Patrons->search( { guarantorid => $self->borrowernumber }, { order_by => { -asc => ['surname','firstname'] } } );
405 =head3 housebound_profile
407 Returns the HouseboundProfile associated with this patron.
409 =cut
411 sub housebound_profile {
412 my ( $self ) = @_;
413 my $profile = $self->_result->housebound_profile;
414 return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
415 if ( $profile );
416 return;
419 =head3 housebound_role
421 Returns the HouseboundRole associated with this patron.
423 =cut
425 sub housebound_role {
426 my ( $self ) = @_;
428 my $role = $self->_result->housebound_role;
429 return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
430 return;
433 =head3 siblings
435 Returns the siblings of this patron.
437 =cut
439 sub siblings {
440 my ( $self ) = @_;
442 my $guarantor = $self->guarantor;
444 return unless $guarantor;
446 return Koha::Patrons->search(
448 guarantorid => {
449 '!=' => undef,
450 '=' => $guarantor->id,
452 borrowernumber => {
453 '!=' => $self->borrowernumber,
459 =head3 merge_with
461 my $patron = Koha::Patrons->find($id);
462 $patron->merge_with( \@patron_ids );
464 This subroutine merges a list of patrons into the patron record. This is accomplished by finding
465 all related patron ids for the patrons to be merged in other tables and changing the ids to be that
466 of the keeper patron.
468 =cut
470 sub merge_with {
471 my ( $self, $patron_ids ) = @_;
473 my @patron_ids = @{ $patron_ids };
475 # Ensure the keeper isn't in the list of patrons to merge
476 @patron_ids = grep { $_ ne $self->id } @patron_ids;
478 my $schema = Koha::Database->new()->schema();
480 my $results;
482 $self->_result->result_source->schema->txn_do( sub {
483 foreach my $patron_id (@patron_ids) {
484 my $patron = Koha::Patrons->find( $patron_id );
486 next unless $patron;
488 # Unbless for safety, the patron will end up being deleted
489 $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
491 while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
492 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
493 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
494 $rs->update({ $field => $self->id });
497 $patron->move_to_deleted();
498 $patron->delete();
502 return $results;
507 =head3 wants_check_for_previous_checkout
509 $wants_check = $patron->wants_check_for_previous_checkout;
511 Return 1 if Koha needs to perform PrevIssue checking, else 0.
513 =cut
515 sub wants_check_for_previous_checkout {
516 my ( $self ) = @_;
517 my $syspref = C4::Context->preference("checkPrevCheckout");
519 # Simple cases
520 ## Hard syspref trumps all
521 return 1 if ($syspref eq 'hardyes');
522 return 0 if ($syspref eq 'hardno');
523 ## Now, patron pref trumps all
524 return 1 if ($self->checkprevcheckout eq 'yes');
525 return 0 if ($self->checkprevcheckout eq 'no');
527 # More complex: patron inherits -> determine category preference
528 my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
529 return 1 if ($checkPrevCheckoutByCat eq 'yes');
530 return 0 if ($checkPrevCheckoutByCat eq 'no');
532 # Finally: category preference is inherit, default to 0
533 if ($syspref eq 'softyes') {
534 return 1;
535 } else {
536 return 0;
540 =head3 do_check_for_previous_checkout
542 $do_check = $patron->do_check_for_previous_checkout($item);
544 Return 1 if the bib associated with $ITEM has previously been checked out to
545 $PATRON, 0 otherwise.
547 =cut
549 sub do_check_for_previous_checkout {
550 my ( $self, $item ) = @_;
552 # Find all items for bib and extract item numbers.
553 my @items = Koha::Items->search({biblionumber => $item->{biblionumber}});
554 my @item_nos;
555 foreach my $item (@items) {
556 push @item_nos, $item->itemnumber;
559 # Create (old)issues search criteria
560 my $criteria = {
561 borrowernumber => $self->borrowernumber,
562 itemnumber => \@item_nos,
565 # Check current issues table
566 my $issues = Koha::Checkouts->search($criteria);
567 return 1 if $issues->count; # 0 || N
569 # Check old issues table
570 my $old_issues = Koha::Old::Checkouts->search($criteria);
571 return $old_issues->count; # 0 || N
574 =head3 is_debarred
576 my $debarment_expiration = $patron->is_debarred;
578 Returns the date a patron debarment will expire, or undef if the patron is not
579 debarred
581 =cut
583 sub is_debarred {
584 my ($self) = @_;
586 return unless $self->debarred;
587 return $self->debarred
588 if $self->debarred =~ '^9999'
589 or dt_from_string( $self->debarred ) > dt_from_string;
590 return;
593 =head3 is_expired
595 my $is_expired = $patron->is_expired;
597 Returns 1 if the patron is expired or 0;
599 =cut
601 sub is_expired {
602 my ($self) = @_;
603 return 0 unless $self->dateexpiry;
604 return 0 if $self->dateexpiry =~ '^9999';
605 return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
606 return 0;
609 =head3 is_going_to_expire
611 my $is_going_to_expire = $patron->is_going_to_expire;
613 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
615 =cut
617 sub is_going_to_expire {
618 my ($self) = @_;
620 my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
622 return 0 unless $delay;
623 return 0 unless $self->dateexpiry;
624 return 0 if $self->dateexpiry =~ '^9999';
625 return 1 if dt_from_string( $self->dateexpiry )->subtract( days => $delay ) < dt_from_string->truncate( to => 'day' );
626 return 0;
629 =head3 set_password
631 $patron->set_password({ password => $plain_text_password [, skip_validation => 1 ] });
633 Set the patron's password.
635 =head4 Exceptions
637 The passed string is validated against the current password enforcement policy.
638 Validation can be skipped by passing the I<skip_validation> parameter.
640 Exceptions are thrown if the password is not good enough.
642 =over 4
644 =item Koha::Exceptions::Password::TooShort
646 =item Koha::Exceptions::Password::WhitespaceCharacters
648 =item Koha::Exceptions::Password::TooWeak
650 =back
652 =cut
654 sub set_password {
655 my ( $self, $args ) = @_;
657 my $password = $args->{password};
659 unless ( $args->{skip_validation} ) {
660 my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password );
662 if ( !$is_valid ) {
663 if ( $error eq 'too_short' ) {
664 my $min_length = C4::Context->preference('minPasswordLength');
665 $min_length = 3 if not $min_length or $min_length < 3;
667 my $password_length = length($password);
668 Koha::Exceptions::Password::TooShort->throw(
669 length => $password_length, min_length => $min_length );
671 elsif ( $error eq 'has_whitespaces' ) {
672 Koha::Exceptions::Password::WhitespaceCharacters->throw();
674 elsif ( $error eq 'too_weak' ) {
675 Koha::Exceptions::Password::TooWeak->throw();
680 my $digest = Koha::AuthUtils::hash_password($password);
681 $self->update(
682 { password => $digest,
683 login_attempts => 0,
687 logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" )
688 if C4::Context->preference("BorrowersLog");
690 return $self;
694 =head3 renew_account
696 my $new_expiry_date = $patron->renew_account
698 Extending the subscription to the expiry date.
700 =cut
702 sub renew_account {
703 my ($self) = @_;
704 my $date;
705 if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
706 $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
707 } else {
708 $date =
709 C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
710 ? dt_from_string( $self->dateexpiry )
711 : dt_from_string;
713 my $expiry_date = $self->category->get_expiry_date($date);
715 $self->dateexpiry($expiry_date);
716 $self->date_renewed( dt_from_string() );
717 $self->store();
719 $self->add_enrolment_fee_if_needed;
721 logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
722 return dt_from_string( $expiry_date )->truncate( to => 'day' );
725 =head3 has_overdues
727 my $has_overdues = $patron->has_overdues;
729 Returns the number of patron's overdues
731 =cut
733 sub has_overdues {
734 my ($self) = @_;
735 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
736 return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
739 =head3 track_login
741 $patron->track_login;
742 $patron->track_login({ force => 1 });
744 Tracks a (successful) login attempt.
745 The preference TrackLastPatronActivity must be enabled. Or you
746 should pass the force parameter.
748 =cut
750 sub track_login {
751 my ( $self, $params ) = @_;
752 return if
753 !$params->{force} &&
754 !C4::Context->preference('TrackLastPatronActivity');
755 $self->lastseen( dt_from_string() )->store;
758 =head3 move_to_deleted
760 my $is_moved = $patron->move_to_deleted;
762 Move a patron to the deletedborrowers table.
763 This can be done before deleting a patron, to make sure the data are not completely deleted.
765 =cut
767 sub move_to_deleted {
768 my ($self) = @_;
769 my $patron_infos = $self->unblessed;
770 delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
771 return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
774 =head3 article_requests
776 my @requests = $borrower->article_requests();
777 my $requests = $borrower->article_requests();
779 Returns either a list of ArticleRequests objects,
780 or an ArtitleRequests object, depending on the
781 calling context.
783 =cut
785 sub article_requests {
786 my ( $self ) = @_;
788 $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
790 return $self->{_article_requests};
793 =head3 article_requests_current
795 my @requests = $patron->article_requests_current
797 Returns the article requests associated with this patron that are incomplete
799 =cut
801 sub article_requests_current {
802 my ( $self ) = @_;
804 $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
806 borrowernumber => $self->id(),
807 -or => [
808 { status => Koha::ArticleRequest::Status::Pending },
809 { status => Koha::ArticleRequest::Status::Processing }
814 return $self->{_article_requests_current};
817 =head3 article_requests_finished
819 my @requests = $biblio->article_requests_finished
821 Returns the article requests associated with this patron that are completed
823 =cut
825 sub article_requests_finished {
826 my ( $self, $borrower ) = @_;
828 $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
830 borrowernumber => $self->id(),
831 -or => [
832 { status => Koha::ArticleRequest::Status::Completed },
833 { status => Koha::ArticleRequest::Status::Canceled }
838 return $self->{_article_requests_finished};
841 =head3 add_enrolment_fee_if_needed
843 my $enrolment_fee = $patron->add_enrolment_fee_if_needed;
845 Add enrolment fee for a patron if needed.
847 =cut
849 sub add_enrolment_fee_if_needed {
850 my ($self) = @_;
851 my $enrolment_fee = $self->category->enrolmentfee;
852 if ( $enrolment_fee && $enrolment_fee > 0 ) {
853 $self->account->add_debit(
855 amount => $enrolment_fee,
856 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
857 interface => C4::Context->interface,
858 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
859 type => 'account'
863 return $enrolment_fee || 0;
866 =head3 checkouts
868 my $checkouts = $patron->checkouts
870 =cut
872 sub checkouts {
873 my ($self) = @_;
874 my $checkouts = $self->_result->issues;
875 return Koha::Checkouts->_new_from_dbic( $checkouts );
878 =head3 pending_checkouts
880 my $pending_checkouts = $patron->pending_checkouts
882 This method will return the same as $self->checkouts, but with a prefetch on
883 items, biblio and biblioitems.
885 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
887 It should not be used directly, prefer to access fields you need instead of
888 retrieving all these fields in one go.
890 =cut
892 sub pending_checkouts {
893 my( $self ) = @_;
894 my $checkouts = $self->_result->issues->search(
897 order_by => [
898 { -desc => 'me.timestamp' },
899 { -desc => 'issuedate' },
900 { -desc => 'issue_id' }, # Sort by issue_id should be enough
902 prefetch => { item => { biblio => 'biblioitems' } },
905 return Koha::Checkouts->_new_from_dbic( $checkouts );
908 =head3 old_checkouts
910 my $old_checkouts = $patron->old_checkouts
912 =cut
914 sub old_checkouts {
915 my ($self) = @_;
916 my $old_checkouts = $self->_result->old_issues;
917 return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
920 =head3 get_overdues
922 my $overdue_items = $patron->get_overdues
924 Return the overdue items
926 =cut
928 sub get_overdues {
929 my ($self) = @_;
930 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
931 return $self->checkouts->search(
933 'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
936 prefetch => { item => { biblio => 'biblioitems' } },
941 =head3 get_routing_lists
943 my @routinglists = $patron->get_routing_lists
945 Returns the routing lists a patron is subscribed to.
947 =cut
949 sub get_routing_lists {
950 my ($self) = @_;
951 my $routing_list_rs = $self->_result->subscriptionroutinglists;
952 return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
955 =head3 get_age
957 my $age = $patron->get_age
959 Return the age of the patron
961 =cut
963 sub get_age {
964 my ($self) = @_;
965 my $today_str = dt_from_string->strftime("%Y-%m-%d");
966 return unless $self->dateofbirth;
967 my $dob_str = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
969 my ( $dob_y, $dob_m, $dob_d ) = split /-/, $dob_str;
970 my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
972 my $age = $today_y - $dob_y;
973 if ( $dob_m . $dob_d > $today_m . $today_d ) {
974 $age--;
977 return $age;
980 =head3 is_valid_age
982 my $is_valid = $patron->is_valid_age
984 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
986 =cut
988 sub is_valid_age {
989 my ($self) = @_;
990 my $age = $self->get_age;
992 my $patroncategory = $self->category;
993 my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
995 return (defined($age) && (($high && ($age > $high)) or ($age < $low))) ? 0 : 1;
998 =head3 account
1000 my $account = $patron->account
1002 =cut
1004 sub account {
1005 my ($self) = @_;
1006 return Koha::Account->new( { patron_id => $self->borrowernumber } );
1009 =head3 holds
1011 my $holds = $patron->holds
1013 Return all the holds placed by this patron
1015 =cut
1017 sub holds {
1018 my ($self) = @_;
1019 my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1020 return Koha::Holds->_new_from_dbic($holds_rs);
1023 =head3 old_holds
1025 my $old_holds = $patron->old_holds
1027 Return all the historical holds for this patron
1029 =cut
1031 sub old_holds {
1032 my ($self) = @_;
1033 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1034 return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1037 =head3 notice_email_address
1039 my $email = $patron->notice_email_address;
1041 Return the email address of patron used for notices.
1042 Returns the empty string if no email address.
1044 =cut
1046 sub notice_email_address{
1047 my ( $self ) = @_;
1049 my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1050 # if syspref is set to 'first valid' (value == OFF), look up email address
1051 if ( $which_address eq 'OFF' ) {
1052 return $self->first_valid_email_address;
1055 return $self->$which_address || '';
1058 =head3 first_valid_email_address
1060 my $first_valid_email_address = $patron->first_valid_email_address
1062 Return the first valid email address for a patron.
1063 For now, the order is defined as email, emailpro, B_email.
1064 Returns the empty string if the borrower has no email addresses.
1066 =cut
1068 sub first_valid_email_address {
1069 my ($self) = @_;
1071 return $self->email() || $self->emailpro() || $self->B_email() || q{};
1074 =head3 get_club_enrollments
1076 =cut
1078 sub get_club_enrollments {
1079 my ( $self, $return_scalar ) = @_;
1081 my $e = Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1083 return $e if $return_scalar;
1085 return wantarray ? $e->as_list : $e;
1088 =head3 get_enrollable_clubs
1090 =cut
1092 sub get_enrollable_clubs {
1093 my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
1095 my $params;
1096 $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1097 if $is_enrollable_from_opac;
1098 $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1100 $params->{borrower} = $self;
1102 my $e = Koha::Clubs->get_enrollable($params);
1104 return $e if $return_scalar;
1106 return wantarray ? $e->as_list : $e;
1109 =head3 account_locked
1111 my $is_locked = $patron->account_locked
1113 Return true if the patron has reached the maximum number of login attempts
1114 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1115 as an administrative lockout (independent of FailedLoginAttempts; see also
1116 Koha::Patron->lock).
1117 Otherwise return false.
1118 If the pref is not set (empty string, null or 0), the feature is considered as
1119 disabled.
1121 =cut
1123 sub account_locked {
1124 my ($self) = @_;
1125 my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1126 return 1 if $FailedLoginAttempts
1127 and $self->login_attempts
1128 and $self->login_attempts >= $FailedLoginAttempts;
1129 return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1130 return 0;
1133 =head3 can_see_patron_infos
1135 my $can_see = $patron->can_see_patron_infos( $patron );
1137 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1139 =cut
1141 sub can_see_patron_infos {
1142 my ( $self, $patron ) = @_;
1143 return unless $patron;
1144 return $self->can_see_patrons_from( $patron->library->branchcode );
1147 =head3 can_see_patrons_from
1149 my $can_see = $patron->can_see_patrons_from( $branchcode );
1151 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1153 =cut
1155 sub can_see_patrons_from {
1156 my ( $self, $branchcode ) = @_;
1157 my $can = 0;
1158 if ( $self->branchcode eq $branchcode ) {
1159 $can = 1;
1160 } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1161 $can = 1;
1162 } elsif ( my $library_groups = $self->library->library_groups ) {
1163 while ( my $library_group = $library_groups->next ) {
1164 if ( $library_group->parent->has_child( $branchcode ) ) {
1165 $can = 1;
1166 last;
1170 return $can;
1173 =head3 libraries_where_can_see_patrons
1175 my $libraries = $patron-libraries_where_can_see_patrons;
1177 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1178 The branchcodes are arbitrarily returned sorted.
1179 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1181 An empty array means no restriction, the patron can see patron's infos from any libraries.
1183 =cut
1185 sub libraries_where_can_see_patrons {
1186 my ( $self ) = @_;
1187 my $userenv = C4::Context->userenv;
1189 return () unless $userenv; # For tests, but userenv should be defined in tests...
1191 my @restricted_branchcodes;
1192 if (C4::Context::only_my_library) {
1193 push @restricted_branchcodes, $self->branchcode;
1195 else {
1196 unless (
1197 $self->has_permission(
1198 { borrowers => 'view_borrower_infos_from_any_libraries' }
1202 my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1203 if ( $library_groups->count )
1205 while ( my $library_group = $library_groups->next ) {
1206 my $parent = $library_group->parent;
1207 if ( $parent->has_child( $self->branchcode ) ) {
1208 push @restricted_branchcodes, $parent->children->get_column('branchcode');
1213 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1217 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1218 @restricted_branchcodes = uniq(@restricted_branchcodes);
1219 @restricted_branchcodes = sort(@restricted_branchcodes);
1220 return @restricted_branchcodes;
1223 sub has_permission {
1224 my ( $self, $flagsrequired ) = @_;
1225 return unless $self->userid;
1226 # TODO code from haspermission needs to be moved here!
1227 return C4::Auth::haspermission( $self->userid, $flagsrequired );
1230 =head3 is_adult
1232 my $is_adult = $patron->is_adult
1234 Return true if the patron has a category with a type Adult (A) or Organization (I)
1236 =cut
1238 sub is_adult {
1239 my ( $self ) = @_;
1240 return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1243 =head3 is_child
1245 my $is_child = $patron->is_child
1247 Return true if the patron has a category with a type Child (C)
1249 =cut
1251 sub is_child {
1252 my( $self ) = @_;
1253 return $self->category->category_type eq 'C' ? 1 : 0;
1256 =head3 has_valid_userid
1258 my $patron = Koha::Patrons->find(42);
1259 $patron->userid( $new_userid );
1260 my $has_a_valid_userid = $patron->has_valid_userid
1262 my $patron = Koha::Patron->new( $params );
1263 my $has_a_valid_userid = $patron->has_valid_userid
1265 Return true if the current userid of this patron is valid/unique, otherwise false.
1267 Note that this should be done in $self->store instead and raise an exception if needed.
1269 =cut
1271 sub has_valid_userid {
1272 my ($self) = @_;
1274 return 0 unless $self->userid;
1276 return 0 if ( $self->userid eq C4::Context->config('user') ); # DB user
1278 my $already_exists = Koha::Patrons->search(
1280 userid => $self->userid,
1282 $self->in_storage
1283 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1284 : ()
1287 )->count;
1288 return $already_exists ? 0 : 1;
1291 =head3 generate_userid
1293 my $patron = Koha::Patron->new( $params );
1294 $patron->generate_userid
1296 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1298 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).
1300 =cut
1302 sub generate_userid {
1303 my ($self) = @_;
1304 my $offset = 0;
1305 my $firstname = $self->firstname // q{};
1306 my $surname = $self->surname // q{};
1307 #The script will "do" the following code and increment the $offset until the generated userid is unique
1308 do {
1309 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1310 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1311 my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1312 $userid = unac_string('utf-8',$userid);
1313 $userid .= $offset unless $offset == 0;
1314 $self->userid( $userid );
1315 $offset++;
1316 } while (! $self->has_valid_userid );
1318 return $self;
1322 =head3 attributes
1324 my $attributes = $patron->attributes
1326 Return object of Koha::Patron::Attributes type with all attributes set for this patron
1328 =cut
1330 sub attributes {
1331 my ( $self ) = @_;
1332 return Koha::Patron::Attributes->search({
1333 borrowernumber => $self->borrowernumber,
1334 branchcode => $self->branchcode,
1338 =head3 lock
1340 Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
1342 Lock and optionally expire a patron account.
1343 Remove holds and article requests if remove flag set.
1344 In order to distinguish from locking by entering a wrong password, let's
1345 call this an administrative lockout.
1347 =cut
1349 sub lock {
1350 my ( $self, $params ) = @_;
1351 $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
1352 if( $params->{expire} ) {
1353 $self->dateexpiry( dt_from_string->subtract(days => 1) );
1355 $self->store;
1356 if( $params->{remove} ) {
1357 $self->holds->delete;
1358 $self->article_requests->delete;
1360 return $self;
1363 =head3 anonymize
1365 Koha::Patrons->find($id)->anonymize;
1367 Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
1368 are randomized, other personal data is cleared too.
1369 Patrons with issues are skipped.
1371 =cut
1373 sub anonymize {
1374 my ( $self ) = @_;
1375 if( $self->_result->issues->count ) {
1376 warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
1377 return;
1379 # Mandatory fields come from the corresponding pref, but email fields
1380 # are removed since scrambled email addresses only generate errors
1381 my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
1382 split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
1383 $mandatory->{userid} = 1; # needed since sub store does not clear field
1384 my @columns = $self->_result->result_source->columns;
1385 @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized/ } @columns;
1386 push @columns, 'dateofbirth'; # add this date back in
1387 foreach my $col (@columns) {
1388 $self->_anonymize_column($col, $mandatory->{lc $col} );
1390 $self->anonymized(1)->store;
1393 sub _anonymize_column {
1394 my ( $self, $col, $mandatory ) = @_;
1395 my $col_info = $self->_result->result_source->column_info($col);
1396 my $type = $col_info->{data_type};
1397 my $nullable = $col_info->{is_nullable};
1398 my $val;
1399 if( $type =~ /char|text/ ) {
1400 $val = $mandatory
1401 ? Koha::Token->new->generate({ pattern => '\w{10}' })
1402 : $nullable
1403 ? undef
1404 : q{};
1405 } elsif( $type =~ /integer|int$|float|dec|double/ ) {
1406 $val = $nullable ? undef : 0;
1407 } elsif( $type =~ /date|time/ ) {
1408 $val = $nullable ? undef : dt_from_string;
1410 $self->$col($val);
1413 =head2 Internal methods
1415 =head3 _type
1417 =cut
1419 sub _type {
1420 return 'Borrower';
1423 =head1 AUTHORS
1425 Kyle M Hall <kyle@bywatersolutions.com>
1426 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1427 Martin Renvoize <martin.renvoize@ptfs-europe.com>
1429 =cut