Bug 19625: Enable Shibboleth auto-provisioning for Plack
[koha.git] / Koha / Patron.pm
blob1637a042ac8e93e5ee2c2ccca4ce9a9ef09ba779
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( uniq );
25 use JSON qw( to_json );
26 use Text::Unaccent qw( unac_string );
28 use C4::Accounts;
29 use C4::Context;
30 use C4::Log;
31 use Koha::AuthUtils;
32 use Koha::Checkouts;
33 use Koha::Database;
34 use Koha::DateUtils;
35 use Koha::Holds;
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;
41 use Koha::Patrons;
42 use Koha::Virtualshelves;
43 use Koha::Club::Enrollments;
44 use Koha::Account;
45 use Koha::Subscription::Routinglists;
47 use base qw(Koha::Object);
49 our $RESULTSET_PATRON_ID_MAPPING = {
50 Accountline => 'borrowernumber',
51 Aqbasketuser => 'borrowernumber',
52 Aqbudget => 'budget_owner_id',
53 Aqbudgetborrower => 'borrowernumber',
54 ArticleRequest => 'borrowernumber',
55 BorrowerAttribute => 'borrowernumber',
56 BorrowerDebarment => 'borrowernumber',
57 BorrowerFile => 'borrowernumber',
58 BorrowerModification => 'borrowernumber',
59 ClubEnrollment => 'borrowernumber',
60 Issue => 'borrowernumber',
61 ItemsLastBorrower => 'borrowernumber',
62 Linktracker => 'borrowernumber',
63 Message => 'borrowernumber',
64 MessageQueue => 'borrowernumber',
65 OldIssue => 'borrowernumber',
66 OldReserve => 'borrowernumber',
67 Rating => 'borrowernumber',
68 Reserve => 'borrowernumber',
69 Review => 'borrowernumber',
70 SearchHistory => 'userid',
71 Statistic => 'borrowernumber',
72 Suggestion => 'suggestedby',
73 TagAll => 'borrowernumber',
74 Virtualshelfcontent => 'borrowernumber',
75 Virtualshelfshare => 'borrowernumber',
76 Virtualshelve => 'owner',
79 =head1 NAME
81 Koha::Patron - Koha Patron Object class
83 =head1 API
85 =head2 Class Methods
87 =cut
89 =head3 new
91 =cut
93 sub new {
94 my ( $class, $params ) = @_;
96 return $class->SUPER::new($params);
99 =head3 fixup_cardnumber
101 Autogenerate next cardnumber from highest value found in database
103 =cut
105 sub fixup_cardnumber {
106 my ( $self ) = @_;
107 my $max = Koha::Patrons->search({
108 cardnumber => {-regexp => '^-?[0-9]+$'}
109 }, {
110 select => \'CAST(cardnumber AS SIGNED)',
111 as => ['cast_cardnumber']
112 })->_resultset->get_column('cast_cardnumber')->max;
113 $self->cardnumber(($max || 0) +1);
116 =head3 trim_whitespace
118 trim whitespace from data which has some non-whitespace in it.
119 Could be moved to Koha::Object if need to be reused
121 =cut
123 sub trim_whitespaces {
124 my( $self ) = @_;
126 my $schema = Koha::Database->new->schema;
127 my @columns = $schema->source($self->_type)->columns;
129 for my $column( @columns ) {
130 my $value = $self->$column;
131 if ( defined $value ) {
132 $value =~ s/^\s*|\s*$//g;
133 $self->$column($value);
136 return $self;
139 =head3 plain_text_password
141 $patron->plain_text_password( $password );
143 stores a copy of the unencrypted password in the object
144 for use in code before encrypting for db
146 =cut
148 sub plain_text_password {
149 my ( $self, $password ) = @_;
150 if ( $password ) {
151 $self->{_plain_text_password} = $password;
152 return $self;
154 return $self->{_plain_text_password}
155 if $self->{_plain_text_password};
157 return;
160 =head3 store
162 Patron specific store method to cleanup record
163 and do other necessary things before saving
164 to db
166 =cut
168 sub store {
169 my ($self) = @_;
171 $self->_result->result_source->schema->txn_do(
172 sub {
173 if (
174 C4::Context->preference("autoMemberNum")
175 and ( not defined $self->cardnumber
176 or $self->cardnumber eq '' )
179 # Warning: The caller is responsible for locking the members table in write
180 # mode, to avoid database corruption.
181 # We are in a transaction but the table is not locked
182 $self->fixup_cardnumber;
185 unless( $self->category->in_storage ) {
186 Koha::Exceptions::Object::FKConstraint->throw(
187 broken_fk => 'categorycode',
188 value => $self->categorycode,
192 $self->trim_whitespaces;
194 # We don't want invalid dates in the db (mysql has a bad habit of inserting 0000-00-00)
195 $self->dateofbirth(undef) unless $self->dateofbirth;
196 $self->debarred(undef) unless $self->debarred;
198 # Set default values if not set
199 $self->sms_provider_id(undef) unless $self->sms_provider_id;
200 $self->guarantorid(undef) unless $self->guarantorid;
202 unless ( $self->in_storage ) { #AddMember
204 # Generate a valid userid/login if needed
205 $self->generate_userid
206 if not $self->userid or not $self->has_valid_userid;
208 # Add expiration date if it isn't already there
209 unless ( $self->dateexpiry ) {
210 $self->dateexpiry( $self->category->get_expiry_date );
213 # Add enrollment date if it isn't already there
214 unless ( $self->dateenrolled ) {
215 $self->dateenrolled(dt_from_string);
218 # Set the privacy depending on the patron's category
219 my $default_privacy = $self->category->default_privacy || q{};
220 $default_privacy =
221 $default_privacy eq 'default' ? 1
222 : $default_privacy eq 'never' ? 2
223 : $default_privacy eq 'forever' ? 0
224 : undef;
225 $self->privacy($default_privacy);
227 unless ( defined $self->privacy_guarantor_checkouts ) {
228 $self->privacy_guarantor_checkouts(0);
231 # Make a copy of the plain text password for later use
232 $self->plain_text_password( $self->password );
234 # Create a disabled account if no password provided
235 $self->password( $self->password
236 ? Koha::AuthUtils::hash_password( $self->password )
237 : '!' );
239 $self->borrowernumber(undef);
241 $self = $self->SUPER::store;
243 $self->add_enrolment_fee_if_needed;
245 logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
246 if C4::Context->preference("BorrowersLog");
248 else { #ModMember
250 # Come from ModMember, but should not be possible (?)
251 $self->dateenrolled(undef) unless $self->dateenrolled;
252 $self->dateexpiry(undef) unless $self->dateexpiry;
255 my $self_from_storage = $self->get_from_storage;
256 # FIXME We should not deal with that here, callers have to do this job
257 # Moved from ModMember to prevent regressions
258 unless ( $self->userid ) {
259 my $stored_userid = $self_from_storage->userid;
260 $self->userid($stored_userid);
263 # Password must be updated using $self->update_password
264 $self->password($self_from_storage->password);
266 if ( C4::Context->preference('FeeOnChangePatronCategory')
267 and $self->category->categorycode ne
268 $self_from_storage->category->categorycode )
270 $self->add_enrolment_fee_if_needed;
273 my $borrowers_log = C4::Context->preference("BorrowersLog");
274 my $previous_cardnumber = $self_from_storage->cardnumber;
275 if ($borrowers_log
276 && ( !defined $previous_cardnumber
277 || $previous_cardnumber ne $self->cardnumber )
280 logaction(
281 "MEMBERS",
282 "MODIFY",
283 $self->borrowernumber,
284 to_json(
286 cardnumber_replaced => {
287 previous_cardnumber => $previous_cardnumber,
288 new_cardnumber => $self->cardnumber,
291 { utf8 => 1, pretty => 1 }
296 logaction( "MEMBERS", "MODIFY", $self->borrowernumber,
297 "UPDATE (executed w/ arg: " . $self->borrowernumber . ")" )
298 if $borrowers_log;
300 $self = $self->SUPER::store;
304 return $self;
307 =head3 delete
309 $patron->delete
311 Delete patron's holds, lists and finally the patron.
313 Lists owned by the borrower are deleted, but entries from the borrower to
314 other lists are kept.
316 =cut
318 sub delete {
319 my ($self) = @_;
321 my $deleted;
322 $self->_result->result_source->schema->txn_do(
323 sub {
324 # Delete Patron's holds
325 $self->holds->delete;
327 # Delete all lists and all shares of this borrower
328 # Consistent with the approach Koha uses on deleting individual lists
329 # Note that entries in virtualshelfcontents added by this borrower to
330 # lists of others will be handled by a table constraint: the borrower
331 # is set to NULL in those entries.
332 # NOTE:
333 # We could handle the above deletes via a constraint too.
334 # But a new BZ report 11889 has been opened to discuss another approach.
335 # Instead of deleting we could also disown lists (based on a pref).
336 # In that way we could save shared and public lists.
337 # The current table constraints support that idea now.
338 # This pref should then govern the results of other routines/methods such as
339 # Koha::Virtualshelf->new->delete too.
340 # FIXME Could be $patron->get_lists
341 $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
343 $deleted = $self->SUPER::delete;
345 logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
348 return $deleted;
352 =head3 category
354 my $patron_category = $patron->category
356 Return the patron category for this patron
358 =cut
360 sub category {
361 my ( $self ) = @_;
362 return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
365 =head3 guarantor
367 Returns a Koha::Patron object for this patron's guarantor
369 =cut
371 sub guarantor {
372 my ( $self ) = @_;
374 return unless $self->guarantorid();
376 return Koha::Patrons->find( $self->guarantorid() );
379 sub image {
380 my ( $self ) = @_;
382 return scalar Koha::Patron::Images->find( $self->borrowernumber );
385 sub library {
386 my ( $self ) = @_;
387 return Koha::Library->_new_from_dbic($self->_result->branchcode);
390 =head3 guarantees
392 Returns the guarantees (list of Koha::Patron) of this patron
394 =cut
396 sub guarantees {
397 my ( $self ) = @_;
399 return Koha::Patrons->search( { guarantorid => $self->borrowernumber }, { order_by => { -asc => ['surname','firstname'] } } );
402 =head3 housebound_profile
404 Returns the HouseboundProfile associated with this patron.
406 =cut
408 sub housebound_profile {
409 my ( $self ) = @_;
410 my $profile = $self->_result->housebound_profile;
411 return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
412 if ( $profile );
413 return;
416 =head3 housebound_role
418 Returns the HouseboundRole associated with this patron.
420 =cut
422 sub housebound_role {
423 my ( $self ) = @_;
425 my $role = $self->_result->housebound_role;
426 return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
427 return;
430 =head3 siblings
432 Returns the siblings of this patron.
434 =cut
436 sub siblings {
437 my ( $self ) = @_;
439 my $guarantor = $self->guarantor;
441 return unless $guarantor;
443 return Koha::Patrons->search(
445 guarantorid => {
446 '!=' => undef,
447 '=' => $guarantor->id,
449 borrowernumber => {
450 '!=' => $self->borrowernumber,
456 =head3 merge_with
458 my $patron = Koha::Patrons->find($id);
459 $patron->merge_with( \@patron_ids );
461 This subroutine merges a list of patrons into the patron record. This is accomplished by finding
462 all related patron ids for the patrons to be merged in other tables and changing the ids to be that
463 of the keeper patron.
465 =cut
467 sub merge_with {
468 my ( $self, $patron_ids ) = @_;
470 my @patron_ids = @{ $patron_ids };
472 # Ensure the keeper isn't in the list of patrons to merge
473 @patron_ids = grep { $_ ne $self->id } @patron_ids;
475 my $schema = Koha::Database->new()->schema();
477 my $results;
479 $self->_result->result_source->schema->txn_do( sub {
480 foreach my $patron_id (@patron_ids) {
481 my $patron = Koha::Patrons->find( $patron_id );
483 next unless $patron;
485 # Unbless for safety, the patron will end up being deleted
486 $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
488 while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
489 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
490 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
491 $rs->update({ $field => $self->id });
494 $patron->move_to_deleted();
495 $patron->delete();
499 return $results;
504 =head3 wants_check_for_previous_checkout
506 $wants_check = $patron->wants_check_for_previous_checkout;
508 Return 1 if Koha needs to perform PrevIssue checking, else 0.
510 =cut
512 sub wants_check_for_previous_checkout {
513 my ( $self ) = @_;
514 my $syspref = C4::Context->preference("checkPrevCheckout");
516 # Simple cases
517 ## Hard syspref trumps all
518 return 1 if ($syspref eq 'hardyes');
519 return 0 if ($syspref eq 'hardno');
520 ## Now, patron pref trumps all
521 return 1 if ($self->checkprevcheckout eq 'yes');
522 return 0 if ($self->checkprevcheckout eq 'no');
524 # More complex: patron inherits -> determine category preference
525 my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
526 return 1 if ($checkPrevCheckoutByCat eq 'yes');
527 return 0 if ($checkPrevCheckoutByCat eq 'no');
529 # Finally: category preference is inherit, default to 0
530 if ($syspref eq 'softyes') {
531 return 1;
532 } else {
533 return 0;
537 =head3 do_check_for_previous_checkout
539 $do_check = $patron->do_check_for_previous_checkout($item);
541 Return 1 if the bib associated with $ITEM has previously been checked out to
542 $PATRON, 0 otherwise.
544 =cut
546 sub do_check_for_previous_checkout {
547 my ( $self, $item ) = @_;
549 # Find all items for bib and extract item numbers.
550 my @items = Koha::Items->search({biblionumber => $item->{biblionumber}});
551 my @item_nos;
552 foreach my $item (@items) {
553 push @item_nos, $item->itemnumber;
556 # Create (old)issues search criteria
557 my $criteria = {
558 borrowernumber => $self->borrowernumber,
559 itemnumber => \@item_nos,
562 # Check current issues table
563 my $issues = Koha::Checkouts->search($criteria);
564 return 1 if $issues->count; # 0 || N
566 # Check old issues table
567 my $old_issues = Koha::Old::Checkouts->search($criteria);
568 return $old_issues->count; # 0 || N
571 =head3 is_debarred
573 my $debarment_expiration = $patron->is_debarred;
575 Returns the date a patron debarment will expire, or undef if the patron is not
576 debarred
578 =cut
580 sub is_debarred {
581 my ($self) = @_;
583 return unless $self->debarred;
584 return $self->debarred
585 if $self->debarred =~ '^9999'
586 or dt_from_string( $self->debarred ) > dt_from_string;
587 return;
590 =head3 is_expired
592 my $is_expired = $patron->is_expired;
594 Returns 1 if the patron is expired or 0;
596 =cut
598 sub is_expired {
599 my ($self) = @_;
600 return 0 unless $self->dateexpiry;
601 return 0 if $self->dateexpiry =~ '^9999';
602 return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
603 return 0;
606 =head3 is_going_to_expire
608 my $is_going_to_expire = $patron->is_going_to_expire;
610 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
612 =cut
614 sub is_going_to_expire {
615 my ($self) = @_;
617 my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
619 return 0 unless $delay;
620 return 0 unless $self->dateexpiry;
621 return 0 if $self->dateexpiry =~ '^9999';
622 return 1 if dt_from_string( $self->dateexpiry )->subtract( days => $delay ) < dt_from_string->truncate( to => 'day' );
623 return 0;
626 =head3 update_password
628 my $updated = $patron->update_password( $userid, $password );
630 Update the userid and the password of a patron.
631 If the userid already exists, returns and let DBIx::Class warns
632 This will add an entry to action_logs if BorrowersLog is set.
634 =cut
636 sub update_password {
637 my ( $self, $userid, $password ) = @_;
638 eval { $self->userid($userid)->store; };
639 return if $@; # Make sure the userid is not already in used by another patron
641 return 0 if $password eq '****' or $password eq '';
643 my $digest = Koha::AuthUtils::hash_password($password);
644 $self->update(
646 password => $digest,
647 login_attempts => 0,
651 logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
652 return $digest;
655 =head3 renew_account
657 my $new_expiry_date = $patron->renew_account
659 Extending the subscription to the expiry date.
661 =cut
663 sub renew_account {
664 my ($self) = @_;
665 my $date;
666 if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
667 $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
668 } else {
669 $date =
670 C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
671 ? dt_from_string( $self->dateexpiry )
672 : dt_from_string;
674 my $expiry_date = $self->category->get_expiry_date($date);
676 $self->dateexpiry($expiry_date);
677 $self->date_renewed( dt_from_string() );
678 $self->store();
680 $self->add_enrolment_fee_if_needed;
682 logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
683 return dt_from_string( $expiry_date )->truncate( to => 'day' );
686 =head3 has_overdues
688 my $has_overdues = $patron->has_overdues;
690 Returns the number of patron's overdues
692 =cut
694 sub has_overdues {
695 my ($self) = @_;
696 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
697 return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
700 =head3 track_login
702 $patron->track_login;
703 $patron->track_login({ force => 1 });
705 Tracks a (successful) login attempt.
706 The preference TrackLastPatronActivity must be enabled. Or you
707 should pass the force parameter.
709 =cut
711 sub track_login {
712 my ( $self, $params ) = @_;
713 return if
714 !$params->{force} &&
715 !C4::Context->preference('TrackLastPatronActivity');
716 $self->lastseen( dt_from_string() )->store;
719 =head3 move_to_deleted
721 my $is_moved = $patron->move_to_deleted;
723 Move a patron to the deletedborrowers table.
724 This can be done before deleting a patron, to make sure the data are not completely deleted.
726 =cut
728 sub move_to_deleted {
729 my ($self) = @_;
730 my $patron_infos = $self->unblessed;
731 delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
732 return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
735 =head3 article_requests
737 my @requests = $borrower->article_requests();
738 my $requests = $borrower->article_requests();
740 Returns either a list of ArticleRequests objects,
741 or an ArtitleRequests object, depending on the
742 calling context.
744 =cut
746 sub article_requests {
747 my ( $self ) = @_;
749 $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
751 return $self->{_article_requests};
754 =head3 article_requests_current
756 my @requests = $patron->article_requests_current
758 Returns the article requests associated with this patron that are incomplete
760 =cut
762 sub article_requests_current {
763 my ( $self ) = @_;
765 $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
767 borrowernumber => $self->id(),
768 -or => [
769 { status => Koha::ArticleRequest::Status::Pending },
770 { status => Koha::ArticleRequest::Status::Processing }
775 return $self->{_article_requests_current};
778 =head3 article_requests_finished
780 my @requests = $biblio->article_requests_finished
782 Returns the article requests associated with this patron that are completed
784 =cut
786 sub article_requests_finished {
787 my ( $self, $borrower ) = @_;
789 $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
791 borrowernumber => $self->id(),
792 -or => [
793 { status => Koha::ArticleRequest::Status::Completed },
794 { status => Koha::ArticleRequest::Status::Canceled }
799 return $self->{_article_requests_finished};
802 =head3 add_enrolment_fee_if_needed
804 my $enrolment_fee = $patron->add_enrolment_fee_if_needed;
806 Add enrolment fee for a patron if needed.
808 =cut
810 sub add_enrolment_fee_if_needed {
811 my ($self) = @_;
812 my $enrolment_fee = $self->category->enrolmentfee;
813 if ( $enrolment_fee && $enrolment_fee > 0 ) {
814 # insert fee in patron debts
815 C4::Accounts::manualinvoice( $self->borrowernumber, '', '', 'A', $enrolment_fee );
817 return $enrolment_fee || 0;
820 =head3 checkouts
822 my $checkouts = $patron->checkouts
824 =cut
826 sub checkouts {
827 my ($self) = @_;
828 my $checkouts = $self->_result->issues;
829 return Koha::Checkouts->_new_from_dbic( $checkouts );
832 =head3 pending_checkouts
834 my $pending_checkouts = $patron->pending_checkouts
836 This method will return the same as $self->checkouts, but with a prefetch on
837 items, biblio and biblioitems.
839 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
841 It should not be used directly, prefer to access fields you need instead of
842 retrieving all these fields in one go.
845 =cut
847 sub pending_checkouts {
848 my( $self ) = @_;
849 my $checkouts = $self->_result->issues->search(
852 order_by => [
853 { -desc => 'me.timestamp' },
854 { -desc => 'issuedate' },
855 { -desc => 'issue_id' }, # Sort by issue_id should be enough
857 prefetch => { item => { biblio => 'biblioitems' } },
860 return Koha::Checkouts->_new_from_dbic( $checkouts );
863 =head3 old_checkouts
865 my $old_checkouts = $patron->old_checkouts
867 =cut
869 sub old_checkouts {
870 my ($self) = @_;
871 my $old_checkouts = $self->_result->old_issues;
872 return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
875 =head3 get_overdues
877 my $overdue_items = $patron->get_overdues
879 Return the overdue items
881 =cut
883 sub get_overdues {
884 my ($self) = @_;
885 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
886 return $self->checkouts->search(
888 'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
891 prefetch => { item => { biblio => 'biblioitems' } },
896 =head3 get_routing_lists
898 my @routinglists = $patron->get_routing_lists
900 Returns the routing lists a patron is subscribed to.
902 =cut
904 sub get_routing_lists {
905 my ($self) = @_;
906 my $routing_list_rs = $self->_result->subscriptionroutinglists;
907 return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
910 =head3 get_age
912 my $age = $patron->get_age
914 Return the age of the patron
916 =cut
918 sub get_age {
919 my ($self) = @_;
920 my $today_str = dt_from_string->strftime("%Y-%m-%d");
921 return unless $self->dateofbirth;
922 my $dob_str = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
924 my ( $dob_y, $dob_m, $dob_d ) = split /-/, $dob_str;
925 my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
927 my $age = $today_y - $dob_y;
928 if ( $dob_m . $dob_d > $today_m . $today_d ) {
929 $age--;
932 return $age;
935 =head3 account
937 my $account = $patron->account
939 =cut
941 sub account {
942 my ($self) = @_;
943 return Koha::Account->new( { patron_id => $self->borrowernumber } );
946 =head3 holds
948 my $holds = $patron->holds
950 Return all the holds placed by this patron
952 =cut
954 sub holds {
955 my ($self) = @_;
956 my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
957 return Koha::Holds->_new_from_dbic($holds_rs);
960 =head3 old_holds
962 my $old_holds = $patron->old_holds
964 Return all the historical holds for this patron
966 =cut
968 sub old_holds {
969 my ($self) = @_;
970 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
971 return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
974 =head3 notice_email_address
976 my $email = $patron->notice_email_address;
978 Return the email address of patron used for notices.
979 Returns the empty string if no email address.
981 =cut
983 sub notice_email_address{
984 my ( $self ) = @_;
986 my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
987 # if syspref is set to 'first valid' (value == OFF), look up email address
988 if ( $which_address eq 'OFF' ) {
989 return $self->first_valid_email_address;
992 return $self->$which_address || '';
995 =head3 first_valid_email_address
997 my $first_valid_email_address = $patron->first_valid_email_address
999 Return the first valid email address for a patron.
1000 For now, the order is defined as email, emailpro, B_email.
1001 Returns the empty string if the borrower has no email addresses.
1003 =cut
1005 sub first_valid_email_address {
1006 my ($self) = @_;
1008 return $self->email() || $self->emailpro() || $self->B_email() || q{};
1011 =head3 get_club_enrollments
1013 =cut
1015 sub get_club_enrollments {
1016 my ( $self, $return_scalar ) = @_;
1018 my $e = Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1020 return $e if $return_scalar;
1022 return wantarray ? $e->as_list : $e;
1025 =head3 get_enrollable_clubs
1027 =cut
1029 sub get_enrollable_clubs {
1030 my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
1032 my $params;
1033 $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1034 if $is_enrollable_from_opac;
1035 $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1037 $params->{borrower} = $self;
1039 my $e = Koha::Clubs->get_enrollable($params);
1041 return $e if $return_scalar;
1043 return wantarray ? $e->as_list : $e;
1046 =head3 account_locked
1048 my $is_locked = $patron->account_locked
1050 Return true if the patron has reach the maximum number of login attempts (see pref FailedLoginAttempts).
1051 Otherwise return false.
1052 If the pref is not set (empty string, null or 0), the feature is considered as disabled.
1054 =cut
1056 sub account_locked {
1057 my ($self) = @_;
1058 my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1059 return ( $FailedLoginAttempts
1060 and $self->login_attempts
1061 and $self->login_attempts >= $FailedLoginAttempts )? 1 : 0;
1064 =head3 can_see_patron_infos
1066 my $can_see = $patron->can_see_patron_infos( $patron );
1068 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1070 =cut
1072 sub can_see_patron_infos {
1073 my ( $self, $patron ) = @_;
1074 return $self->can_see_patrons_from( $patron->library->branchcode );
1077 =head3 can_see_patrons_from
1079 my $can_see = $patron->can_see_patrons_from( $branchcode );
1081 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1083 =cut
1085 sub can_see_patrons_from {
1086 my ( $self, $branchcode ) = @_;
1087 my $can = 0;
1088 if ( $self->branchcode eq $branchcode ) {
1089 $can = 1;
1090 } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1091 $can = 1;
1092 } elsif ( my $library_groups = $self->library->library_groups ) {
1093 while ( my $library_group = $library_groups->next ) {
1094 if ( $library_group->parent->has_child( $branchcode ) ) {
1095 $can = 1;
1096 last;
1100 return $can;
1103 =head3 libraries_where_can_see_patrons
1105 my $libraries = $patron-libraries_where_can_see_patrons;
1107 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1108 The branchcodes are arbitrarily returned sorted.
1109 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1111 An empty array means no restriction, the patron can see patron's infos from any libraries.
1113 =cut
1115 sub libraries_where_can_see_patrons {
1116 my ( $self ) = @_;
1117 my $userenv = C4::Context->userenv;
1119 return () unless $userenv; # For tests, but userenv should be defined in tests...
1121 my @restricted_branchcodes;
1122 if (C4::Context::only_my_library) {
1123 push @restricted_branchcodes, $self->branchcode;
1125 else {
1126 unless (
1127 $self->has_permission(
1128 { borrowers => 'view_borrower_infos_from_any_libraries' }
1132 my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1133 if ( $library_groups->count )
1135 while ( my $library_group = $library_groups->next ) {
1136 my $parent = $library_group->parent;
1137 if ( $parent->has_child( $self->branchcode ) ) {
1138 push @restricted_branchcodes, $parent->children->get_column('branchcode');
1143 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1147 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1148 @restricted_branchcodes = uniq(@restricted_branchcodes);
1149 @restricted_branchcodes = sort(@restricted_branchcodes);
1150 return @restricted_branchcodes;
1153 sub has_permission {
1154 my ( $self, $flagsrequired ) = @_;
1155 return unless $self->userid;
1156 # TODO code from haspermission needs to be moved here!
1157 return C4::Auth::haspermission( $self->userid, $flagsrequired );
1160 =head3 is_adult
1162 my $is_adult = $patron->is_adult
1164 Return true if the patron has a category with a type Adult (A) or Organization (I)
1166 =cut
1168 sub is_adult {
1169 my ( $self ) = @_;
1170 return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1173 =head3 is_child
1175 my $is_child = $patron->is_child
1177 Return true if the patron has a category with a type Child (C)
1179 =cut
1180 sub is_child {
1181 my( $self ) = @_;
1182 return $self->category->category_type eq 'C' ? 1 : 0;
1185 =head3 has_valid_userid
1187 my $patron = Koha::Patrons->find(42);
1188 $patron->userid( $new_userid );
1189 my $has_a_valid_userid = $patron->has_valid_userid
1191 my $patron = Koha::Patron->new( $params );
1192 my $has_a_valid_userid = $patron->has_valid_userid
1194 Return true if the current userid of this patron is valid/unique, otherwise false.
1196 Note that this should be done in $self->store instead and raise an exception if needed.
1198 =cut
1200 sub has_valid_userid {
1201 my ($self) = @_;
1203 return 0 unless $self->userid;
1205 return 0 if ( $self->userid eq C4::Context->config('user') ); # DB user
1207 my $already_exists = Koha::Patrons->search(
1209 userid => $self->userid,
1211 $self->in_storage
1212 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1213 : ()
1216 )->count;
1217 return $already_exists ? 0 : 1;
1220 =head3 generate_userid
1222 my $patron = Koha::Patron->new( $params );
1223 $patron->generate_userid
1225 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1227 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).
1229 =cut
1231 sub generate_userid {
1232 my ($self) = @_;
1233 my $offset = 0;
1234 my $firstname = $self->firstname // q{};
1235 my $surname = $self->surname // q{};
1236 #The script will "do" the following code and increment the $offset until the generated userid is unique
1237 do {
1238 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1239 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1240 my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1241 $userid = unac_string('utf-8',$userid);
1242 $userid .= $offset unless $offset == 0;
1243 $self->userid( $userid );
1244 $offset++;
1245 } while (! $self->has_valid_userid );
1247 return $self;
1251 =head2 Internal methods
1253 =head3 _type
1255 =cut
1257 sub _type {
1258 return 'Borrower';
1261 =head1 AUTHOR
1263 Kyle M Hall <kyle@bywatersolutions.com>
1264 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1266 =cut