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
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
23 use List::MoreUtils qw( any uniq );
24 use JSON qw( to_json );
25 use Unicode::Normalize qw( NFKD );
29 use C4::Auth qw( checkpw_hash );
31 use C4::Letters qw( GetPreparedLetter EnqueueLetter SendQueuedMessages );
32 use C4::Log qw( logaction );
35 use Koha::ArticleRequests;
39 use Koha::CirculationRules;
40 use Koha::Club::Enrollments;
41 use Koha::CurbsidePickups;
43 use Koha::DateUtils qw( dt_from_string );
46 use Koha::Exceptions::Password;
48 use Koha::Old::Checkouts;
49 use Koha::OverdueRules;
50 use Koha::Patron::Attributes;
51 use Koha::Patron::Categories;
52 use Koha::Patron::Consents;
53 use Koha::Patron::Debarments;
54 use Koha::Patron::HouseboundProfile;
55 use Koha::Patron::HouseboundRole;
56 use Koha::Patron::Images;
57 use Koha::Patron::Messages;
58 use Koha::Patron::Modifications;
59 use Koha::Patron::MessagePreferences;
60 use Koha::Patron::Relationships;
61 use Koha::Patron::Restrictions;
65 use Koha::Result::Boolean;
66 use Koha::Subscription::Routinglists;
68 use Koha::Virtualshelves;
70 use base qw(Koha::Object);
72 use constant ADMINISTRATIVE_LOCKOUT => -1;
74 our $RESULTSET_PATRON_ID_MAPPING = {
75 Accountline => 'borrowernumber',
76 Aqbasketuser => 'borrowernumber',
77 Aqbudget => 'budget_owner_id',
78 Aqbudgetborrower => 'borrowernumber',
79 ArticleRequest => 'borrowernumber',
80 BorrowerDebarment => 'borrowernumber',
81 BorrowerFile => 'borrowernumber',
82 BorrowerModification => 'borrowernumber',
83 ClubEnrollment => 'borrowernumber',
84 Issue => 'borrowernumber',
85 ItemsLastBorrower => 'borrowernumber',
86 Linktracker => 'borrowernumber',
87 Message => 'borrowernumber',
88 MessageQueue => 'borrowernumber',
89 OldIssue => 'borrowernumber',
90 OldReserve => 'borrowernumber',
91 Rating => 'borrowernumber',
92 Reserve => 'borrowernumber',
93 Review => 'borrowernumber',
94 SearchHistory => 'userid',
95 Statistic => 'borrowernumber',
96 Suggestion => 'suggestedby',
97 TagAll => 'borrowernumber',
98 Virtualshelfcontent => 'borrowernumber',
99 Virtualshelfshare => 'borrowernumber',
100 Virtualshelve => 'owner',
105 Koha::Patron - Koha Patron Object class
116 my ( $class, $params ) = @_;
118 return $class->SUPER::new($params);
121 =head3 fixup_cardnumber
123 Autogenerate next cardnumber from highest value found in database
127 sub fixup_cardnumber {
130 my $max = $self->cardnumber;
131 Koha::Plugins->call( 'patron_barcode_transform', \$max );
133 $max ||= Koha::Patrons->search({
134 cardnumber => {-regexp => '^-?[0-9]+$'}
136 select => \'CAST(cardnumber AS SIGNED)',
137 as => ['cast_cardnumber']
138 })->_resultset->get_column('cast_cardnumber')->max;
139 $self->cardnumber(($max || 0) +1);
142 =head3 trim_whitespace
144 trim whitespace from data which has some non-whitespace in it.
145 Could be moved to Koha::Object if need to be reused
149 sub trim_whitespaces {
152 my $schema = Koha::Database->new->schema;
153 my @columns = $schema->source($self->_type)->columns;
155 for my $column( @columns ) {
156 my $value = $self->$column;
157 if ( defined $value ) {
158 $value =~ s/^\s*|\s*$//g;
159 $self->$column($value);
165 =head3 plain_text_password
167 $patron->plain_text_password( $password );
169 stores a copy of the unencrypted password in the object
170 for use in code before encrypting for db
174 sub plain_text_password {
175 my ( $self, $password ) = @_;
177 $self->{_plain_text_password} = $password;
180 return $self->{_plain_text_password}
181 if $self->{_plain_text_password};
188 Patron specific store method to cleanup record
189 and do other necessary things before saving
196 my $params = @_ ? shift : {};
198 my $guarantors = $params->{guarantors} // [];
200 $self->_result->result_source->schema->txn_do(
203 C4::Context->preference("autoMemberNum")
204 and ( not defined $self->cardnumber
205 or $self->cardnumber eq '' )
208 # Warning: The caller is responsible for locking the members table in write
209 # mode, to avoid database corruption.
210 # We are in a transaction but the table is not locked
211 $self->fixup_cardnumber;
214 unless ( $self->category->in_storage ) {
215 Koha::Exceptions::Object::FKConstraint->throw(
216 broken_fk => 'categorycode',
217 value => $self->categorycode,
221 $self->trim_whitespaces;
223 my $new_cardnumber = $self->cardnumber;
224 Koha::Plugins->call( 'patron_barcode_transform', \$new_cardnumber );
225 $self->cardnumber($new_cardnumber);
227 # Set surname to uppercase if uppercasesurname is true
228 $self->surname( uc( $self->surname ) )
229 if C4::Context->preference("uppercasesurnames");
231 $self->relationship(undef) # We do not want to store an empty string in this field
232 if defined $self->relationship
233 and $self->relationship eq "";
235 for my $note_field (qw( borrowernotes opacnote )) {
236 if ( !$self->in_storage || $self->_result->is_column_changed($note_field) ) {
237 $self->$note_field( C4::Scrubber->new('note')->scrub( $self->$note_field ) );
241 unless ( $self->in_storage ) { #AddMember
243 # Generate a valid userid/login if needed
244 $self->generate_userid unless $self->userid;
245 Koha::Exceptions::Patron::InvalidUserid->throw( userid => $self->userid )
246 unless $self->has_valid_userid;
248 # Add expiration date if it isn't already there
249 unless ( $self->dateexpiry ) {
250 $self->dateexpiry( $self->category->get_expiry_date );
253 # Add enrollment date if it isn't already there
254 unless ( $self->dateenrolled ) {
255 $self->dateenrolled(dt_from_string);
258 # Set the privacy depending on the patron's category
259 my $default_privacy = $self->category->default_privacy || q{};
261 $default_privacy eq 'default' ? 1
262 : $default_privacy eq 'never' ? 2
263 : $default_privacy eq 'forever' ? 0
265 $self->privacy($default_privacy);
267 # Call any check_password plugins if password is passed
268 if ( C4::Context->config("enable_plugins") && $self->password ) {
269 my @plugins = Koha::Plugins->new()->GetPlugins(
271 method => 'check_password',
274 foreach my $plugin (@plugins) {
276 # This plugin hook will also be used by a plugin for the Norwegian national
277 # patron database. This is why we need to pass both the password and the
278 # borrowernumber to the plugin.
279 my $ret = $plugin->check_password(
281 password => $self->password,
282 borrowernumber => $self->borrowernumber
285 if ( $ret->{'error'} == 1 ) {
286 Koha::Exceptions::Password::Plugin->throw();
291 # Make a copy of the plain text password for later use
292 $self->plain_text_password( $self->password );
294 $self->password_expiration_date(
296 ? $self->category->get_password_expiry_date || undef
300 # Create a disabled account if no password provided
303 ? Koha::AuthUtils::hash_password( $self->password )
307 $self->borrowernumber(undef);
309 if ( C4::Context->preference('ChildNeedsGuarantor')
310 and ( $self->is_child or $self->category->can_be_guarantee )
311 and $self->contactname eq ""
314 Koha::Exceptions::Patron::Relationship::NoGuarantor->throw();
317 foreach my $guarantor (@$guarantors) {
318 if ( $guarantor->is_child or $guarantor->category->can_be_guarantee ) {
319 Koha::Exceptions::Patron::Relationship::InvalidRelationship->throw( invalid_guarantor => 1 );
323 $self = $self->SUPER::store;
325 $self->add_enrolment_fee_if_needed(0);
327 logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
328 if C4::Context->preference("BorrowersLog");
331 my $self_from_storage = $self->get_from_storage;
333 # Do not accept invalid userid here
334 $self->generate_userid unless $self->userid;
335 Koha::Exceptions::Patron::InvalidUserid->throw( userid => $self->userid )
336 unless $self->has_valid_userid;
338 # If a borrower has set their privacy to never we should immediately anonymize
340 if ( $self->privacy() == 2 && $self_from_storage->privacy() != 2 ) {
342 $self->old_checkouts->anonymize;
344 Koha::Exceptions::Patron::FailedAnonymizing->throw( error => @_ );
348 # Password must be updated using $self->set_password
349 $self->password( $self_from_storage->password );
351 if ( $self->category->categorycode ne $self_from_storage->category->categorycode ) {
353 # Add enrolement fee on category change if required
354 $self->add_enrolment_fee_if_needed(1)
355 if C4::Context->preference('FeeOnChangePatronCategory');
357 # Clean up guarantors on category change if required
358 $self->guarantor_relationships->delete
359 unless ( $self->category->can_be_guarantee );
363 my @existing_guarantors = $self->guarantor_relationships()->guarantors->as_list;
364 push @$guarantors, @existing_guarantors;
366 if ( C4::Context->preference('ChildNeedsGuarantor')
367 and ( $self->is_child or $self->category->can_be_guarantee )
368 and $self->contactname eq ""
371 Koha::Exceptions::Patron::Relationship::NoGuarantor->throw();
374 foreach my $guarantor (@$guarantors) {
375 if ( $guarantor->is_child or $guarantor->category->can_be_guarantee ) {
376 Koha::Exceptions::Patron::Relationship::InvalidRelationship->throw( invalid_guarantor => 1 );
381 if ( C4::Context->preference("BorrowersLog") ) {
383 my $from_storage = $self_from_storage->unblessed;
384 my $from_object = $self->unblessed;
386 # Object's dateexpiry is a DateTime object which stringifies to iso8601 datetime,
387 # but the column in only a date so we need to convert the datetime to just a date
388 # to know if it has actually changed.
389 $from_object->{dateexpiry} = dt_from_string( $from_object->{dateexpiry} )->ymd
390 if $from_object->{dateexpiry};
392 my @skip_fields = (qw/lastseen updated_on/);
393 for my $key ( keys %{$from_storage} ) {
394 next if any { /$key/ } @skip_fields;
395 my $storage_value = $from_storage->{$key} // q{};
396 my $object_value = $from_object->{$key} // q{};
397 if ( ( $storage_value || $object_value )
398 && ( $storage_value ne $object_value ) )
401 before => $from_storage->{$key},
402 after => $from_object->{$key}
407 if ( defined($info) ) {
411 $self->borrowernumber,
414 { utf8 => 1, pretty => 1, canonical => 1 }
421 $self = $self->SUPER::store;
432 Delete patron's holds, lists and finally the patron.
434 Lists owned by the borrower are deleted or ownership is transferred depending on the
435 ListOwnershipUponPatronDeletion pref, but entries from the borrower to other lists are kept.
442 my $anonymous_patron = C4::Context->preference("AnonymousPatron");
443 Koha::Exceptions::Patron::FailedDeleteAnonymousPatron->throw() if $anonymous_patron && $self->id eq $anonymous_patron;
445 # Check if patron is protected
446 Koha::Exceptions::Patron::FailedDeleteProtectedPatron->throw() if $self->protected == 1;
448 $self->_result->result_source->schema->txn_do(
450 # Cancel Patron's holds
451 my $holds = $self->holds;
452 while( my $hold = $holds->next ){
456 # Handle lists (virtualshelves)
457 $self->virtualshelves->disown_or_delete;
459 # We cannot have a FK on borrower_modifications.borrowernumber, the table is also used
461 $_->delete for Koha::Patron::Modifications->search( { borrowernumber => $self->borrowernumber } )->as_list;
463 $self->SUPER::delete;
465 logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
473 my $patron_category = $patron->category
475 Return the patron category for this patron
481 return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
491 return Koha::Patron::Images->find( $self->borrowernumber );
496 Returns a Koha::Library object representing the patron's home library.
502 return Koha::Library->_new_from_dbic($self->_result->branchcode);
507 Returns a Koha::SMS::Provider object representing the patron's SMS provider.
513 my $sms_provider_rs = $self->_result->sms_provider;
514 return unless $sms_provider_rs;
515 return Koha::SMS::Provider->_new_from_dbic($sms_provider_rs);
518 =head3 guarantor_relationships
520 Returns Koha::Patron::Relationships object for this patron's guarantors
522 Returns the set of relationships for the patrons that are guarantors for this patron.
524 Note that a guarantor should exist as a patron in Koha; it was not possible
525 to add them without a guarantor_id in the interface for some time. Bug 30472
526 restricts it on db level.
530 sub guarantor_relationships {
533 return Koha::Patron::Relationships->search( { guarantee_id => $self->id } );
536 =head3 guarantee_relationships
538 Returns Koha::Patron::Relationships object for this patron's guarantors
540 Returns the set of relationships for the patrons that are guarantees for this patron.
542 The method returns Koha::Patron::Relationship objects for the sake
543 of consistency with the guantors method.
544 A guarantee by definition must exist as a patron in Koha.
548 sub guarantee_relationships {
551 return Koha::Patron::Relationships->search(
552 { guarantor_id => $self->id },
554 prefetch => 'guarantee',
555 order_by => { -asc => [ 'guarantee.surname', 'guarantee.firstname' ] },
560 =head3 relationships_debt
562 Returns the amount owed by the patron's guarantors *and* the other guarantees of those guarantors
566 sub relationships_debt {
567 my ($self, $params) = @_;
569 my $include_guarantors = $params->{include_guarantors};
570 my $only_this_guarantor = $params->{only_this_guarantor};
571 my $include_this_patron = $params->{include_this_patron};
574 if ( $only_this_guarantor ) {
575 @guarantors = $self->guarantee_relationships->count ? ( $self ) : ();
576 Koha::Exceptions::BadParameter->throw( { parameter => 'only_this_guarantor' } ) unless @guarantors;
577 } elsif ( $self->guarantor_relationships->count ) {
578 # I am a guarantee, just get all my guarantors
579 @guarantors = $self->guarantor_relationships->guarantors->as_list;
581 # I am a guarantor, I need to get all the guarantors of all my guarantees
582 @guarantors = map { $_->guarantor_relationships->guarantors->as_list } $self->guarantee_relationships->guarantees->as_list;
585 my $non_issues_charges = 0;
586 my $seen = $include_this_patron ? {} : { $self->id => 1 }; # For tracking members already added to the total
587 foreach my $guarantor (@guarantors) {
588 $non_issues_charges += $guarantor->account->non_issues_charges if $include_guarantors && !$seen->{ $guarantor->id };
590 # We've added what the guarantor owes, not added in that guarantor's guarantees as well
591 my @guarantees = map { $_->guarantee } $guarantor->guarantee_relationships->as_list;
592 my $guarantees_non_issues_charges = 0;
593 foreach my $guarantee (@guarantees) {
594 next if $seen->{ $guarantee->id };
595 $guarantees_non_issues_charges += $guarantee->account->non_issues_charges;
596 # Mark this guarantee as seen so we don't double count a guarantee linked to multiple guarantors
597 $seen->{ $guarantee->id } = 1;
600 $non_issues_charges += $guarantees_non_issues_charges;
601 $seen->{ $guarantor->id } = 1;
604 return $non_issues_charges;
607 =head3 housebound_profile
609 Returns the HouseboundProfile associated with this patron.
613 sub housebound_profile {
615 my $profile = $self->_result->housebound_profile;
616 return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
621 =head3 housebound_role
623 Returns the HouseboundRole associated with this patron.
627 sub housebound_role {
630 my $role = $self->_result->housebound_role;
631 return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
637 Returns the siblings of this patron.
644 my @guarantors = $self->guarantor_relationships()->guarantors()->as_list;
646 return unless @guarantors;
649 map { $_->guarantee_relationships()->guarantees()->as_list } @guarantors;
651 return unless @siblings;
655 grep { !$seen{ $_->id }++ && ( $_->id != $self->id ) } @siblings;
657 return Koha::Patrons->search( { borrowernumber => { -in => [ map { $_->id } @siblings ] } } );
662 my $patron = Koha::Patrons->find($id);
663 $patron->merge_with( \@patron_ids );
665 This subroutine merges a list of patrons into the patron record. This is accomplished by finding
666 all related patron ids for the patrons to be merged in other tables and changing the ids to be that
667 of the keeper patron.
672 my ( $self, $patron_ids ) = @_;
674 my $anonymous_patron = C4::Context->preference("AnonymousPatron");
675 return if $anonymous_patron && $self->id eq $anonymous_patron;
677 # Do not merge other patrons into a protected patron
678 return if $self->protected;
680 my @patron_ids = @{ $patron_ids };
682 # Ensure the keeper isn't in the list of patrons to merge
683 @patron_ids = grep { $_ ne $self->id } @patron_ids;
685 my $schema = Koha::Database->new()->schema();
689 $self->_result->result_source->schema->txn_do( sub {
690 foreach my $patron_id (@patron_ids) {
692 next if $patron_id eq $anonymous_patron;
694 my $patron = Koha::Patrons->find( $patron_id );
698 # Do not merge protected patrons into other patrons
699 next if $patron->protected;
701 # Unbless for safety, the patron will end up being deleted
702 $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
704 my $attributes = $patron->extended_attributes;
705 my $new_attributes = [
706 map { { code => $_->code, attribute => $_->attribute } }
709 $attributes->delete; # We need to delete before trying to merge them to prevent exception on unique and repeatable
710 for my $attribute ( @$new_attributes ) {
712 $self->add_extended_attribute($attribute);
714 # Don't block the merge if there is a non-repeatable attribute that cannot be added to the current patron.
715 unless ( $_->isa('Koha::Exceptions::Patron::Attribute::NonRepeatable') ) {
721 while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
722 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
723 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
724 $rs->update({ $field => $self->id });
725 if ( $r eq 'BorrowerDebarment' ) {
726 Koha::Patron::Debarments::UpdateBorrowerDebarmentFlags($self->id);
730 $patron->move_to_deleted();
739 =head3 messaging_preferences
741 my $patron = Koha::Patrons->find($id);
742 $patron->messaging_preferences();
746 sub messaging_preferences {
749 return Koha::Patron::MessagePreferences->search({
750 borrowernumber => $self->borrowernumber,
754 =head3 wants_check_for_previous_checkout
756 $wants_check = $patron->wants_check_for_previous_checkout;
758 Return 1 if Koha needs to perform PrevIssue checking, else 0.
762 sub wants_check_for_previous_checkout {
764 my $syspref = C4::Context->preference("checkPrevCheckout");
767 ## Hard syspref trumps all
768 return 1 if ($syspref eq 'hardyes');
769 return 0 if ($syspref eq 'hardno');
770 ## Now, patron pref trumps all
771 return 1 if ($self->checkprevcheckout eq 'yes');
772 return 0 if ($self->checkprevcheckout eq 'no');
774 # More complex: patron inherits -> determine category preference
775 my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
776 return 1 if ($checkPrevCheckoutByCat eq 'yes');
777 return 0 if ($checkPrevCheckoutByCat eq 'no');
779 # Finally: category preference is inherit, default to 0
780 if ($syspref eq 'softyes') {
787 =head3 do_check_for_previous_checkout
789 $do_check = $patron->do_check_for_previous_checkout($item);
791 Return 1 if the bib associated with $ITEM has previously been checked out to
792 $PATRON, 0 otherwise.
796 sub do_check_for_previous_checkout {
797 my ( $self, $item ) = @_;
800 my $biblio = Koha::Biblios->find( $item->{biblionumber} );
801 if ( $biblio->is_serial ) {
802 push @item_nos, $item->{itemnumber};
804 # Get all itemnumbers for given bibliographic record.
805 @item_nos = $biblio->items->get_column( 'itemnumber' );
808 # Create (old)issues search criteria
810 borrowernumber => $self->borrowernumber,
811 itemnumber => \@item_nos,
814 my $delay = C4::Context->preference('CheckPrevCheckoutDelay') || 0;
816 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
817 my $newer_than = dt_from_string()->subtract( days => $delay );
818 $criteria->{'returndate'} = { '>' => $dtf->format_datetime($newer_than), };
821 # Check current issues table
822 my $issues = Koha::Checkouts->search($criteria);
823 return 1 if $issues->count; # 0 || N
825 # Check old issues table
826 my $old_issues = Koha::Old::Checkouts->search($criteria);
827 return $old_issues->count; # 0 || N
832 my $debarment_expiration = $patron->is_debarred;
834 Returns the date a patron debarment will expire, or undef if the patron is not
842 return unless $self->debarred;
843 return $self->debarred
844 if $self->debarred =~ '^9999'
845 or dt_from_string( $self->debarred ) > dt_from_string;
851 my $is_expired = $patron->is_expired;
853 Returns 1 if the patron is expired or 0;
859 return 0 unless $self->dateexpiry;
860 return 0 if $self->dateexpiry =~ '^9999';
861 return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
867 $patron->is_active({ [ since => $date ], [ days|weeks|months|years => $value ] })
869 A patron is considered 'active' if the following conditions hold:
871 - account did not expire
872 - account has not been anonymized
873 - enrollment or lastseen within period specified
875 Note: lastseen is updated for triggers defined in preference
876 TrackLastPatronActivityTriggers. This includes logins, issues, holds, etc.
878 The period to check is defined by $date or $value in days, weeks or months. You should
879 pass one of those; otherwise an exception is thrown.
884 my ( $self, $params ) = @_;
885 return 0 if $self->is_expired or $self->anonymized;
888 if ( $params->{since} ) {
889 $dt = dt_from_string( $params->{since}, 'iso' );
890 } elsif ( grep { $params->{$_} } qw(days weeks months years) ) {
891 $dt = dt_from_string();
892 foreach my $duration (qw(days weeks months years)) {
893 $dt = $dt->subtract( $duration => $params->{$duration} ) if $params->{$duration};
896 Koha::Exceptions::MissingParameter->throw('is_active needs date or period');
899 # Enrollment within this period?
900 return 1 if DateTime->compare( dt_from_string( $self->dateenrolled ), $dt ) > -1;
902 # We look at lastseen regardless of TrackLastPatronActivityTriggers. If lastseen is set
903 # recently, the triggers may have been removed after that, etc.
904 return 1 if $self->lastseen && DateTime->compare( dt_from_string( $self->lastseen ), $dt ) > -1;
909 =head3 password_expired
911 my $password_expired = $patron->password_expired;
913 Returns 1 if the patron's password is expired or 0;
917 sub password_expired {
919 return 0 unless $self->password_expiration_date;
920 return 1 if dt_from_string( $self->password_expiration_date ) <= dt_from_string->truncate( to => 'day' );
924 =head3 is_going_to_expire
926 my $is_going_to_expire = $patron->is_going_to_expire;
928 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
932 sub is_going_to_expire {
935 my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
937 return 0 unless $delay;
938 return 0 unless $self->dateexpiry;
939 return 0 if $self->dateexpiry =~ '^9999';
940 return 1 if dt_from_string( $self->dateexpiry, undef, 'floating' )->subtract( days => $delay ) < dt_from_string(undef, undef, 'floating')->truncate( to => 'day' );
946 $patron->set_password({ password => $plain_text_password [, skip_validation => 1, action => NAME ] });
948 Set the patron's password.
950 Allows optional action parameter to change name of action logged (when enabled). Used for reset password.
954 The passed string is validated against the current password enforcement policy.
955 Validation can be skipped by passing the I<skip_validation> parameter.
957 Exceptions are thrown if the password is not good enough.
961 =item Koha::Exceptions::Password::TooShort
963 =item Koha::Exceptions::Password::WhitespaceCharacters
965 =item Koha::Exceptions::Password::TooWeak
967 =item Koha::Exceptions::Password::Plugin (if a "check password" plugin is enabled)
974 my ( $self, $args ) = @_;
976 my $password = $args->{password};
977 my $action = $args->{action} || "CHANGE PASS";
979 unless ( $args->{skip_validation} ) {
980 my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password, $self->category );
983 if ( $error eq 'too_short' ) {
984 my $min_length = $self->category->effective_min_password_length;
985 $min_length = 3 if not $min_length or $min_length < 3;
987 my $password_length = length($password);
988 Koha::Exceptions::Password::TooShort->throw(
989 length => $password_length, min_length => $min_length );
991 elsif ( $error eq 'has_whitespaces' ) {
992 Koha::Exceptions::Password::WhitespaceCharacters->throw();
994 elsif ( $error eq 'too_weak' ) {
995 Koha::Exceptions::Password::TooWeak->throw();
1000 if ( C4::Context->config("enable_plugins") ) {
1001 # Call any check_password plugins
1002 my @plugins = Koha::Plugins->new()->GetPlugins({
1003 method => 'check_password',
1005 foreach my $plugin ( @plugins ) {
1006 # This plugin hook will also be used by a plugin for the Norwegian national
1007 # patron database. This is why we need to pass both the password and the
1008 # borrowernumber to the plugin.
1009 my $ret = $plugin->check_password(
1011 password => $password,
1012 borrowernumber => $self->borrowernumber
1015 # This plugin hook will also be used by a plugin for the Norwegian national
1016 # patron database. This is why we need to call the actual plugins and then
1017 # check skip_validation afterwards.
1018 if ( $ret->{'error'} == 1 && !$args->{skip_validation} ) {
1019 Koha::Exceptions::Password::Plugin->throw();
1024 if ( C4::Context->preference('NotifyPasswordChange') ) {
1025 my $self_from_storage = $self->get_from_storage;
1026 if ( !C4::Auth::checkpw_hash( $password, $self_from_storage->password ) ) {
1027 my $emailaddr = $self_from_storage->notice_email_address;
1029 # if we manage to find a valid email address, send notice
1031 my $letter = C4::Letters::GetPreparedLetter(
1032 module => 'members',
1033 letter_code => 'PASSWORD_CHANGE',
1034 branchcode => $self_from_storage->branchcode,
1036 lang => $self_from_storage->lang || 'default',
1038 'branches' => $self_from_storage->branchcode,
1039 'borrowers' => $self_from_storage->borrowernumber,
1041 want_librarian => 1,
1044 my $message_id = C4::Letters::EnqueueLetter(
1047 borrowernumber => $self_from_storage->id,
1048 to_address => $emailaddr,
1049 message_transport_type => 'email'
1052 C4::Letters::SendQueuedMessages( { message_id => $message_id } ) if $message_id;
1057 my $digest = Koha::AuthUtils::hash_password($password);
1059 $self->password_expiration_date( $self->category->get_password_expiry_date || undef );
1061 # We do not want to call $self->store and retrieve password from DB
1062 $self->password($digest);
1063 $self->login_attempts(0);
1064 $self->SUPER::store;
1066 logaction( "MEMBERS", $action, $self->borrowernumber, "" )
1067 if C4::Context->preference("BorrowersLog");
1073 =head3 renew_account
1075 my $new_expiry_date = $patron->renew_account
1077 Extending the subscription to the expiry date.
1084 if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
1085 $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
1088 C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
1089 ? dt_from_string( $self->dateexpiry )
1092 my $expiry_date = $self->category->get_expiry_date($date);
1094 $self->dateexpiry($expiry_date);
1095 $self->date_renewed( dt_from_string() );
1098 $self->add_enrolment_fee_if_needed(1);
1100 logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
1101 return dt_from_string( $expiry_date )->truncate( to => 'day' );
1106 my $has_overdues = $patron->has_overdues;
1108 Returns the number of patron's overdues
1114 my $date = dt_from_string();
1115 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
1116 return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime($date) } })->count;
1121 =head3 has_restricting_overdues
1123 my $has_restricting_overdues = $patron->has_restricting_overdues({ issue_branchcode => $branchcode });
1125 Returns true if patron has overdues that would result in debarment.
1129 sub has_restricting_overdues {
1130 my ( $self, $params ) = @_;
1132 my $date = dt_from_string()->truncate( to => 'day' );
1134 # If ignoring unrestricted overdues, calculate which delay value for
1135 # overdue messages is set with restrictions. Then only include overdue
1136 # issues older than that date when counting.
1137 #TODO: bail out/throw exception if $params->{issue_branchcode} not set?
1138 my $debarred_delay = _get_overdue_debarred_delay( $params->{issue_branchcode}, $self->categorycode() );
1139 return 0 unless defined $debarred_delay;
1141 # Emulate the conditions in overdue_notices.pl.
1142 # The overdue_notices-script effectively truncates both issues.date_due and current date
1143 # to days when selecting overdue issues.
1144 # Hours and minutes for issues.date_due is usually set to 23 and 59 respectively, though can theoretically
1145 # be set to any other value (truncated to minutes, except if CalcDateDue gets a $startdate)
1147 # No matter what time of day date_due is set to, overdue_notices.pl will select all issues that are due
1148 # the current date or later. We can emulate this query by instead of truncating both to days in the SQL-query,
1149 # using the condition that date_due must be less then the current date truncated to days (time set to 00:00:00)
1150 # offset by one day in the future.
1152 $date->add( days => 1 );
1155 if ( C4::Context->preference('OverdueNoticeCalendar') ) {
1156 $calendar = Koha::Calendar->new( branchcode => $params->{issue_branchcode} );
1159 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
1160 my $issues = $self->_result->issues->search( { date_due => { '<' => $dtf->format_datetime($date) } } );
1161 my $now = dt_from_string();
1163 while ( my $issue = $issues->next ) {
1165 C4::Context->preference('OverdueNoticeCalendar')
1166 ? $calendar->days_between( dt_from_string( $issue->date_due ), $now )->in_units('days')
1167 : $now->delta_days( dt_from_string( $issue->date_due ) )->in_units('days');
1168 if ( $days_between >= $debarred_delay ) {
1175 # Fetch first delayX value from overduerules where debarredX is set, or 0 for no delay
1176 sub _get_overdue_debarred_delay {
1177 my ( $branchcode, $categorycode ) = @_;
1178 my $dbh = C4::Context->dbh();
1180 # We get default rules if there is no rule for this branch
1181 my $rule = Koha::OverdueRules->find(
1183 branchcode => $branchcode,
1184 categorycode => $categorycode
1187 || Koha::OverdueRules->find(
1190 categorycode => $categorycode
1195 return $rule->delay1 if $rule->debarred1;
1196 return $rule->delay2 if $rule->debarred2;
1197 return $rule->delay3 if $rule->debarred3;
1201 =head3 update_lastseen
1203 $patron->update_lastseen('activity');
1205 Updates the lastseen field, limited to one update per day, whenever the activity passed is
1206 listed in TrackLastPatronActivityTriggers.
1208 The method should be called upon successful completion of the activity.
1212 sub update_lastseen {
1213 my ( $self, $activity ) = @_;
1214 my $tracked_activities = {
1215 map { ( lc $_, 1 ); } split /\s*\,\s*/,
1216 C4::Context->preference('TrackLastPatronActivityTriggers')
1218 return $self unless $tracked_activities->{$activity};
1220 my $cache = Koha::Caches->get_instance();
1221 my $cache_key = "track_activity_" . $self->borrowernumber;
1222 my $cached = $cache->get_from_cache($cache_key);
1223 my $now = dt_from_string();
1224 return $self if $cached && $cached eq $now->ymd;
1226 $self->lastseen($now)->store;
1227 $cache->set_in_cache( $cache_key, $now->ymd );
1231 =head3 move_to_deleted
1233 my $is_moved = $patron->move_to_deleted;
1235 Move a patron to the deletedborrowers table.
1236 This can be done before deleting a patron, to make sure the data are not completely deleted.
1240 sub move_to_deleted {
1242 my $patron_infos = $self->unblessed;
1243 delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
1244 return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
1247 =head3 can_request_article
1249 if ( $patron->can_request_article( $library->id ) ) { ... }
1251 Returns true if the patron can request articles. As limits apply for the patron
1252 on the same day, those completed the same day are considered as current.
1254 A I<library_id> can be passed as parameter, falling back to userenv if absent.
1258 sub can_request_article {
1259 my ($self, $library_id) = @_;
1261 $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1263 my $rule = Koha::CirculationRules->get_effective_rule(
1265 branchcode => $library_id,
1266 categorycode => $self->categorycode,
1267 rule_name => 'open_article_requests_limit'
1271 my $limit = ($rule) ? $rule->rule_value : undef;
1273 return 1 unless defined $limit;
1275 my $count = Koha::ArticleRequests->search(
1276 [ { borrowernumber => $self->borrowernumber, status => [ 'REQUESTED', 'PENDING', 'PROCESSING' ] },
1277 { borrowernumber => $self->borrowernumber, status => 'COMPLETED', updated_on => { '>=' => \'CAST(NOW() AS DATE)' } },
1280 return $count < $limit ? 1 : 0;
1283 =head3 article_request_fee
1285 my $fee = $patron->article_request_fee(
1287 [ library_id => $library->id, ]
1291 Returns the fee to be charged to the patron when it places an article request.
1293 A I<library_id> can be passed as parameter, falling back to userenv if absent.
1297 sub article_request_fee {
1298 my ($self, $params) = @_;
1300 my $library_id = $params->{library_id};
1302 $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1304 my $rule = Koha::CirculationRules->get_effective_rule(
1306 branchcode => $library_id,
1307 categorycode => $self->categorycode,
1308 rule_name => 'article_request_fee'
1312 my $fee = ($rule) ? $rule->rule_value + 0 : 0;
1317 =head3 add_article_request_fee_if_needed
1319 my $fee = $patron->add_article_request_fee_if_needed(
1321 [ item_id => $item->id,
1322 library_id => $library->id, ]
1326 If an article request fee needs to be charged, it adds a debit to the patron's
1329 Returns the fee line.
1331 A I<library_id> can be passed as parameter, falling back to userenv if absent.
1335 sub add_article_request_fee_if_needed {
1336 my ($self, $params) = @_;
1338 my $library_id = $params->{library_id};
1339 my $item_id = $params->{item_id};
1341 $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1343 my $amount = $self->article_request_fee(
1345 library_id => $library_id,
1351 if ( $amount > 0 ) {
1352 $debit_line = $self->account->add_debit(
1355 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
1356 interface => C4::Context->interface,
1357 library_id => $library_id,
1358 type => 'ARTICLE_REQUEST',
1359 item_id => $item_id,
1367 =head3 article_requests
1369 my $article_requests = $patron->article_requests;
1371 Returns the patron article requests.
1375 sub article_requests {
1378 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
1381 =head3 add_enrolment_fee_if_needed
1383 my $enrolment_fee = $patron->add_enrolment_fee_if_needed($renewal);
1385 Add enrolment fee for a patron if needed.
1387 $renewal - boolean denoting whether this is an account renewal or not
1391 sub add_enrolment_fee_if_needed {
1392 my ($self, $renewal) = @_;
1393 my $enrolment_fee = $self->category->enrolmentfee;
1394 if ( $enrolment_fee && $enrolment_fee > 0 ) {
1395 my $type = $renewal ? 'ACCOUNT_RENEW' : 'ACCOUNT';
1396 $self->account->add_debit(
1398 amount => $enrolment_fee,
1399 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
1400 interface => C4::Context->interface,
1401 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
1406 return $enrolment_fee || 0;
1411 my $checkouts = $patron->checkouts
1417 my $checkouts = $self->_result->issues;
1418 return Koha::Checkouts->_new_from_dbic( $checkouts );
1421 =head3 pending_checkouts
1423 my $pending_checkouts = $patron->pending_checkouts
1425 This method will return the same as $self->checkouts, but with a prefetch on
1426 items, biblio and biblioitems.
1428 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
1430 It should not be used directly, prefer to access fields you need instead of
1431 retrieving all these fields in one go.
1435 sub pending_checkouts {
1437 my $checkouts = $self->_result->issues->search(
1441 { -desc => 'me.timestamp' },
1442 { -desc => 'issuedate' },
1443 { -desc => 'issue_id' }, # Sort by issue_id should be enough
1445 prefetch => { item => { biblio => 'biblioitems' } },
1448 return Koha::Checkouts->_new_from_dbic( $checkouts );
1451 =head3 old_checkouts
1453 my $old_checkouts = $patron->old_checkouts
1459 my $old_checkouts = $self->_result->old_issues;
1460 return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
1465 my $overdue_items = $patron->overdues
1467 Return the overdue items
1473 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
1474 return $self->checkouts->search(
1476 'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
1479 prefetch => { item => { biblio => 'biblioitems' } },
1487 my $restrictions = $patron->restrictions;
1489 Returns the patron restrictions.
1495 my $restrictions_rs = $self->_result->restrictions;
1496 return Koha::Patron::Restrictions->_new_from_dbic($restrictions_rs);
1499 =head3 get_routing_lists
1501 my $routinglists = $patron->get_routing_lists
1503 Returns the routing lists a patron is subscribed to.
1507 sub get_routing_lists {
1509 my $routing_list_rs = $self->_result->subscriptionroutinglists;
1510 return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
1515 my $age = $patron->get_age
1517 Return the age of the patron
1524 return unless $self->dateofbirth;
1526 #Set timezone to floating to avoid any datetime math issues caused by DST
1527 my $date_of_birth = dt_from_string( $self->dateofbirth, undef, 'floating' );
1528 my $today = dt_from_string(undef, undef, 'floating')->truncate( to => 'day' );
1530 return $today->subtract_datetime( $date_of_birth )->years;
1535 my $is_valid = $patron->is_valid_age
1537 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
1543 my $age = $self->get_age;
1545 my $patroncategory = $self->category;
1546 my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
1548 return (defined($age) && (($high && ($age > $high)) or ($low && ($age < $low)))) ? 0 : 1;
1553 my $account = $patron->account
1559 return Koha::Account->new( { patron_id => $self->borrowernumber } );
1564 my $holds = $patron->holds
1566 Return all the holds placed by this patron
1572 my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1573 return Koha::Holds->_new_from_dbic($holds_rs);
1578 my $old_holds = $patron->old_holds
1580 Return all the historical holds for this patron
1586 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1587 return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1590 =head3 curbside_pickups
1592 my $curbside_pickups = $patron->curbside_pickups;
1594 Return all the curbside pickups for this patron
1598 sub curbside_pickups {
1600 my $curbside_pickups_rs = $self->_result->curbside_pickups_borrowernumbers->search;
1601 return Koha::CurbsidePickups->_new_from_dbic($curbside_pickups_rs);
1604 =head3 return_claims
1606 my $return_claims = $patron->return_claims
1612 my $return_claims = $self->_result->return_claims_borrowernumbers;
1613 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $return_claims );
1616 =head3 notice_email_address
1618 my $email = $patron->notice_email_address;
1620 Return the email address of patron used for notices.
1621 Returns the empty string if no email address.
1625 sub notice_email_address{
1628 my $which_address = C4::Context->preference("EmailFieldPrimary");
1629 # if syspref is set to 'first valid' (value == OFF), look up email address
1630 if ( $which_address eq 'OFF' ) {
1631 return $self->first_valid_email_address;
1634 return $self->$which_address || '';
1637 =head3 first_valid_email_address
1639 my $first_valid_email_address = $patron->first_valid_email_address
1641 Return the first valid email address for a patron.
1642 For now, the order is defined as email, emailpro, B_email.
1643 Returns the empty string if the borrower has no email addresses.
1647 sub first_valid_email_address {
1652 my @fields = split /\s*\|\s*/,
1653 C4::Context->preference('EmailFieldPrecedence');
1654 for my $field (@fields) {
1655 $email = $self->$field;
1662 =head3 get_club_enrollments
1666 sub get_club_enrollments {
1669 return Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1672 =head3 get_enrollable_clubs
1676 sub get_enrollable_clubs {
1677 my ( $self, $is_enrollable_from_opac ) = @_;
1680 $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1681 if $is_enrollable_from_opac;
1682 $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1684 $params->{borrower} = $self;
1686 return Koha::Clubs->get_enrollable($params);
1690 =head3 get_lists_with_patron
1692 my @lists = $patron->get_lists_with_patron;
1694 FIXME: This method returns a DBIC resultset instead of a Koha::Objects-based
1699 sub get_lists_with_patron {
1701 my $borrowernumber = $self->borrowernumber;
1703 return Koha::Database->new()->schema()->resultset('PatronList')->search(
1705 'patron_list_patrons.borrowernumber' => $borrowernumber,
1708 join => 'patron_list_patrons',
1715 =head3 account_locked
1717 my $is_locked = $patron->account_locked
1719 Return true if the patron has reached the maximum number of login attempts
1720 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1721 as an administrative lockout (independent of FailedLoginAttempts; see also
1722 Koha::Patron->lock).
1723 Otherwise return false.
1724 If the pref is not set (empty string, null or 0), the feature is considered as
1729 sub account_locked {
1731 my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1732 return 1 if $FailedLoginAttempts
1733 and $self->login_attempts
1734 and $self->login_attempts >= $FailedLoginAttempts;
1735 return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1739 =head3 can_see_patron_infos
1741 my $can_see = $patron->can_see_patron_infos( $patron );
1743 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1747 sub can_see_patron_infos {
1748 my ( $self, $patron ) = @_;
1749 return unless $patron;
1750 return $self->can_see_patrons_from( $patron->branchcode );
1753 =head3 can_see_patrons_from
1755 my $can_see = $patron->can_see_patrons_from( $branchcode );
1757 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1761 sub can_see_patrons_from {
1762 my ( $self, $branchcode ) = @_;
1764 return $self->can_see_things_from(
1766 branchcode => $branchcode,
1767 permission => 'borrowers',
1768 subpermission => 'view_borrower_infos_from_any_libraries',
1773 =head3 can_edit_items_from
1775 my $can_edit = $patron->can_edit_items_from( $branchcode );
1777 Return true if the I<Koha::Patron> can edit items from the given branchcode
1781 sub can_edit_items_from {
1782 my ( $self, $branchcode ) = @_;
1784 return 1 if C4::Context->IsSuperLibrarian();
1786 my $userenv = C4::Context->userenv();
1787 if ( $userenv && C4::Context->preference('IndependentBranches') ) {
1788 return $userenv->{branch} eq $branchcode;
1791 return $self->can_see_things_from(
1793 branchcode => $branchcode,
1794 permission => 'editcatalogue',
1795 subpermission => 'edit_any_item',
1800 =head3 libraries_where_can_edit_items
1802 my $libraries = $patron->libraries_where_can_edit_items;
1804 Return the list of branchcodes(!) of libraries the patron is allowed to items for.
1805 The branchcodes are arbitrarily returned sorted.
1806 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1808 An empty array means no restriction, the user can edit any item.
1812 sub libraries_where_can_edit_items {
1815 return $self->libraries_where_can_see_things(
1817 permission => 'editcatalogue',
1818 subpermission => 'edit_any_item',
1819 group_feature => 'ft_limit_item_editing',
1824 =head3 libraries_where_can_see_patrons
1826 my $libraries = $patron->libraries_where_can_see_patrons;
1828 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1829 The branchcodes are arbitrarily returned sorted.
1830 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1832 An empty array means no restriction, the patron can see patron's infos from any libraries.
1836 sub libraries_where_can_see_patrons {
1839 return $self->libraries_where_can_see_things(
1841 permission => 'borrowers',
1842 subpermission => 'view_borrower_infos_from_any_libraries',
1843 group_feature => 'ft_hide_patron_info',
1848 =head3 can_see_things_from
1850 my $can_see = $patron->can_see_things_from( $branchcode );
1852 Return true if the I<Koha::Patron> can perform some action on the given thing
1856 sub can_see_things_from {
1857 my ( $self, $params ) = @_;
1859 my $branchcode = $params->{branchcode};
1860 my $permission = $params->{permission};
1861 my $subpermission = $params->{subpermission};
1863 return 1 if C4::Context->IsSuperLibrarian();
1866 if ( $self->branchcode eq $branchcode ) {
1868 } elsif ( $self->has_permission( { $permission => $subpermission } ) ) {
1870 } elsif ( my @branches = $self->libraries_where_can_see_patrons ) {
1871 $can = ( any { $_ eq $branchcode } @branches ) ? 1 : 0;
1878 my $can_log_into = $patron->can_log_into( $library );
1880 Given a I<Koha::Library> object, it returns a boolean representing
1881 the fact the patron can log into a the library.
1886 my ( $self, $library ) = @_;
1890 if ( C4::Context->preference('IndependentBranches') ) {
1892 if $self->is_superlibrarian
1893 or $self->branchcode eq $library->id;
1903 =head3 libraries_where_can_see_things
1905 my $libraries = $patron->libraries_where_can_see_things;
1907 Returns a list of libraries where an aribitarary action is allowed to be taken by the logged in librarian
1908 against an object based on some branchcode related to the object ( patron branchcode, item homebranch, etc ).
1910 We are supposing here that the object is related to the logged in librarian (use of C4::Context::only_my_library)
1912 An empty array means no restriction, the thing can see thing's infos from any libraries.
1916 sub libraries_where_can_see_things {
1917 my ( $self, $params ) = @_;
1918 my $permission = $params->{permission};
1919 my $subpermission = $params->{subpermission};
1920 my $group_feature = $params->{group_feature};
1922 return $self->{"_restricted_branchcodes:$permission:$subpermission:$group_feature"}
1923 if exists( $self->{"_restricted_branchcodes:$permission:$subpermission:$group_feature"} );
1925 my $userenv = C4::Context->userenv;
1927 return () unless $userenv; # For tests, but userenv should be defined in tests...
1929 my @restricted_branchcodes;
1930 if (C4::Context::only_my_library) {
1931 push @restricted_branchcodes, $self->branchcode;
1935 $self->has_permission(
1936 { $permission => $subpermission }
1940 my $library_groups = $self->library->library_groups({ $group_feature => 1 });
1941 if ( $library_groups->count )
1943 while ( my $library_group = $library_groups->next ) {
1944 my $parent = $library_group->parent;
1945 if ( $parent->has_child( $self->branchcode ) ) {
1946 push @restricted_branchcodes, $parent->children->get_column('branchcode');
1951 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1955 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1956 @restricted_branchcodes = uniq(@restricted_branchcodes);
1957 @restricted_branchcodes = sort(@restricted_branchcodes);
1959 $self->{"_restricted_branchcodes:$permission:$subpermission:$group_feature"} = \@restricted_branchcodes;
1960 return @{ $self->{"_restricted_branchcodes:$permission:$subpermission:$group_feature"} };
1963 =head3 has_permission
1965 my $permission = $patron->has_permission($required);
1967 See C4::Auth::haspermission for details of syntax for $required
1971 sub has_permission {
1972 my ( $self, $flagsrequired ) = @_;
1973 return unless $self->userid;
1974 # TODO code from haspermission needs to be moved here!
1975 return C4::Auth::haspermission( $self->userid, $flagsrequired );
1978 =head3 is_superlibrarian
1980 my $is_superlibrarian = $patron->is_superlibrarian;
1982 Return true if the patron is a superlibrarian.
1986 sub is_superlibrarian {
1988 return $self->has_permission( { superlibrarian => 1 } ) ? 1 : 0;
1993 my $is_adult = $patron->is_adult
1995 Return true if the patron has a category with a type Adult (A) or Organization (I)
2001 return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
2006 my $is_child = $patron->is_child
2008 Return true if the patron has a category with a type Child (C)
2014 return $self->category->category_type eq 'C' ? 1 : 0;
2017 =head3 has_valid_userid
2019 my $patron = Koha::Patrons->find(42);
2020 $patron->userid( $new_userid );
2021 my $has_a_valid_userid = $patron->has_valid_userid
2023 my $patron = Koha::Patron->new( $params );
2024 my $has_a_valid_userid = $patron->has_valid_userid
2026 Return true if the current userid of this patron is valid/unique, otherwise false.
2028 Note that this should be done in $self->store instead and raise an exception if needed.
2032 sub has_valid_userid {
2035 return 0 unless $self->userid;
2037 return 0 if ( $self->userid eq C4::Context->config('user') ); # DB user
2039 my $already_exists = Koha::Patrons->search(
2041 userid => $self->userid,
2044 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
2049 return $already_exists ? 0 : 1;
2052 =head3 generate_userid
2054 $patron->generate_userid;
2056 If you do not have a plugin for generating a userid, we will call
2057 the internal method here that returns firstname.surname[.number],
2058 where number is an optional suffix to make the userid unique.
2059 (Its behavior has not been changed on bug 32426.)
2061 If you have plugin(s), the first valid response will be used.
2062 A plugin is assumed to return a valid userid as suggestion, but not
2063 assumed to save it already.
2064 Does not fallback to internal (you could arrange for that in your plugin).
2065 Clears userid when there are no valid plugin responses.
2069 sub generate_userid {
2071 my @responses = Koha::Plugins->call(
2072 'patron_generate_userid', { patron => $self },
2074 unless( @responses ) {
2075 # Empty list only possible when there are NO enabled plugins for this method.
2076 # In that case we provide internal response.
2077 return $self->_generate_userid_internal;
2079 # If a plugin returned false value or invalid value, we do however not return
2080 # internal response. The plugins should deal with that themselves. So we prevent
2081 # unexpected/unwelcome internal codes for plugin failures.
2082 foreach my $response ( grep { $_ } @responses ) {
2083 $self->userid( $response );
2084 return $self if $self->has_valid_userid;
2086 $self->userid(undef);
2090 sub _generate_userid_internal { # as we always did
2093 my $firstname = $self->firstname // q{};
2094 my $surname = $self->surname // q{};
2095 #The script will "do" the following code and increment the $offset until the generated userid is unique
2097 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
2098 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
2099 my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
2100 $userid = NFKD( $userid );
2101 $userid =~ s/\p{NonspacingMark}//g;
2102 $userid .= $offset unless $offset == 0;
2103 $self->userid( $userid );
2105 } while (! $self->has_valid_userid );
2110 =head3 add_extended_attribute
2114 sub add_extended_attribute {
2115 my ($self, $attribute) = @_;
2117 return Koha::Patron::Attribute->new(
2120 ( borrowernumber => $self->borrowernumber ),
2126 =head3 extended_attributes
2128 Return object of Koha::Patron::Attributes type with all attributes set for this patron
2134 sub extended_attributes {
2135 my ( $self, $attributes ) = @_;
2136 if ($attributes) { # setter
2137 my $schema = $self->_result->result_source->schema;
2140 # Remove the existing one
2141 $self->extended_attributes->filter_by_branch_limitations->delete;
2143 # Insert the new ones
2145 for my $attribute (@$attributes) {
2146 $self->add_extended_attribute($attribute);
2147 $new_types->{$attribute->{code}} = 1;
2150 # Check globally mandatory types
2151 my @required_attribute_types =
2152 Koha::Patron::Attribute::Types->search(
2155 category_code => [ undef, $self->categorycode ],
2156 'borrower_attribute_types_branches.b_branchcode' =>
2159 { join => 'borrower_attribute_types_branches' }
2160 )->get_column('code');
2161 for my $type ( @required_attribute_types ) {
2162 Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute->throw(
2164 ) if !$new_types->{$type};
2170 my $rs = $self->_result->borrower_attributes;
2171 # We call search to use the filters in Koha::Patron::Attributes->search
2172 return Koha::Patron::Attributes->_new_from_dbic($rs)->search;
2177 my $messages = $patron->messages;
2179 Return the message attached to the patron.
2185 my $messages_rs = $self->_result->messages_borrowernumbers->search;
2186 return Koha::Patron::Messages->_new_from_dbic($messages_rs);
2191 Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
2193 Lock and optionally expire a patron account.
2194 Remove holds and article requests if remove flag set.
2195 In order to distinguish from locking by entering a wrong password, let's
2196 call this an administrative lockout.
2201 my ( $self, $params ) = @_;
2202 $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
2203 if( $params->{expire} ) {
2204 $self->dateexpiry( dt_from_string->subtract(days => 1) );
2207 if( $params->{remove} ) {
2208 $self->holds->delete;
2209 $self->article_requests->delete;
2216 Koha::Patrons->find($id)->anonymize;
2218 Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
2219 are randomized, other personal data is cleared too.
2220 Patrons with issues are skipped.
2226 if( $self->_result->issues->count ) {
2227 warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
2230 # Mandatory fields come from the corresponding pref, but email fields
2231 # are removed since scrambled email addresses only generate errors
2232 my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
2233 split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
2234 $mandatory->{userid} = 1; # needed since sub store does not clear field
2235 my @columns = $self->_result->result_source->columns;
2236 @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized|auth_method/ } @columns;
2237 push @columns, 'dateofbirth'; # add this date back in
2238 foreach my $col (@columns) {
2239 $self->_anonymize_column($col, $mandatory->{lc $col} );
2241 $self->anonymized(1)->store;
2244 sub _anonymize_column {
2245 my ( $self, $col, $mandatory ) = @_;
2246 my $col_info = $self->_result->result_source->column_info($col);
2247 my $type = $col_info->{data_type};
2248 my $nullable = $col_info->{is_nullable};
2250 if( $type =~ /char|text/ ) {
2252 ? Koha::Token->new->generate({ pattern => '\w{10}' })
2256 } elsif( $type =~ /integer|int$|float|dec|double/ ) {
2257 $val = $nullable ? undef : 0;
2258 } elsif( $type =~ /date|time/ ) {
2259 $val = $nullable ? undef : dt_from_string;
2264 =head3 add_guarantor
2266 my $relationship = $patron->add_guarantor(
2268 borrowernumber => $borrowernumber,
2269 relationships => $relationship,
2273 Adds a new guarantor to a patron.
2278 my ( $self, $params ) = @_;
2280 my $guarantor_id = $params->{guarantor_id};
2281 my $relationship = $params->{relationship};
2283 return Koha::Patron::Relationship->new(
2285 guarantee_id => $self->id,
2286 guarantor_id => $guarantor_id,
2287 relationship => $relationship
2292 =head3 get_extended_attribute
2294 my $attribute_value = $patron->get_extended_attribute( $code );
2296 Return the attribute for the code passed in parameter.
2298 It not exist it returns undef
2300 Note that this will not work for repeatable attribute types.
2302 Maybe you certainly not want to use this method, it is actually only used for SHOW_BARCODE
2303 (which should be a real patron's attribute (not extended)
2307 sub get_extended_attribute {
2308 my ( $self, $code, $value ) = @_;
2309 my $rs = $self->_result->borrower_attributes;
2311 my $attribute = $rs->search({ code => $code, ( $value ? ( attribute => $value ) : () ) });
2312 return unless $attribute->count;
2313 return $attribute->next;
2316 =head3 set_default_messaging_preferences
2318 $patron->set_default_messaging_preferences
2320 Sets default messaging preferences on patron.
2322 See Koha::Patron::MessagePreference(s) for more documentation, especially on
2327 sub set_default_messaging_preferences {
2328 my ($self, $categorycode) = @_;
2330 my $options = Koha::Patron::MessagePreferences->get_options;
2332 foreach my $option (@$options) {
2333 # Check that this option has preference configuration for this category
2334 unless (Koha::Patron::MessagePreferences->search({
2335 message_attribute_id => $option->{message_attribute_id},
2336 categorycode => $categorycode || $self->categorycode,
2341 # Delete current setting
2342 Koha::Patron::MessagePreferences->search({
2343 borrowernumber => $self->borrowernumber,
2344 message_attribute_id => $option->{message_attribute_id},
2347 Koha::Patron::MessagePreference->new_from_default({
2348 borrowernumber => $self->borrowernumber,
2349 categorycode => $categorycode || $self->categorycode,
2350 message_attribute_id => $option->{message_attribute_id},
2357 =head3 is_accessible
2359 if ( $patron->is_accessible({ user => $logged_in_user }) ) { ... }
2361 This overloaded method validates whether the current I<Koha::Patron> object can be accessed
2362 by the logged in user.
2364 Returns 0 if the I<user> parameter is missing.
2369 my ( $self, $params ) = @_;
2371 unless ( defined( $params->{user} ) ) {
2372 Koha::Exceptions::MissingParameter->throw( error => "The `user` parameter is mandatory" );
2375 my $consumer = $params->{user};
2376 return $consumer->can_see_patron_infos($self);
2379 =head3 unredact_list
2381 This method returns the list of database fields that should be visible, even for restricted users,
2382 for both API and UI output purposes
2387 return ['branchcode'];
2392 my $json = $patron->to_api;
2394 Overloaded method that returns a JSON representation of the Koha::Patron object,
2395 suitable for API output.
2400 my ( $self, $params ) = @_;
2402 my $json_patron = $self->SUPER::to_api( $params );
2404 return unless $json_patron;
2406 $json_patron->{restricted} = ( $self->is_debarred )
2408 : Mojo::JSON->false;
2410 return $json_patron;
2413 =head3 to_api_mapping
2415 This method returns the mapping for representing a Koha::Patron object
2420 sub to_api_mapping {
2422 borrowernotes => 'staff_notes',
2423 borrowernumber => 'patron_id',
2424 branchcode => 'library_id',
2425 categorycode => 'category_id',
2426 checkprevcheckout => 'check_previous_checkout',
2427 contactfirstname => undef, # Unused
2428 contactname => undef, # Unused
2429 contactnote => 'altaddress_notes',
2430 contacttitle => undef, # Unused
2431 dateenrolled => 'date_enrolled',
2432 dateexpiry => 'expiry_date',
2433 dateofbirth => 'date_of_birth',
2434 debarred => undef, # replaced by 'restricted'
2435 debarredcomment => undef, # calculated, API consumers will use /restrictions instead
2436 emailpro => 'secondary_email',
2437 flags => undef, # permissions manipulation handled in /permissions
2438 gonenoaddress => 'incorrect_address',
2439 lastseen => 'last_seen',
2440 lost => 'patron_card_lost',
2441 opacnote => 'opac_notes',
2442 othernames => 'other_name',
2443 password => undef, # password manipulation handled in /password
2444 phonepro => 'secondary_phone',
2445 relationship => 'relationship_type',
2447 smsalertnumber => 'sms_number',
2448 sort1 => 'statistics_1',
2449 sort2 => 'statistics_2',
2450 autorenew_checkouts => 'autorenew_checkouts',
2451 streetnumber => 'street_number',
2452 streettype => 'street_type',
2453 zipcode => 'postal_code',
2454 B_address => 'altaddress_address',
2455 B_address2 => 'altaddress_address2',
2456 B_city => 'altaddress_city',
2457 B_country => 'altaddress_country',
2458 B_email => 'altaddress_email',
2459 B_phone => 'altaddress_phone',
2460 B_state => 'altaddress_state',
2461 B_streetnumber => 'altaddress_street_number',
2462 B_streettype => 'altaddress_street_type',
2463 B_zipcode => 'altaddress_postal_code',
2464 altcontactaddress1 => 'altcontact_address',
2465 altcontactaddress2 => 'altcontact_address2',
2466 altcontactaddress3 => 'altcontact_city',
2467 altcontactcountry => 'altcontact_country',
2468 altcontactfirstname => 'altcontact_firstname',
2469 altcontactphone => 'altcontact_phone',
2470 altcontactsurname => 'altcontact_surname',
2471 altcontactstate => 'altcontact_state',
2472 altcontactzipcode => 'altcontact_postal_code',
2473 password_expiration_date => undef,
2474 primary_contact_method => undef,
2476 auth_method => undef,
2482 Returns a map of column name to string representations including the string.
2487 my ( $self, $params ) = @_;
2491 str => $self->library->branchname,
2495 str => $self->category->description,
2496 type => 'patron_category',
2503 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_name => 'DUE'});
2504 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports });
2505 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports, test_mode => 1 });
2507 Queue messages to a patron. Can pass a message that is part of the message_attributes
2508 table or supply the transport to use.
2510 If passed a message name we retrieve the patrons preferences for transports
2511 Otherwise we use the supplied transport. In the case of email or sms we fall back to print if
2512 we have no address/number for sending
2514 $letter_params is a hashref of the values to be passed to GetPreparedLetter
2516 test_mode will only report which notices would be sent, but nothing will be queued
2521 my ( $self, $params ) = @_;
2522 my $letter_params = $params->{letter_params};
2523 my $test_mode = $params->{test_mode};
2525 return unless $letter_params;
2526 return unless exists $params->{message_name} xor $params->{message_transports}; # We only want one of these
2528 my $library = Koha::Libraries->find( $letter_params->{branchcode} );
2529 my $from_email_address = $library->from_email_address;
2531 my @message_transports;
2533 $letter_code = $letter_params->{letter_code};
2534 if( $params->{message_name} ){
2535 my $messaging_prefs = C4::Members::Messaging::GetMessagingPreferences( {
2536 borrowernumber => $letter_params->{borrowernumber},
2537 message_name => $params->{message_name}
2539 @message_transports = ( keys %{ $messaging_prefs->{transports} } );
2540 $letter_code = $messaging_prefs->{transports}->{$message_transports[0]} unless $letter_code;
2542 @message_transports = @{$params->{message_transports}};
2544 return unless defined $letter_code;
2545 $letter_params->{letter_code} = $letter_code;
2548 foreach my $mtt (@message_transports){
2549 next if ($mtt eq 'itiva' and C4::Context->preference('TalkingTechItivaPhoneNotification') );
2550 # Notice is handled by TalkingTech_itiva_outbound.pl
2551 if ( ( $mtt eq 'email' and not $self->notice_email_address )
2552 or ( $mtt eq 'sms' and not $self->smsalertnumber )
2553 or ( $mtt eq 'phone' and not $self->phone ) )
2555 push @{ $return{fallback} }, $mtt;
2558 next if $mtt eq 'print' && $print_sent;
2559 $letter_params->{message_transport_type} = $mtt;
2560 my $letter = C4::Letters::GetPreparedLetter( %$letter_params );
2561 C4::Letters::EnqueueLetter({
2563 borrowernumber => $self->borrowernumber,
2564 from_address => $from_email_address,
2565 message_transport_type => $mtt
2566 }) unless $test_mode;
2567 push @{$return{sent}}, $mtt;
2568 $print_sent = 1 if $mtt eq 'print';
2573 =head3 safe_to_delete
2575 my $result = $patron->safe_to_delete;
2576 if ( $result eq 'has_guarantees' ) { ... }
2577 elsif ( $result ) { ... }
2578 else { # cannot delete }
2580 This method tells if the Koha:Patron object can be deleted. Possible return values
2586 =item 'has_checkouts'
2590 =item 'has_guarantees'
2592 =item 'is_anonymous_patron'
2594 =item 'is_protected'
2600 sub safe_to_delete {
2603 my $anonymous_patron = C4::Context->preference('AnonymousPatron');
2607 if ( $anonymous_patron && $self->id eq $anonymous_patron ) {
2608 $error = 'is_anonymous_patron';
2610 elsif ( $self->checkouts->count ) {
2611 $error = 'has_checkouts';
2613 elsif ( $self->account->outstanding_debits->total_outstanding > 0 ) {
2614 $error = 'has_debt';
2616 elsif ( $self->guarantee_relationships->count ) {
2617 $error = 'has_guarantees';
2619 elsif ( $self->protected ) {
2620 $error = 'is_protected';
2624 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
2627 return Koha::Result::Boolean->new(1);
2632 my $recalls = $patron->recalls;
2634 Return the patron's recalls.
2641 return Koha::Recalls->search({ patron_id => $self->borrowernumber });
2644 =head3 account_balance
2646 my $balance = $patron->account_balance
2648 Return the patron's account balance
2652 sub account_balance {
2654 return $self->account->balance;
2657 =head3 notify_library_of_registration
2659 $patron->notify_library_of_registration( $email_patron_registrations );
2661 Send patron registration email to library if EmailPatronRegistrations system preference is enabled.
2665 sub notify_library_of_registration {
2666 my ( $self, $email_patron_registrations ) = @_;
2669 my $letter = C4::Letters::GetPreparedLetter(
2670 module => 'members',
2671 letter_code => 'OPAC_REG',
2672 branchcode => $self->branchcode,
2673 lang => $self->lang || 'default',
2675 'borrowers' => $self->borrowernumber
2680 if ( $email_patron_registrations eq "BranchEmailAddress" ) {
2681 my $library = Koha::Libraries->find( $self->branchcode );
2682 $to_address = $library->inbound_email_address;
2684 elsif ( $email_patron_registrations eq "KohaAdminEmailAddress" ) {
2685 $to_address = C4::Context->preference('ReplytoDefault')
2686 || C4::Context->preference('KohaAdminEmailAddress');
2690 C4::Context->preference('EmailAddressForPatronRegistrations')
2691 || C4::Context->preference('ReplytoDefault')
2692 || C4::Context->preference('KohaAdminEmailAddress');
2695 my $message_id = C4::Letters::EnqueueLetter(
2698 borrowernumber => $self->borrowernumber,
2699 to_address => $to_address,
2700 message_transport_type => 'email'
2702 ) or warn "can't enqueue letter $letter";
2703 if ( $message_id ) {
2709 =head3 has_messaging_preference
2711 my $bool = $patron->has_messaging_preference({
2712 message_name => $message_name, # A value from message_attributes.message_name
2713 message_transport_type => $message_transport_type, # email, sms, phone, itiva, etc...
2714 wants_digest => $wants_digest, # 1 if you are looking for the digest version, don't pass if you just want either
2719 sub has_messaging_preference {
2720 my ( $self, $params ) = @_;
2722 my $message_name = $params->{message_name};
2723 my $message_transport_type = $params->{message_transport_type};
2724 my $wants_digest = $params->{wants_digest};
2726 return $self->_result->search_related_rs(
2727 'borrower_message_preferences',
2731 [ 'borrower_message_transport_preferences', 'message_attribute' ]
2736 =head3 can_patron_change_staff_only_lists
2738 $patron->can_patron_change_staff_only_lists;
2740 Return 1 if a patron has 'Superlibrarian' or 'Catalogue' permission.
2741 Otherwise, return 0.
2745 sub can_patron_change_staff_only_lists {
2746 my ( $self, $params ) = @_;
2747 return 1 if C4::Auth::haspermission( $self->userid, { 'catalogue' => 1 });
2751 =head3 can_patron_change_permitted_staff_lists
2753 $patron->can_patron_change_permitted_staff_lists;
2755 Return 1 if a patron has 'Superlibrarian' or 'Catalogue' and 'edit_public_list_contents' permissions.
2756 Otherwise, return 0.
2760 sub can_patron_change_permitted_staff_lists {
2761 my ( $self, $params ) = @_;
2762 return 1 if C4::Auth::haspermission( $self->userid, { 'catalogue' => 1, lists => 'edit_public_list_contents' } );
2766 =head3 encode_secret
2768 $patron->encode_secret($secret32);
2770 Secret (TwoFactorAuth expects it in base32 format) is encrypted.
2771 You still need to call ->store.
2776 my ( $self, $secret ) = @_;
2778 return $self->secret( Koha::Encryption->new->encrypt_hex($secret) );
2780 return $self->secret($secret);
2783 =head3 decoded_secret
2785 my $secret32 = $patron->decoded_secret;
2787 Decode the patron secret. We expect to get back a base32 string, but this
2788 is not checked here. Caller of encode_secret is responsible for that.
2792 sub decoded_secret {
2794 if( $self->secret ) {
2795 return Koha::Encryption->new->decrypt_hex( $self->secret );
2797 return $self->secret;
2800 =head3 virtualshelves
2802 my $shelves = $patron->virtualshelves;
2806 sub virtualshelves {
2808 return Koha::Virtualshelves->_new_from_dbic( scalar $self->_result->virtualshelves );
2813 my $savings = $patron->get_savings;
2815 Use the replacement price of patron's old and current issues to calculate how much they have 'saved' by using the library.
2822 my @itemnumbers = grep { defined $_ } ( $self->old_checkouts->get_column('itemnumber'), $self->checkouts->get_column('itemnumber') );
2824 return Koha::Items->search(
2825 { itemnumber => { -in => \@itemnumbers } },
2826 { select => [ { sum => 'me.replacementprice' } ],
2827 as => ['total_savings']
2829 )->next->get_column('total_savings') // 0;
2832 =head3 alert_subscriptions
2834 my $subscriptions = $patron->alert_subscriptions;
2836 Return a Koha::Subscriptions object containing subscriptions for which the patron has subscribed to email alerts.
2840 sub alert_subscriptions {
2843 my @alerts = $self->_result->alerts;
2844 my @subscription_ids = map { $_->externalid } @alerts;
2846 return Koha::Subscriptions->search( { subscriptionid => \@subscription_ids } );
2851 my $consent = $patron->consent(TYPE);
2853 Returns the first consent of type TYPE (there should be only one) or a new instance
2854 of Koha::Patron::Consent.
2859 my ( $self, $type ) = @_;
2860 Koha::Exceptions::MissingParameter->throw('Missing consent type') if !$type;
2861 my $consents = Koha::Patron::Consents->search(
2863 borrowernumber => $self->borrowernumber,
2867 return $consents && $consents->count
2869 : Koha::Patron::Consent->new( { borrowernumber => $self->borrowernumber, type => $type } );
2872 =head2 Internal methods
2884 Kyle M Hall <kyle@bywatersolutions.com>
2885 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
2886 Martin Renvoize <martin.renvoize@ptfs-europe.com>