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::Log qw( logaction );
31 use Koha::ArticleRequests;
35 use Koha::CirculationRules;
36 use Koha::Club::Enrollments;
38 use Koha::DateUtils qw( dt_from_string );
40 use Koha::Exceptions::Password;
42 use Koha::Old::Checkouts;
43 use Koha::Patron::Attributes;
44 use Koha::Patron::Categories;
45 use Koha::Patron::Debarments;
46 use Koha::Patron::HouseboundProfile;
47 use Koha::Patron::HouseboundRole;
48 use Koha::Patron::Images;
49 use Koha::Patron::Messages;
50 use Koha::Patron::Modifications;
51 use Koha::Patron::Relationships;
55 use Koha::Result::Boolean;
56 use Koha::Subscription::Routinglists;
58 use Koha::Virtualshelves;
60 use base qw(Koha::Object);
62 use constant ADMINISTRATIVE_LOCKOUT => -1;
64 our $RESULTSET_PATRON_ID_MAPPING = {
65 Accountline => 'borrowernumber',
66 Aqbasketuser => 'borrowernumber',
67 Aqbudget => 'budget_owner_id',
68 Aqbudgetborrower => 'borrowernumber',
69 ArticleRequest => 'borrowernumber',
70 BorrowerDebarment => 'borrowernumber',
71 BorrowerFile => 'borrowernumber',
72 BorrowerModification => 'borrowernumber',
73 ClubEnrollment => 'borrowernumber',
74 Issue => 'borrowernumber',
75 ItemsLastBorrower => 'borrowernumber',
76 Linktracker => 'borrowernumber',
77 Message => 'borrowernumber',
78 MessageQueue => 'borrowernumber',
79 OldIssue => 'borrowernumber',
80 OldReserve => 'borrowernumber',
81 Rating => 'borrowernumber',
82 Reserve => 'borrowernumber',
83 Review => 'borrowernumber',
84 SearchHistory => 'userid',
85 Statistic => 'borrowernumber',
86 Suggestion => 'suggestedby',
87 TagAll => 'borrowernumber',
88 Virtualshelfcontent => 'borrowernumber',
89 Virtualshelfshare => 'borrowernumber',
90 Virtualshelve => 'owner',
95 Koha::Patron - Koha Patron Object class
106 my ( $class, $params ) = @_;
108 return $class->SUPER::new($params);
111 =head3 fixup_cardnumber
113 Autogenerate next cardnumber from highest value found in database
117 sub fixup_cardnumber {
120 my $max = $self->cardnumber;
121 Koha::Plugins->call( 'patron_barcode_transform', \$max );
123 $max ||= Koha::Patrons->search({
124 cardnumber => {-regexp => '^-?[0-9]+$'}
126 select => \'CAST(cardnumber AS SIGNED)',
127 as => ['cast_cardnumber']
128 })->_resultset->get_column('cast_cardnumber')->max;
129 $self->cardnumber(($max || 0) +1);
132 =head3 trim_whitespace
134 trim whitespace from data which has some non-whitespace in it.
135 Could be moved to Koha::Object if need to be reused
139 sub trim_whitespaces {
142 my $schema = Koha::Database->new->schema;
143 my @columns = $schema->source($self->_type)->columns;
145 for my $column( @columns ) {
146 my $value = $self->$column;
147 if ( defined $value ) {
148 $value =~ s/^\s*|\s*$//g;
149 $self->$column($value);
155 =head3 plain_text_password
157 $patron->plain_text_password( $password );
159 stores a copy of the unencrypted password in the object
160 for use in code before encrypting for db
164 sub plain_text_password {
165 my ( $self, $password ) = @_;
167 $self->{_plain_text_password} = $password;
170 return $self->{_plain_text_password}
171 if $self->{_plain_text_password};
178 Patron specific store method to cleanup record
179 and do other necessary things before saving
187 $self->_result->result_source->schema->txn_do(
190 C4::Context->preference("autoMemberNum")
191 and ( not defined $self->cardnumber
192 or $self->cardnumber eq '' )
195 # Warning: The caller is responsible for locking the members table in write
196 # mode, to avoid database corruption.
197 # We are in a transaction but the table is not locked
198 $self->fixup_cardnumber;
201 unless( $self->category->in_storage ) {
202 Koha::Exceptions::Object::FKConstraint->throw(
203 broken_fk => 'categorycode',
204 value => $self->categorycode,
208 $self->trim_whitespaces;
210 my $new_cardnumber = $self->cardnumber;
211 Koha::Plugins->call( 'patron_barcode_transform', \$new_cardnumber );
212 $self->cardnumber( $new_cardnumber );
214 # Set surname to uppercase if uppercasesurname is true
215 $self->surname( uc($self->surname) )
216 if C4::Context->preference("uppercasesurnames");
218 $self->relationship(undef) # We do not want to store an empty string in this field
219 if defined $self->relationship
220 and $self->relationship eq "";
222 unless ( $self->in_storage ) { #AddMember
224 # Generate a valid userid/login if needed
225 $self->generate_userid
226 if not $self->userid or not $self->has_valid_userid;
228 # Add expiration date if it isn't already there
229 unless ( $self->dateexpiry ) {
230 $self->dateexpiry( $self->category->get_expiry_date );
233 # Add enrollment date if it isn't already there
234 unless ( $self->dateenrolled ) {
235 $self->dateenrolled(dt_from_string);
238 # Set the privacy depending on the patron's category
239 my $default_privacy = $self->category->default_privacy || q{};
241 $default_privacy eq 'default' ? 1
242 : $default_privacy eq 'never' ? 2
243 : $default_privacy eq 'forever' ? 0
245 $self->privacy($default_privacy);
247 # Call any check_password plugins if password is passed
248 if ( C4::Context->config("enable_plugins") && $self->password ) {
249 my @plugins = Koha::Plugins->new()->GetPlugins({
250 method => 'check_password',
252 foreach my $plugin ( @plugins ) {
253 # This plugin hook will also be used by a plugin for the Norwegian national
254 # patron database. This is why we need to pass both the password and the
255 # borrowernumber to the plugin.
256 my $ret = $plugin->check_password(
258 password => $self->password,
259 borrowernumber => $self->borrowernumber
262 if ( $ret->{'error'} == 1 ) {
263 Koha::Exceptions::Password::Plugin->throw();
268 # Make a copy of the plain text password for later use
269 $self->plain_text_password( $self->password );
271 $self->password_expiration_date( $self->password
272 ? $self->category->get_password_expiry_date || undef
274 # Create a disabled account if no password provided
275 $self->password( $self->password
276 ? Koha::AuthUtils::hash_password( $self->password )
279 $self->borrowernumber(undef);
281 $self = $self->SUPER::store;
283 $self->add_enrolment_fee_if_needed(0);
285 logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
286 if C4::Context->preference("BorrowersLog");
290 my $self_from_storage = $self->get_from_storage;
291 # FIXME We should not deal with that here, callers have to do this job
292 # Moved from ModMember to prevent regressions
293 unless ( $self->userid ) {
294 my $stored_userid = $self_from_storage->userid;
295 $self->userid($stored_userid);
298 # Password must be updated using $self->set_password
299 $self->password($self_from_storage->password);
301 if ( $self->category->categorycode ne
302 $self_from_storage->category->categorycode )
304 # Add enrolement fee on category change if required
305 $self->add_enrolment_fee_if_needed(1)
306 if C4::Context->preference('FeeOnChangePatronCategory');
308 # Clean up guarantors on category change if required
309 $self->guarantor_relationships->delete
310 unless ( $self->category->can_be_guarantee );
315 if ( C4::Context->preference("BorrowersLog") ) {
317 my $from_storage = $self_from_storage->unblessed;
318 my $from_object = $self->unblessed;
319 my @skip_fields = (qw/lastseen updated_on/);
320 for my $key ( keys %{$from_storage} ) {
321 next if any { /$key/ } @skip_fields;
324 !defined( $from_storage->{$key} )
325 && defined( $from_object->{$key} )
327 || ( defined( $from_storage->{$key} )
328 && !defined( $from_object->{$key} ) )
330 defined( $from_storage->{$key} )
331 && defined( $from_object->{$key} )
332 && ( $from_storage->{$key} ne
333 $from_object->{$key} )
338 before => $from_storage->{$key},
339 after => $from_object->{$key}
344 if ( defined($info) ) {
348 $self->borrowernumber,
351 { utf8 => 1, pretty => 1, canonical => 1 }
358 $self = $self->SUPER::store;
369 Delete patron's holds, lists and finally the patron.
371 Lists owned by the borrower are deleted or ownership is transfered depending on the
372 ListOwnershipUponPatronDeletion pref, but entries from the borrower to other lists are kept.
379 my $anonymous_patron = C4::Context->preference("AnonymousPatron");
380 Koha::Exceptions::Patron::FailedDeleteAnonymousPatron->throw() if $anonymous_patron && $self->id eq $anonymous_patron;
382 $self->_result->result_source->schema->txn_do(
384 # Cancel Patron's holds
385 my $holds = $self->holds;
386 while( my $hold = $holds->next ){
390 # FIXME Could be $patron->get_lists
391 # If ListOwnershipUponPatronDeletion = transfer, change ownership of all
392 # public and shared lists to the user who deleted them.
393 if ( C4::Context->preference('ListOwnershipUponPatronDeletion') eq 'transfer' ) {
394 my $userenv = C4::Context->userenv();
395 my $usernumber = (ref($userenv) eq 'HASH') ? $userenv->{'number'} : 0;
396 my @publiclists = Koha::Virtualshelves->get_public_shelves;
397 my @sharedlists = Koha::Virtualshelves->get_shared_shelves({ borrowernumber => $self->borrowernumber });
398 foreach my $plist ( @publiclists ) {
399 if ( $plist->owner == $self->borrowernumber ) {
400 my $unique_name = $plist->shelfname . '_' . $self->borrowernumber;
401 $plist->set({ owner => $usernumber, shelfname => $unique_name })->store;
404 foreach my $slist ( @sharedlists ) {
405 my $unique_name = $slist->shelfname . '_' . $self->borrowernumber;
406 $slist->set({ owner => $usernumber, shelfname => $unique_name })->store;
407 # if staff member had a share, remove it
408 $slist->remove_share( $usernumber );
412 # Delete any remaining lists that this user is an owner of (always private lists,
413 # only public and shared lists if ListOwnershipUponPatronDeletion = delete)
414 $_->delete for Koha::Virtualshelves->search({ owner => $self->borrowernumber });
416 # We cannot have a FK on borrower_modifications.borrowernumber, the table is also used
418 $_->delete for Koha::Patron::Modifications->search( { borrowernumber => $self->borrowernumber } )->as_list;
420 $self->SUPER::delete;
422 logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
431 my $patron_category = $patron->category
433 Return the patron category for this patron
439 return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
449 return Koha::Patron::Images->find( $self->borrowernumber );
454 Returns a Koha::Library object representing the patron's home library.
460 return Koha::Library->_new_from_dbic($self->_result->branchcode);
465 Returns a Koha::SMS::Provider object representing the patron's SMS provider.
471 my $sms_provider_rs = $self->_result->sms_provider;
472 return unless $sms_provider_rs;
473 return Koha::SMS::Provider->_new_from_dbic($sms_provider_rs);
476 =head3 guarantor_relationships
478 Returns Koha::Patron::Relationships object for this patron's guarantors
480 Returns the set of relationships for the patrons that are guarantors for this patron.
482 This is returned instead of a Koha::Patron object because the guarantor
483 may not exist as a patron in Koha. If this is true, the guarantors name
484 exists in the Koha::Patron::Relationship object and will have no guarantor_id.
488 sub guarantor_relationships {
491 return Koha::Patron::Relationships->search( { guarantee_id => $self->id } );
494 =head3 guarantee_relationships
496 Returns Koha::Patron::Relationships object for this patron's guarantors
498 Returns the set of relationships for the patrons that are guarantees for this patron.
500 The method returns Koha::Patron::Relationship objects for the sake
501 of consistency with the guantors method.
502 A guarantee by definition must exist as a patron in Koha.
506 sub guarantee_relationships {
509 return Koha::Patron::Relationships->search(
510 { guarantor_id => $self->id },
512 prefetch => 'guarantee',
513 order_by => { -asc => [ 'guarantee.surname', 'guarantee.firstname' ] },
518 =head3 relationships_debt
520 Returns the amount owed by the patron's guarantors *and* the other guarantees of those guarantors
524 sub relationships_debt {
525 my ($self, $params) = @_;
527 my $include_guarantors = $params->{include_guarantors};
528 my $only_this_guarantor = $params->{only_this_guarantor};
529 my $include_this_patron = $params->{include_this_patron};
532 if ( $only_this_guarantor ) {
533 @guarantors = $self->guarantee_relationships->count ? ( $self ) : ();
534 Koha::Exceptions::BadParameter->throw( { parameter => 'only_this_guarantor' } ) unless @guarantors;
535 } elsif ( $self->guarantor_relationships->count ) {
536 # I am a guarantee, just get all my guarantors
537 @guarantors = $self->guarantor_relationships->guarantors->as_list;
539 # I am a guarantor, I need to get all the guarantors of all my guarantees
540 @guarantors = map { $_->guarantor_relationships->guarantors->as_list } $self->guarantee_relationships->guarantees->as_list;
543 my $non_issues_charges = 0;
544 my $seen = $include_this_patron ? {} : { $self->id => 1 }; # For tracking members already added to the total
545 foreach my $guarantor (@guarantors) {
546 $non_issues_charges += $guarantor->account->non_issues_charges if $include_guarantors && !$seen->{ $guarantor->id };
548 # We've added what the guarantor owes, not added in that guarantor's guarantees as well
549 my @guarantees = map { $_->guarantee } $guarantor->guarantee_relationships->as_list;
550 my $guarantees_non_issues_charges = 0;
551 foreach my $guarantee (@guarantees) {
552 next if $seen->{ $guarantee->id };
553 $guarantees_non_issues_charges += $guarantee->account->non_issues_charges;
554 # Mark this guarantee as seen so we don't double count a guarantee linked to multiple guarantors
555 $seen->{ $guarantee->id } = 1;
558 $non_issues_charges += $guarantees_non_issues_charges;
559 $seen->{ $guarantor->id } = 1;
562 return $non_issues_charges;
565 =head3 housebound_profile
567 Returns the HouseboundProfile associated with this patron.
571 sub housebound_profile {
573 my $profile = $self->_result->housebound_profile;
574 return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
579 =head3 housebound_role
581 Returns the HouseboundRole associated with this patron.
585 sub housebound_role {
588 my $role = $self->_result->housebound_role;
589 return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
595 Returns the siblings of this patron.
602 my @guarantors = $self->guarantor_relationships()->guarantors()->as_list;
604 return unless @guarantors;
607 map { $_->guarantee_relationships()->guarantees()->as_list } @guarantors;
609 return unless @siblings;
613 grep { !$seen{ $_->id }++ && ( $_->id != $self->id ) } @siblings;
615 return Koha::Patrons->search( { borrowernumber => { -in => [ map { $_->id } @siblings ] } } );
620 my $patron = Koha::Patrons->find($id);
621 $patron->merge_with( \@patron_ids );
623 This subroutine merges a list of patrons into the patron record. This is accomplished by finding
624 all related patron ids for the patrons to be merged in other tables and changing the ids to be that
625 of the keeper patron.
630 my ( $self, $patron_ids ) = @_;
632 my $anonymous_patron = C4::Context->preference("AnonymousPatron");
633 return if $anonymous_patron && $self->id eq $anonymous_patron;
635 my @patron_ids = @{ $patron_ids };
637 # Ensure the keeper isn't in the list of patrons to merge
638 @patron_ids = grep { $_ ne $self->id } @patron_ids;
640 my $schema = Koha::Database->new()->schema();
644 $self->_result->result_source->schema->txn_do( sub {
645 foreach my $patron_id (@patron_ids) {
647 next if $patron_id eq $anonymous_patron;
649 my $patron = Koha::Patrons->find( $patron_id );
653 # Unbless for safety, the patron will end up being deleted
654 $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
656 my $attributes = $patron->extended_attributes;
657 my $new_attributes = [
658 map { { code => $_->code, attribute => $_->attribute } }
661 $attributes->delete; # We need to delete before trying to merge them to prevent exception on unique and repeatable
662 for my $attribute ( @$new_attributes ) {
664 $self->add_extended_attribute($attribute);
666 # Don't block the merge if there is a non-repeatable attribute that cannot be added to the current patron.
667 unless ( $_->isa('Koha::Exceptions::Patron::Attribute::NonRepeatable') ) {
673 while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
674 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
675 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
676 $rs->update({ $field => $self->id });
677 if ( $r eq 'BorrowerDebarment' ) {
678 Koha::Patron::Debarments::UpdateBorrowerDebarmentFlags($self->id);
682 $patron->move_to_deleted();
692 =head3 wants_check_for_previous_checkout
694 $wants_check = $patron->wants_check_for_previous_checkout;
696 Return 1 if Koha needs to perform PrevIssue checking, else 0.
700 sub wants_check_for_previous_checkout {
702 my $syspref = C4::Context->preference("checkPrevCheckout");
705 ## Hard syspref trumps all
706 return 1 if ($syspref eq 'hardyes');
707 return 0 if ($syspref eq 'hardno');
708 ## Now, patron pref trumps all
709 return 1 if ($self->checkprevcheckout eq 'yes');
710 return 0 if ($self->checkprevcheckout eq 'no');
712 # More complex: patron inherits -> determine category preference
713 my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
714 return 1 if ($checkPrevCheckoutByCat eq 'yes');
715 return 0 if ($checkPrevCheckoutByCat eq 'no');
717 # Finally: category preference is inherit, default to 0
718 if ($syspref eq 'softyes') {
725 =head3 do_check_for_previous_checkout
727 $do_check = $patron->do_check_for_previous_checkout($item);
729 Return 1 if the bib associated with $ITEM has previously been checked out to
730 $PATRON, 0 otherwise.
734 sub do_check_for_previous_checkout {
735 my ( $self, $item ) = @_;
738 my $biblio = Koha::Biblios->find( $item->{biblionumber} );
739 if ( $biblio->is_serial ) {
740 push @item_nos, $item->{itemnumber};
742 # Get all itemnumbers for given bibliographic record.
743 @item_nos = $biblio->items->get_column( 'itemnumber' );
746 # Create (old)issues search criteria
748 borrowernumber => $self->borrowernumber,
749 itemnumber => \@item_nos,
752 my $delay = C4::Context->preference('CheckPrevCheckoutDelay') || 0;
754 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
755 my $newer_than = dt_from_string()->subtract( days => $delay );
756 $criteria->{'returndate'} = { '>' => $dtf->format_datetime($newer_than), };
759 # Check current issues table
760 my $issues = Koha::Checkouts->search($criteria);
761 return 1 if $issues->count; # 0 || N
763 # Check old issues table
764 my $old_issues = Koha::Old::Checkouts->search($criteria);
765 return $old_issues->count; # 0 || N
770 my $debarment_expiration = $patron->is_debarred;
772 Returns the date a patron debarment will expire, or undef if the patron is not
780 return unless $self->debarred;
781 return $self->debarred
782 if $self->debarred =~ '^9999'
783 or dt_from_string( $self->debarred ) > dt_from_string;
789 my $is_expired = $patron->is_expired;
791 Returns 1 if the patron is expired or 0;
797 return 0 unless $self->dateexpiry;
798 return 0 if $self->dateexpiry =~ '^9999';
799 return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
803 =head3 password_expired
805 my $password_expired = $patron->password_expired;
807 Returns 1 if the patron's password is expired or 0;
811 sub password_expired {
813 return 0 unless $self->password_expiration_date;
814 return 1 if dt_from_string( $self->password_expiration_date ) <= dt_from_string->truncate( to => 'day' );
818 =head3 is_going_to_expire
820 my $is_going_to_expire = $patron->is_going_to_expire;
822 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
826 sub is_going_to_expire {
829 my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
831 return 0 unless $delay;
832 return 0 unless $self->dateexpiry;
833 return 0 if $self->dateexpiry =~ '^9999';
834 return 1 if dt_from_string( $self->dateexpiry, undef, 'floating' )->subtract( days => $delay ) < dt_from_string(undef, undef, 'floating')->truncate( to => 'day' );
840 $patron->set_password({ password => $plain_text_password [, skip_validation => 1 ] });
842 Set the patron's password.
846 The passed string is validated against the current password enforcement policy.
847 Validation can be skipped by passing the I<skip_validation> parameter.
849 Exceptions are thrown if the password is not good enough.
853 =item Koha::Exceptions::Password::TooShort
855 =item Koha::Exceptions::Password::WhitespaceCharacters
857 =item Koha::Exceptions::Password::TooWeak
859 =item Koha::Exceptions::Password::Plugin (if a "check password" plugin is enabled)
866 my ( $self, $args ) = @_;
868 my $password = $args->{password};
870 unless ( $args->{skip_validation} ) {
871 my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password, $self->category );
874 if ( $error eq 'too_short' ) {
875 my $min_length = $self->category->effective_min_password_length;
876 $min_length = 3 if not $min_length or $min_length < 3;
878 my $password_length = length($password);
879 Koha::Exceptions::Password::TooShort->throw(
880 length => $password_length, min_length => $min_length );
882 elsif ( $error eq 'has_whitespaces' ) {
883 Koha::Exceptions::Password::WhitespaceCharacters->throw();
885 elsif ( $error eq 'too_weak' ) {
886 Koha::Exceptions::Password::TooWeak->throw();
891 if ( C4::Context->config("enable_plugins") ) {
892 # Call any check_password plugins
893 my @plugins = Koha::Plugins->new()->GetPlugins({
894 method => 'check_password',
896 foreach my $plugin ( @plugins ) {
897 # This plugin hook will also be used by a plugin for the Norwegian national
898 # patron database. This is why we need to pass both the password and the
899 # borrowernumber to the plugin.
900 my $ret = $plugin->check_password(
902 password => $password,
903 borrowernumber => $self->borrowernumber
906 # This plugin hook will also be used by a plugin for the Norwegian national
907 # patron database. This is why we need to call the actual plugins and then
908 # check skip_validation afterwards.
909 if ( $ret->{'error'} == 1 && !$args->{skip_validation} ) {
910 Koha::Exceptions::Password::Plugin->throw();
915 my $digest = Koha::AuthUtils::hash_password($password);
917 $self->password_expiration_date( $self->category->get_password_expiry_date || undef );
919 # We do not want to call $self->store and retrieve password from DB
920 $self->password($digest);
921 $self->login_attempts(0);
924 logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" )
925 if C4::Context->preference("BorrowersLog");
933 my $new_expiry_date = $patron->renew_account
935 Extending the subscription to the expiry date.
942 if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
943 $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
946 C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
947 ? dt_from_string( $self->dateexpiry )
950 my $expiry_date = $self->category->get_expiry_date($date);
952 $self->dateexpiry($expiry_date);
953 $self->date_renewed( dt_from_string() );
956 $self->add_enrolment_fee_if_needed(1);
958 logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
959 return dt_from_string( $expiry_date )->truncate( to => 'day' );
964 my $has_overdues = $patron->has_overdues;
966 Returns the number of patron's overdues
972 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
973 return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
978 $patron->track_login;
979 $patron->track_login({ force => 1 });
981 Tracks a (successful) login attempt.
982 The preference TrackLastPatronActivity must be enabled. Or you
983 should pass the force parameter.
988 my ( $self, $params ) = @_;
991 !C4::Context->preference('TrackLastPatronActivity');
992 $self->lastseen( dt_from_string() )->store;
995 =head3 move_to_deleted
997 my $is_moved = $patron->move_to_deleted;
999 Move a patron to the deletedborrowers table.
1000 This can be done before deleting a patron, to make sure the data are not completely deleted.
1004 sub move_to_deleted {
1006 my $patron_infos = $self->unblessed;
1007 delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
1008 return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
1011 =head3 can_request_article
1013 if ( $patron->can_request_article( $library->id ) ) { ... }
1015 Returns true if the patron can request articles. As limits apply for the patron
1016 on the same day, those completed the same day are considered as current.
1018 A I<library_id> can be passed as parameter, falling back to userenv if absent.
1022 sub can_request_article {
1023 my ($self, $library_id) = @_;
1025 $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1027 my $rule = Koha::CirculationRules->get_effective_rule(
1029 branchcode => $library_id,
1030 categorycode => $self->categorycode,
1031 rule_name => 'open_article_requests_limit'
1035 my $limit = ($rule) ? $rule->rule_value : undef;
1037 return 1 unless defined $limit;
1039 my $count = Koha::ArticleRequests->search(
1040 [ { borrowernumber => $self->borrowernumber, status => [ 'REQUESTED', 'PENDING', 'PROCESSING' ] },
1041 { borrowernumber => $self->borrowernumber, status => 'COMPLETED', updated_on => { '>=' => \'CAST(NOW() AS DATE)' } },
1044 return $count < $limit ? 1 : 0;
1047 =head3 article_request_fee
1049 my $fee = $patron->article_request_fee(
1051 [ library_id => $library->id, ]
1055 Returns the fee to be charged to the patron when it places an article request.
1057 A I<library_id> can be passed as parameter, falling back to userenv if absent.
1061 sub article_request_fee {
1062 my ($self, $params) = @_;
1064 my $library_id = $params->{library_id};
1066 $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1068 my $rule = Koha::CirculationRules->get_effective_rule(
1070 branchcode => $library_id,
1071 categorycode => $self->categorycode,
1072 rule_name => 'article_request_fee'
1076 my $fee = ($rule) ? $rule->rule_value + 0 : 0;
1081 =head3 add_article_request_fee_if_needed
1083 my $fee = $patron->add_article_request_fee_if_needed(
1085 [ item_id => $item->id,
1086 library_id => $library->id, ]
1090 If an article request fee needs to be charged, it adds a debit to the patron's
1093 Returns the fee line.
1095 A I<library_id> can be passed as parameter, falling back to userenv if absent.
1099 sub add_article_request_fee_if_needed {
1100 my ($self, $params) = @_;
1102 my $library_id = $params->{library_id};
1103 my $item_id = $params->{item_id};
1105 $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1107 my $amount = $self->article_request_fee(
1109 library_id => $library_id,
1115 if ( $amount > 0 ) {
1116 $debit_line = $self->account->add_debit(
1119 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
1120 interface => C4::Context->interface,
1121 library_id => $library_id,
1122 type => 'ARTICLE_REQUEST',
1123 item_id => $item_id,
1131 =head3 article_requests
1133 my $article_requests = $patron->article_requests;
1135 Returns the patron article requests.
1139 sub article_requests {
1142 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
1145 =head3 add_enrolment_fee_if_needed
1147 my $enrolment_fee = $patron->add_enrolment_fee_if_needed($renewal);
1149 Add enrolment fee for a patron if needed.
1151 $renewal - boolean denoting whether this is an account renewal or not
1155 sub add_enrolment_fee_if_needed {
1156 my ($self, $renewal) = @_;
1157 my $enrolment_fee = $self->category->enrolmentfee;
1158 if ( $enrolment_fee && $enrolment_fee > 0 ) {
1159 my $type = $renewal ? 'ACCOUNT_RENEW' : 'ACCOUNT';
1160 $self->account->add_debit(
1162 amount => $enrolment_fee,
1163 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
1164 interface => C4::Context->interface,
1165 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
1170 return $enrolment_fee || 0;
1175 my $checkouts = $patron->checkouts
1181 my $checkouts = $self->_result->issues;
1182 return Koha::Checkouts->_new_from_dbic( $checkouts );
1185 =head3 pending_checkouts
1187 my $pending_checkouts = $patron->pending_checkouts
1189 This method will return the same as $self->checkouts, but with a prefetch on
1190 items, biblio and biblioitems.
1192 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
1194 It should not be used directly, prefer to access fields you need instead of
1195 retrieving all these fields in one go.
1199 sub pending_checkouts {
1201 my $checkouts = $self->_result->issues->search(
1205 { -desc => 'me.timestamp' },
1206 { -desc => 'issuedate' },
1207 { -desc => 'issue_id' }, # Sort by issue_id should be enough
1209 prefetch => { item => { biblio => 'biblioitems' } },
1212 return Koha::Checkouts->_new_from_dbic( $checkouts );
1215 =head3 old_checkouts
1217 my $old_checkouts = $patron->old_checkouts
1223 my $old_checkouts = $self->_result->old_issues;
1224 return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
1229 my $overdue_items = $patron->overdues
1231 Return the overdue items
1237 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
1238 return $self->checkouts->search(
1240 'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
1243 prefetch => { item => { biblio => 'biblioitems' } },
1248 =head3 get_routing_lists
1250 my $routinglists = $patron->get_routing_lists
1252 Returns the routing lists a patron is subscribed to.
1256 sub get_routing_lists {
1258 my $routing_list_rs = $self->_result->subscriptionroutinglists;
1259 return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
1264 my $age = $patron->get_age
1266 Return the age of the patron
1273 return unless $self->dateofbirth;
1275 my $date_of_birth = dt_from_string( $self->dateofbirth );
1276 my $today = dt_from_string->truncate( to => 'day' );
1278 return $today->subtract_datetime( $date_of_birth )->years;
1283 my $is_valid = $patron->is_valid_age
1285 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
1291 my $age = $self->get_age;
1293 my $patroncategory = $self->category;
1294 my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
1296 return (defined($age) && (($high && ($age > $high)) or ($low && ($age < $low)))) ? 0 : 1;
1301 my $account = $patron->account
1307 return Koha::Account->new( { patron_id => $self->borrowernumber } );
1312 my $holds = $patron->holds
1314 Return all the holds placed by this patron
1320 my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1321 return Koha::Holds->_new_from_dbic($holds_rs);
1326 my $old_holds = $patron->old_holds
1328 Return all the historical holds for this patron
1334 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1335 return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1338 =head3 return_claims
1340 my $return_claims = $patron->return_claims
1346 my $return_claims = $self->_result->return_claims_borrowernumbers;
1347 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $return_claims );
1350 =head3 notice_email_address
1352 my $email = $patron->notice_email_address;
1354 Return the email address of patron used for notices.
1355 Returns the empty string if no email address.
1359 sub notice_email_address{
1362 my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1363 # if syspref is set to 'first valid' (value == OFF), look up email address
1364 if ( $which_address eq 'OFF' ) {
1365 return $self->first_valid_email_address;
1368 return $self->$which_address || '';
1371 =head3 first_valid_email_address
1373 my $first_valid_email_address = $patron->first_valid_email_address
1375 Return the first valid email address for a patron.
1376 For now, the order is defined as email, emailpro, B_email.
1377 Returns the empty string if the borrower has no email addresses.
1381 sub first_valid_email_address {
1384 return $self->email() || $self->emailpro() || $self->B_email() || q{};
1387 =head3 get_club_enrollments
1391 sub get_club_enrollments {
1394 return Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1397 =head3 get_enrollable_clubs
1401 sub get_enrollable_clubs {
1402 my ( $self, $is_enrollable_from_opac ) = @_;
1405 $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1406 if $is_enrollable_from_opac;
1407 $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1409 $params->{borrower} = $self;
1411 return Koha::Clubs->get_enrollable($params);
1414 =head3 account_locked
1416 my $is_locked = $patron->account_locked
1418 Return true if the patron has reached the maximum number of login attempts
1419 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1420 as an administrative lockout (independent of FailedLoginAttempts; see also
1421 Koha::Patron->lock).
1422 Otherwise return false.
1423 If the pref is not set (empty string, null or 0), the feature is considered as
1428 sub account_locked {
1430 my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1431 return 1 if $FailedLoginAttempts
1432 and $self->login_attempts
1433 and $self->login_attempts >= $FailedLoginAttempts;
1434 return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1438 =head3 can_see_patron_infos
1440 my $can_see = $patron->can_see_patron_infos( $patron );
1442 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1446 sub can_see_patron_infos {
1447 my ( $self, $patron ) = @_;
1448 return unless $patron;
1449 return $self->can_see_patrons_from( $patron->branchcode );
1452 =head3 can_see_patrons_from
1454 my $can_see = $patron->can_see_patrons_from( $branchcode );
1456 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1460 sub can_see_patrons_from {
1461 my ( $self, $branchcode ) = @_;
1463 if ( $self->branchcode eq $branchcode ) {
1465 } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1467 } elsif ( my $library_groups = $self->library->library_groups ) {
1468 while ( my $library_group = $library_groups->next ) {
1469 if ( $library_group->parent->has_child( $branchcode ) ) {
1480 my $can_log_into = $patron->can_log_into( $library );
1482 Given a I<Koha::Library> object, it returns a boolean representing
1483 the fact the patron can log into a the library.
1488 my ( $self, $library ) = @_;
1492 if ( C4::Context->preference('IndependentBranches') ) {
1494 if $self->is_superlibrarian
1495 or $self->branchcode eq $library->id;
1505 =head3 libraries_where_can_see_patrons
1507 my $libraries = $patron-libraries_where_can_see_patrons;
1509 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1510 The branchcodes are arbitrarily returned sorted.
1511 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1513 An empty array means no restriction, the patron can see patron's infos from any libraries.
1517 sub libraries_where_can_see_patrons {
1519 my $userenv = C4::Context->userenv;
1521 return () unless $userenv; # For tests, but userenv should be defined in tests...
1523 my @restricted_branchcodes;
1524 if (C4::Context::only_my_library) {
1525 push @restricted_branchcodes, $self->branchcode;
1529 $self->has_permission(
1530 { borrowers => 'view_borrower_infos_from_any_libraries' }
1534 my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1535 if ( $library_groups->count )
1537 while ( my $library_group = $library_groups->next ) {
1538 my $parent = $library_group->parent;
1539 if ( $parent->has_child( $self->branchcode ) ) {
1540 push @restricted_branchcodes, $parent->children->get_column('branchcode');
1545 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1549 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1550 @restricted_branchcodes = uniq(@restricted_branchcodes);
1551 @restricted_branchcodes = sort(@restricted_branchcodes);
1552 return @restricted_branchcodes;
1555 =head3 has_permission
1557 my $permission = $patron->has_permission($required);
1559 See C4::Auth::haspermission for details of syntax for $required
1563 sub has_permission {
1564 my ( $self, $flagsrequired ) = @_;
1565 return unless $self->userid;
1566 # TODO code from haspermission needs to be moved here!
1567 return C4::Auth::haspermission( $self->userid, $flagsrequired );
1570 =head3 is_superlibrarian
1572 my $is_superlibrarian = $patron->is_superlibrarian;
1574 Return true if the patron is a superlibrarian.
1578 sub is_superlibrarian {
1580 return $self->has_permission( { superlibrarian => 1 } ) ? 1 : 0;
1585 my $is_adult = $patron->is_adult
1587 Return true if the patron has a category with a type Adult (A) or Organization (I)
1593 return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1598 my $is_child = $patron->is_child
1600 Return true if the patron has a category with a type Child (C)
1606 return $self->category->category_type eq 'C' ? 1 : 0;
1609 =head3 has_valid_userid
1611 my $patron = Koha::Patrons->find(42);
1612 $patron->userid( $new_userid );
1613 my $has_a_valid_userid = $patron->has_valid_userid
1615 my $patron = Koha::Patron->new( $params );
1616 my $has_a_valid_userid = $patron->has_valid_userid
1618 Return true if the current userid of this patron is valid/unique, otherwise false.
1620 Note that this should be done in $self->store instead and raise an exception if needed.
1624 sub has_valid_userid {
1627 return 0 unless $self->userid;
1629 return 0 if ( $self->userid eq C4::Context->config('user') ); # DB user
1631 my $already_exists = Koha::Patrons->search(
1633 userid => $self->userid,
1636 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1641 return $already_exists ? 0 : 1;
1644 =head3 generate_userid
1646 my $patron = Koha::Patron->new( $params );
1647 $patron->generate_userid
1649 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1651 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).
1655 sub generate_userid {
1658 my $firstname = $self->firstname // q{};
1659 my $surname = $self->surname // q{};
1660 #The script will "do" the following code and increment the $offset until the generated userid is unique
1662 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1663 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1664 my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1665 $userid = NFKD( $userid );
1666 $userid =~ s/\p{NonspacingMark}//g;
1667 $userid .= $offset unless $offset == 0;
1668 $self->userid( $userid );
1670 } while (! $self->has_valid_userid );
1675 =head3 add_extended_attribute
1679 sub add_extended_attribute {
1680 my ($self, $attribute) = @_;
1682 return Koha::Patron::Attribute->new(
1685 ( borrowernumber => $self->borrowernumber ),
1691 =head3 extended_attributes
1693 Return object of Koha::Patron::Attributes type with all attributes set for this patron
1699 sub extended_attributes {
1700 my ( $self, $attributes ) = @_;
1701 if ($attributes) { # setter
1702 my $schema = $self->_result->result_source->schema;
1705 # Remove the existing one
1706 $self->extended_attributes->filter_by_branch_limitations->delete;
1708 # Insert the new ones
1710 for my $attribute (@$attributes) {
1711 $self->add_extended_attribute($attribute);
1712 $new_types->{$attribute->{code}} = 1;
1715 # Check globally mandatory types
1716 my @required_attribute_types =
1717 Koha::Patron::Attribute::Types->search(
1720 category_code => [ undef, $self->categorycode ],
1721 'borrower_attribute_types_branches.b_branchcode' =>
1724 { join => 'borrower_attribute_types_branches' }
1725 )->get_column('code');
1726 for my $type ( @required_attribute_types ) {
1727 Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute->throw(
1729 ) if !$new_types->{$type};
1735 my $rs = $self->_result->borrower_attributes;
1736 # We call search to use the filters in Koha::Patron::Attributes->search
1737 return Koha::Patron::Attributes->_new_from_dbic($rs)->search;
1742 my $messages = $patron->messages;
1744 Return the message attached to the patron.
1750 my $messages_rs = $self->_result->messages_borrowernumbers->search;
1751 return Koha::Patron::Messages->_new_from_dbic($messages_rs);
1756 Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
1758 Lock and optionally expire a patron account.
1759 Remove holds and article requests if remove flag set.
1760 In order to distinguish from locking by entering a wrong password, let's
1761 call this an administrative lockout.
1766 my ( $self, $params ) = @_;
1767 $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
1768 if( $params->{expire} ) {
1769 $self->dateexpiry( dt_from_string->subtract(days => 1) );
1772 if( $params->{remove} ) {
1773 $self->holds->delete;
1774 $self->article_requests->delete;
1781 Koha::Patrons->find($id)->anonymize;
1783 Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
1784 are randomized, other personal data is cleared too.
1785 Patrons with issues are skipped.
1791 if( $self->_result->issues->count ) {
1792 warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
1795 # Mandatory fields come from the corresponding pref, but email fields
1796 # are removed since scrambled email addresses only generate errors
1797 my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
1798 split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
1799 $mandatory->{userid} = 1; # needed since sub store does not clear field
1800 my @columns = $self->_result->result_source->columns;
1801 @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized|auth_method/ } @columns;
1802 push @columns, 'dateofbirth'; # add this date back in
1803 foreach my $col (@columns) {
1804 $self->_anonymize_column($col, $mandatory->{lc $col} );
1806 $self->anonymized(1)->store;
1809 sub _anonymize_column {
1810 my ( $self, $col, $mandatory ) = @_;
1811 my $col_info = $self->_result->result_source->column_info($col);
1812 my $type = $col_info->{data_type};
1813 my $nullable = $col_info->{is_nullable};
1815 if( $type =~ /char|text/ ) {
1817 ? Koha::Token->new->generate({ pattern => '\w{10}' })
1821 } elsif( $type =~ /integer|int$|float|dec|double/ ) {
1822 $val = $nullable ? undef : 0;
1823 } elsif( $type =~ /date|time/ ) {
1824 $val = $nullable ? undef : dt_from_string;
1829 =head3 add_guarantor
1831 my $relationship = $patron->add_guarantor(
1833 borrowernumber => $borrowernumber,
1834 relationships => $relationship,
1838 Adds a new guarantor to a patron.
1843 my ( $self, $params ) = @_;
1845 my $guarantor_id = $params->{guarantor_id};
1846 my $relationship = $params->{relationship};
1848 return Koha::Patron::Relationship->new(
1850 guarantee_id => $self->id,
1851 guarantor_id => $guarantor_id,
1852 relationship => $relationship
1857 =head3 get_extended_attribute
1859 my $attribute_value = $patron->get_extended_attribute( $code );
1861 Return the attribute for the code passed in parameter.
1863 It not exist it returns undef
1865 Note that this will not work for repeatable attribute types.
1867 Maybe you certainly not want to use this method, it is actually only used for SHOW_BARCODE
1868 (which should be a real patron's attribute (not extended)
1872 sub get_extended_attribute {
1873 my ( $self, $code, $value ) = @_;
1874 my $rs = $self->_result->borrower_attributes;
1876 my $attribute = $rs->search({ code => $code, ( $value ? ( attribute => $value ) : () ) });
1877 return unless $attribute->count;
1878 return $attribute->next;
1883 my $json = $patron->to_api;
1885 Overloaded method that returns a JSON representation of the Koha::Patron object,
1886 suitable for API output.
1891 my ( $self, $params ) = @_;
1893 my $json_patron = $self->SUPER::to_api( $params );
1895 $json_patron->{restricted} = ( $self->is_debarred )
1897 : Mojo::JSON->false;
1899 return $json_patron;
1902 =head3 to_api_mapping
1904 This method returns the mapping for representing a Koha::Patron object
1909 sub to_api_mapping {
1911 borrowernotes => 'staff_notes',
1912 borrowernumber => 'patron_id',
1913 branchcode => 'library_id',
1914 categorycode => 'category_id',
1915 checkprevcheckout => 'check_previous_checkout',
1916 contactfirstname => undef, # Unused
1917 contactname => undef, # Unused
1918 contactnote => 'altaddress_notes',
1919 contacttitle => undef, # Unused
1920 dateenrolled => 'date_enrolled',
1921 dateexpiry => 'expiry_date',
1922 dateofbirth => 'date_of_birth',
1923 debarred => undef, # replaced by 'restricted'
1924 debarredcomment => undef, # calculated, API consumers will use /restrictions instead
1925 emailpro => 'secondary_email',
1926 flags => undef, # permissions manipulation handled in /permissions
1927 gonenoaddress => 'incorrect_address',
1928 lastseen => 'last_seen',
1929 lost => 'patron_card_lost',
1930 opacnote => 'opac_notes',
1931 othernames => 'other_name',
1932 password => undef, # password manipulation handled in /password
1933 phonepro => 'secondary_phone',
1934 relationship => 'relationship_type',
1936 smsalertnumber => 'sms_number',
1937 sort1 => 'statistics_1',
1938 sort2 => 'statistics_2',
1939 autorenew_checkouts => 'autorenew_checkouts',
1940 streetnumber => 'street_number',
1941 streettype => 'street_type',
1942 zipcode => 'postal_code',
1943 B_address => 'altaddress_address',
1944 B_address2 => 'altaddress_address2',
1945 B_city => 'altaddress_city',
1946 B_country => 'altaddress_country',
1947 B_email => 'altaddress_email',
1948 B_phone => 'altaddress_phone',
1949 B_state => 'altaddress_state',
1950 B_streetnumber => 'altaddress_street_number',
1951 B_streettype => 'altaddress_street_type',
1952 B_zipcode => 'altaddress_postal_code',
1953 altcontactaddress1 => 'altcontact_address',
1954 altcontactaddress2 => 'altcontact_address2',
1955 altcontactaddress3 => 'altcontact_city',
1956 altcontactcountry => 'altcontact_country',
1957 altcontactfirstname => 'altcontact_firstname',
1958 altcontactphone => 'altcontact_phone',
1959 altcontactsurname => 'altcontact_surname',
1960 altcontactstate => 'altcontact_state',
1961 altcontactzipcode => 'altcontact_postal_code',
1962 password_expiration_date => undef,
1963 primary_contact_method => undef,
1965 auth_method => undef,
1971 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_name => 'DUE'});
1972 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports });
1973 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports, test_mode => 1 });
1975 Queue messages to a patron. Can pass a message that is part of the message_attributes
1976 table or supply the transport to use.
1978 If passed a message name we retrieve the patrons preferences for transports
1979 Otherwise we use the supplied transport. In the case of email or sms we fall back to print if
1980 we have no address/number for sending
1982 $letter_params is a hashref of the values to be passed to GetPreparedLetter
1984 test_mode will only report which notices would be sent, but nothing will be queued
1989 my ( $self, $params ) = @_;
1990 my $letter_params = $params->{letter_params};
1991 my $test_mode = $params->{test_mode};
1993 return unless $letter_params;
1994 return unless exists $params->{message_name} xor $params->{message_transports}; # We only want one of these
1996 my $library = Koha::Libraries->find( $letter_params->{branchcode} );
1997 my $from_email_address = $library->from_email_address;
1999 my @message_transports;
2001 $letter_code = $letter_params->{letter_code};
2002 if( $params->{message_name} ){
2003 my $messaging_prefs = C4::Members::Messaging::GetMessagingPreferences( {
2004 borrowernumber => $letter_params->{borrowernumber},
2005 message_name => $params->{message_name}
2007 @message_transports = ( keys %{ $messaging_prefs->{transports} } );
2008 $letter_code = $messaging_prefs->{transports}->{$message_transports[0]} unless $letter_code;
2010 @message_transports = @{$params->{message_transports}};
2012 return unless defined $letter_code;
2013 $letter_params->{letter_code} = $letter_code;
2016 foreach my $mtt (@message_transports){
2017 next if ($mtt eq 'itiva' and C4::Context->preference('TalkingTechItivaPhoneNotification') );
2018 # Notice is handled by TalkingTech_itiva_outbound.pl
2019 if ( ( $mtt eq 'email' and not $self->notice_email_address )
2020 or ( $mtt eq 'sms' and not $self->smsalertnumber )
2021 or ( $mtt eq 'phone' and not $self->phone ) )
2023 push @{ $return{fallback} }, $mtt;
2026 next if $mtt eq 'print' && $print_sent;
2027 $letter_params->{message_transport_type} = $mtt;
2028 my $letter = C4::Letters::GetPreparedLetter( %$letter_params );
2029 C4::Letters::EnqueueLetter({
2031 borrowernumber => $self->borrowernumber,
2032 from_address => $from_email_address,
2033 message_transport_type => $mtt
2034 }) unless $test_mode;
2035 push @{$return{sent}}, $mtt;
2036 $print_sent = 1 if $mtt eq 'print';
2041 =head3 safe_to_delete
2043 my $result = $patron->safe_to_delete;
2044 if ( $result eq 'has_guarantees' ) { ... }
2045 elsif ( $result ) { ... }
2046 else { # cannot delete }
2048 This method tells if the Koha:Patron object can be deleted. Possible return values
2054 =item 'has_checkouts'
2058 =item 'has_guarantees'
2060 =item 'is_anonymous_patron'
2066 sub safe_to_delete {
2069 my $anonymous_patron = C4::Context->preference('AnonymousPatron');
2073 if ( $anonymous_patron && $self->id eq $anonymous_patron ) {
2074 $error = 'is_anonymous_patron';
2076 elsif ( $self->checkouts->count ) {
2077 $error = 'has_checkouts';
2079 elsif ( $self->account->outstanding_debits->total_outstanding > 0 ) {
2080 $error = 'has_debt';
2082 elsif ( $self->guarantee_relationships->count ) {
2083 $error = 'has_guarantees';
2087 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
2090 return Koha::Result::Boolean->new(1);
2095 my $recalls = $patron->recalls;
2097 Return the patron's recalls.
2104 return Koha::Recalls->search({ patron_id => $self->borrowernumber });
2107 =head3 account_balance
2109 my $balance = $patron->account_balance
2111 Return the patron's account balance
2115 sub account_balance {
2117 return $self->account->balance;
2121 =head3 has_messaging_preference
2123 my $bool = $patron->has_messaging_preference({
2124 message_name => $message_name, # A value from message_attributes.message_name
2125 message_transport_type => $message_transport_type, # email, sms, phone, itiva, etc...
2126 wants_digest => $wants_digest, # 1 if you are looking for the digest version, don't pass if you just want either
2131 sub has_messaging_preference {
2132 my ( $self, $params ) = @_;
2134 my $message_name = $params->{message_name};
2135 my $message_transport_type = $params->{message_transport_type};
2136 my $wants_digest = $params->{wants_digest};
2138 return $self->_result->search_related_rs(
2139 'borrower_message_preferences',
2143 [ 'borrower_message_transport_preferences', 'message_attribute' ]
2148 =head3 can_patron_change_staff_only_lists
2150 $patron->can_patron_change_staff_only_lists;
2152 Return 1 if a patron has 'Superlibrarian' or 'Catalogue' permission.
2153 Otherwise, return 0.
2157 sub can_patron_change_staff_only_lists {
2158 my ( $self, $params ) = @_;
2159 return 1 if C4::Auth::haspermission( $self->userid, { 'catalogue' => 1 });
2163 =head3 encode_secret
2165 $patron->encode_secret($secret32);
2167 Secret (TwoFactorAuth expects it in base32 format) is encrypted.
2168 You still need to call ->store.
2173 my ( $self, $secret ) = @_;
2175 return $self->secret( Koha::Encryption->new->encrypt_hex($secret) );
2177 return $self->secret($secret);
2180 =head3 decoded_secret
2182 my $secret32 = $patron->decoded_secret;
2184 Decode the patron secret. We expect to get back a base32 string, but this
2185 is not checked here. Caller of encode_secret is responsible for that.
2189 sub decoded_secret {
2191 if( $self->secret ) {
2192 return Koha::Encryption->new->decrypt_hex( $self->secret );
2194 return $self->secret;
2197 =head2 Internal methods
2209 Kyle M Hall <kyle@bywatersolutions.com>
2210 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
2211 Martin Renvoize <martin.renvoize@ptfs-europe.com>