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 );
39 use Koha::Exceptions::Password;
41 use Koha::Old::Checkouts;
42 use Koha::Patron::Attributes;
43 use Koha::Patron::Categories;
44 use Koha::Patron::Debarments;
45 use Koha::Patron::HouseboundProfile;
46 use Koha::Patron::HouseboundRole;
47 use Koha::Patron::Images;
48 use Koha::Patron::Messages;
49 use Koha::Patron::Modifications;
50 use Koha::Patron::Relationships;
54 use Koha::Result::Boolean;
55 use Koha::Subscription::Routinglists;
57 use Koha::Virtualshelves;
59 use base qw(Koha::Object);
61 use constant ADMINISTRATIVE_LOCKOUT => -1;
63 our $RESULTSET_PATRON_ID_MAPPING = {
64 Accountline => 'borrowernumber',
65 Aqbasketuser => 'borrowernumber',
66 Aqbudget => 'budget_owner_id',
67 Aqbudgetborrower => 'borrowernumber',
68 ArticleRequest => 'borrowernumber',
69 BorrowerDebarment => 'borrowernumber',
70 BorrowerFile => 'borrowernumber',
71 BorrowerModification => 'borrowernumber',
72 ClubEnrollment => 'borrowernumber',
73 Issue => 'borrowernumber',
74 ItemsLastBorrower => 'borrowernumber',
75 Linktracker => 'borrowernumber',
76 Message => 'borrowernumber',
77 MessageQueue => 'borrowernumber',
78 OldIssue => 'borrowernumber',
79 OldReserve => 'borrowernumber',
80 Rating => 'borrowernumber',
81 Reserve => 'borrowernumber',
82 Review => 'borrowernumber',
83 SearchHistory => 'userid',
84 Statistic => 'borrowernumber',
85 Suggestion => 'suggestedby',
86 TagAll => 'borrowernumber',
87 Virtualshelfcontent => 'borrowernumber',
88 Virtualshelfshare => 'borrowernumber',
89 Virtualshelve => 'owner',
94 Koha::Patron - Koha Patron Object class
105 my ( $class, $params ) = @_;
107 return $class->SUPER::new($params);
110 =head3 fixup_cardnumber
112 Autogenerate next cardnumber from highest value found in database
116 sub fixup_cardnumber {
119 my $max = $self->cardnumber;
120 Koha::Plugins->call( 'patron_barcode_transform', \$max );
122 $max ||= Koha::Patrons->search({
123 cardnumber => {-regexp => '^-?[0-9]+$'}
125 select => \'CAST(cardnumber AS SIGNED)',
126 as => ['cast_cardnumber']
127 })->_resultset->get_column('cast_cardnumber')->max;
128 $self->cardnumber(($max || 0) +1);
131 =head3 trim_whitespace
133 trim whitespace from data which has some non-whitespace in it.
134 Could be moved to Koha::Object if need to be reused
138 sub trim_whitespaces {
141 my $schema = Koha::Database->new->schema;
142 my @columns = $schema->source($self->_type)->columns;
144 for my $column( @columns ) {
145 my $value = $self->$column;
146 if ( defined $value ) {
147 $value =~ s/^\s*|\s*$//g;
148 $self->$column($value);
154 =head3 plain_text_password
156 $patron->plain_text_password( $password );
158 stores a copy of the unencrypted password in the object
159 for use in code before encrypting for db
163 sub plain_text_password {
164 my ( $self, $password ) = @_;
166 $self->{_plain_text_password} = $password;
169 return $self->{_plain_text_password}
170 if $self->{_plain_text_password};
177 Patron specific store method to cleanup record
178 and do other necessary things before saving
186 $self->_result->result_source->schema->txn_do(
189 C4::Context->preference("autoMemberNum")
190 and ( not defined $self->cardnumber
191 or $self->cardnumber eq '' )
194 # Warning: The caller is responsible for locking the members table in write
195 # mode, to avoid database corruption.
196 # We are in a transaction but the table is not locked
197 $self->fixup_cardnumber;
200 unless( $self->category->in_storage ) {
201 Koha::Exceptions::Object::FKConstraint->throw(
202 broken_fk => 'categorycode',
203 value => $self->categorycode,
207 $self->trim_whitespaces;
209 my $new_cardnumber = $self->cardnumber;
210 Koha::Plugins->call( 'patron_barcode_transform', \$new_cardnumber );
211 $self->cardnumber( $new_cardnumber );
213 # Set surname to uppercase if uppercasesurname is true
214 $self->surname( uc($self->surname) )
215 if C4::Context->preference("uppercasesurnames");
217 $self->relationship(undef) # We do not want to store an empty string in this field
218 if defined $self->relationship
219 and $self->relationship eq "";
221 unless ( $self->in_storage ) { #AddMember
223 # Generate a valid userid/login if needed
224 $self->generate_userid
225 if not $self->userid or not $self->has_valid_userid;
227 # Add expiration date if it isn't already there
228 unless ( $self->dateexpiry ) {
229 $self->dateexpiry( $self->category->get_expiry_date );
232 # Add enrollment date if it isn't already there
233 unless ( $self->dateenrolled ) {
234 $self->dateenrolled(dt_from_string);
237 # Set the privacy depending on the patron's category
238 my $default_privacy = $self->category->default_privacy || q{};
240 $default_privacy eq 'default' ? 1
241 : $default_privacy eq 'never' ? 2
242 : $default_privacy eq 'forever' ? 0
244 $self->privacy($default_privacy);
246 # Call any check_password plugins if password is passed
247 if ( C4::Context->config("enable_plugins") && $self->password ) {
248 my @plugins = Koha::Plugins->new()->GetPlugins({
249 method => 'check_password',
251 foreach my $plugin ( @plugins ) {
252 # This plugin hook will also be used by a plugin for the Norwegian national
253 # patron database. This is why we need to pass both the password and the
254 # borrowernumber to the plugin.
255 my $ret = $plugin->check_password(
257 password => $self->password,
258 borrowernumber => $self->borrowernumber
261 if ( $ret->{'error'} == 1 ) {
262 Koha::Exceptions::Password::Plugin->throw();
267 # Make a copy of the plain text password for later use
268 $self->plain_text_password( $self->password );
270 # Create a disabled account if no password provided
271 $self->password( $self->password
272 ? Koha::AuthUtils::hash_password( $self->password )
275 $self->borrowernumber(undef);
277 $self = $self->SUPER::store;
279 $self->add_enrolment_fee_if_needed(0);
281 logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
282 if C4::Context->preference("BorrowersLog");
286 my $self_from_storage = $self->get_from_storage;
287 # FIXME We should not deal with that here, callers have to do this job
288 # Moved from ModMember to prevent regressions
289 unless ( $self->userid ) {
290 my $stored_userid = $self_from_storage->userid;
291 $self->userid($stored_userid);
294 # Password must be updated using $self->set_password
295 $self->password($self_from_storage->password);
297 if ( $self->category->categorycode ne
298 $self_from_storage->category->categorycode )
300 # Add enrolement fee on category change if required
301 $self->add_enrolment_fee_if_needed(1)
302 if C4::Context->preference('FeeOnChangePatronCategory');
304 # Clean up guarantors on category change if required
305 $self->guarantor_relationships->delete
306 if ( $self->category->category_type ne 'C'
307 && $self->category->category_type ne 'P' );
312 if ( C4::Context->preference("BorrowersLog") ) {
314 my $from_storage = $self_from_storage->unblessed;
315 my $from_object = $self->unblessed;
316 my @skip_fields = (qw/lastseen updated_on/);
317 for my $key ( keys %{$from_storage} ) {
318 next if any { /$key/ } @skip_fields;
321 !defined( $from_storage->{$key} )
322 && defined( $from_object->{$key} )
324 || ( defined( $from_storage->{$key} )
325 && !defined( $from_object->{$key} ) )
327 defined( $from_storage->{$key} )
328 && defined( $from_object->{$key} )
329 && ( $from_storage->{$key} ne
330 $from_object->{$key} )
335 before => $from_storage->{$key},
336 after => $from_object->{$key}
341 if ( defined($info) ) {
345 $self->borrowernumber,
348 { utf8 => 1, pretty => 1, canonical => 1 }
355 $self = $self->SUPER::store;
366 Delete patron's holds, lists and finally the patron.
368 Lists owned by the borrower are deleted, but entries from the borrower to
369 other lists are kept.
376 my $anonymous_patron = C4::Context->preference("AnonymousPatron");
377 Koha::Exceptions::Patron::FailedDeleteAnonymousPatron->throw() if $anonymous_patron && $self->id eq $anonymous_patron;
379 $self->_result->result_source->schema->txn_do(
381 # Cancel Patron's holds
382 my $holds = $self->holds;
383 while( my $hold = $holds->next ){
387 # Delete all lists and all shares of this borrower
388 # Consistent with the approach Koha uses on deleting individual lists
389 # Note that entries in virtualshelfcontents added by this borrower to
390 # lists of others will be handled by a table constraint: the borrower
391 # is set to NULL in those entries.
393 # We could handle the above deletes via a constraint too.
394 # But a new BZ report 11889 has been opened to discuss another approach.
395 # Instead of deleting we could also disown lists (based on a pref).
396 # In that way we could save shared and public lists.
397 # The current table constraints support that idea now.
398 # This pref should then govern the results of other routines/methods such as
399 # Koha::Virtualshelf->new->delete too.
400 # FIXME Could be $patron->get_lists
401 $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } )->as_list;
403 # We cannot have a FK on borrower_modifications.borrowernumber, the table is also used
405 $_->delete for Koha::Patron::Modifications->search( { borrowernumber => $self->borrowernumber } )->as_list;
407 $self->SUPER::delete;
409 logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
418 my $patron_category = $patron->category
420 Return the patron category for this patron
426 return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
436 return Koha::Patron::Images->find( $self->borrowernumber );
441 Returns a Koha::Library object representing the patron's home library.
447 return Koha::Library->_new_from_dbic($self->_result->branchcode);
452 Returns a Koha::SMS::Provider object representing the patron's SMS provider.
458 my $sms_provider_rs = $self->_result->sms_provider;
459 return unless $sms_provider_rs;
460 return Koha::SMS::Provider->_new_from_dbic($sms_provider_rs);
463 =head3 guarantor_relationships
465 Returns Koha::Patron::Relationships object for this patron's guarantors
467 Returns the set of relationships for the patrons that are guarantors for this patron.
469 This is returned instead of a Koha::Patron object because the guarantor
470 may not exist as a patron in Koha. If this is true, the guarantors name
471 exists in the Koha::Patron::Relationship object and will have no guarantor_id.
475 sub guarantor_relationships {
478 return Koha::Patron::Relationships->search( { guarantee_id => $self->id } );
481 =head3 guarantee_relationships
483 Returns Koha::Patron::Relationships object for this patron's guarantors
485 Returns the set of relationships for the patrons that are guarantees for this patron.
487 The method returns Koha::Patron::Relationship objects for the sake
488 of consistency with the guantors method.
489 A guarantee by definition must exist as a patron in Koha.
493 sub guarantee_relationships {
496 return Koha::Patron::Relationships->search(
497 { guarantor_id => $self->id },
499 prefetch => 'guarantee',
500 order_by => { -asc => [ 'guarantee.surname', 'guarantee.firstname' ] },
505 =head3 relationships_debt
507 Returns the amount owed by the patron's guarantors *and* the other guarantees of those guarantors
511 sub relationships_debt {
512 my ($self, $params) = @_;
514 my $include_guarantors = $params->{include_guarantors};
515 my $only_this_guarantor = $params->{only_this_guarantor};
516 my $include_this_patron = $params->{include_this_patron};
519 if ( $only_this_guarantor ) {
520 @guarantors = $self->guarantee_relationships->count ? ( $self ) : ();
521 Koha::Exceptions::BadParameter->throw( { parameter => 'only_this_guarantor' } ) unless @guarantors;
522 } elsif ( $self->guarantor_relationships->count ) {
523 # I am a guarantee, just get all my guarantors
524 @guarantors = $self->guarantor_relationships->guarantors->as_list;
526 # I am a guarantor, I need to get all the guarantors of all my guarantees
527 @guarantors = map { $_->guarantor_relationships->guarantors->as_list } $self->guarantee_relationships->guarantees->as_list;
530 my $non_issues_charges = 0;
531 my $seen = $include_this_patron ? {} : { $self->id => 1 }; # For tracking members already added to the total
532 foreach my $guarantor (@guarantors) {
533 $non_issues_charges += $guarantor->account->non_issues_charges if $include_guarantors && !$seen->{ $guarantor->id };
535 # We've added what the guarantor owes, not added in that guarantor's guarantees as well
536 my @guarantees = map { $_->guarantee } $guarantor->guarantee_relationships->as_list;
537 my $guarantees_non_issues_charges = 0;
538 foreach my $guarantee (@guarantees) {
539 next if $seen->{ $guarantee->id };
540 $guarantees_non_issues_charges += $guarantee->account->non_issues_charges;
541 # Mark this guarantee as seen so we don't double count a guarantee linked to multiple guarantors
542 $seen->{ $guarantee->id } = 1;
545 $non_issues_charges += $guarantees_non_issues_charges;
546 $seen->{ $guarantor->id } = 1;
549 return $non_issues_charges;
552 =head3 housebound_profile
554 Returns the HouseboundProfile associated with this patron.
558 sub housebound_profile {
560 my $profile = $self->_result->housebound_profile;
561 return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
566 =head3 housebound_role
568 Returns the HouseboundRole associated with this patron.
572 sub housebound_role {
575 my $role = $self->_result->housebound_role;
576 return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
582 Returns the siblings of this patron.
589 my @guarantors = $self->guarantor_relationships()->guarantors()->as_list;
591 return unless @guarantors;
594 map { $_->guarantee_relationships()->guarantees()->as_list } @guarantors;
596 return unless @siblings;
600 grep { !$seen{ $_->id }++ && ( $_->id != $self->id ) } @siblings;
602 return Koha::Patrons->search( { borrowernumber => { -in => [ map { $_->id } @siblings ] } } );
607 my $patron = Koha::Patrons->find($id);
608 $patron->merge_with( \@patron_ids );
610 This subroutine merges a list of patrons into the patron record. This is accomplished by finding
611 all related patron ids for the patrons to be merged in other tables and changing the ids to be that
612 of the keeper patron.
617 my ( $self, $patron_ids ) = @_;
619 my $anonymous_patron = C4::Context->preference("AnonymousPatron");
620 return if $anonymous_patron && $self->id eq $anonymous_patron;
622 my @patron_ids = @{ $patron_ids };
624 # Ensure the keeper isn't in the list of patrons to merge
625 @patron_ids = grep { $_ ne $self->id } @patron_ids;
627 my $schema = Koha::Database->new()->schema();
631 $self->_result->result_source->schema->txn_do( sub {
632 foreach my $patron_id (@patron_ids) {
634 next if $patron_id eq $anonymous_patron;
636 my $patron = Koha::Patrons->find( $patron_id );
640 # Unbless for safety, the patron will end up being deleted
641 $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
643 my $attributes = $patron->extended_attributes;
644 my $new_attributes = [
645 map { { code => $_->code, attribute => $_->attribute } }
648 $attributes->delete; # We need to delete before trying to merge them to prevent exception on unique and repeatable
649 for my $attribute ( @$new_attributes ) {
651 $self->add_extended_attribute($attribute);
653 # Don't block the merge if there is a non-repeatable attribute that cannot be added to the current patron.
654 unless ( $_->isa('Koha::Exceptions::Patron::Attribute::NonRepeatable') ) {
660 while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
661 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
662 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
663 $rs->update({ $field => $self->id });
664 if ( $r eq 'BorrowerDebarment' ) {
665 Koha::Patron::Debarments::UpdateBorrowerDebarmentFlags($self->id);
669 $patron->move_to_deleted();
679 =head3 wants_check_for_previous_checkout
681 $wants_check = $patron->wants_check_for_previous_checkout;
683 Return 1 if Koha needs to perform PrevIssue checking, else 0.
687 sub wants_check_for_previous_checkout {
689 my $syspref = C4::Context->preference("checkPrevCheckout");
692 ## Hard syspref trumps all
693 return 1 if ($syspref eq 'hardyes');
694 return 0 if ($syspref eq 'hardno');
695 ## Now, patron pref trumps all
696 return 1 if ($self->checkprevcheckout eq 'yes');
697 return 0 if ($self->checkprevcheckout eq 'no');
699 # More complex: patron inherits -> determine category preference
700 my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
701 return 1 if ($checkPrevCheckoutByCat eq 'yes');
702 return 0 if ($checkPrevCheckoutByCat eq 'no');
704 # Finally: category preference is inherit, default to 0
705 if ($syspref eq 'softyes') {
712 =head3 do_check_for_previous_checkout
714 $do_check = $patron->do_check_for_previous_checkout($item);
716 Return 1 if the bib associated with $ITEM has previously been checked out to
717 $PATRON, 0 otherwise.
721 sub do_check_for_previous_checkout {
722 my ( $self, $item ) = @_;
725 my $biblio = Koha::Biblios->find( $item->{biblionumber} );
726 if ( $biblio->is_serial ) {
727 push @item_nos, $item->{itemnumber};
729 # Get all itemnumbers for given bibliographic record.
730 @item_nos = $biblio->items->get_column( 'itemnumber' );
733 # Create (old)issues search criteria
735 borrowernumber => $self->borrowernumber,
736 itemnumber => \@item_nos,
739 my $delay = C4::Context->preference('CheckPrevCheckoutDelay') || 0;
741 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
742 my $newer_than = dt_from_string()->subtract( days => $delay );
743 $criteria->{'returndate'} = { '>' => $dtf->format_datetime($newer_than), };
746 # Check current issues table
747 my $issues = Koha::Checkouts->search($criteria);
748 return 1 if $issues->count; # 0 || N
750 # Check old issues table
751 my $old_issues = Koha::Old::Checkouts->search($criteria);
752 return $old_issues->count; # 0 || N
757 my $debarment_expiration = $patron->is_debarred;
759 Returns the date a patron debarment will expire, or undef if the patron is not
767 return unless $self->debarred;
768 return $self->debarred
769 if $self->debarred =~ '^9999'
770 or dt_from_string( $self->debarred ) > dt_from_string;
776 my $is_expired = $patron->is_expired;
778 Returns 1 if the patron is expired or 0;
784 return 0 unless $self->dateexpiry;
785 return 0 if $self->dateexpiry =~ '^9999';
786 return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
790 =head3 is_going_to_expire
792 my $is_going_to_expire = $patron->is_going_to_expire;
794 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
798 sub is_going_to_expire {
801 my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
803 return 0 unless $delay;
804 return 0 unless $self->dateexpiry;
805 return 0 if $self->dateexpiry =~ '^9999';
806 return 1 if dt_from_string( $self->dateexpiry, undef, 'floating' )->subtract( days => $delay ) < dt_from_string(undef, undef, 'floating')->truncate( to => 'day' );
812 $patron->set_password({ password => $plain_text_password [, skip_validation => 1 ] });
814 Set the patron's password.
818 The passed string is validated against the current password enforcement policy.
819 Validation can be skipped by passing the I<skip_validation> parameter.
821 Exceptions are thrown if the password is not good enough.
825 =item Koha::Exceptions::Password::TooShort
827 =item Koha::Exceptions::Password::WhitespaceCharacters
829 =item Koha::Exceptions::Password::TooWeak
831 =item Koha::Exceptions::Password::Plugin (if a "check password" plugin is enabled)
838 my ( $self, $args ) = @_;
840 my $password = $args->{password};
842 unless ( $args->{skip_validation} ) {
843 my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password, $self->category );
846 if ( $error eq 'too_short' ) {
847 my $min_length = $self->category->effective_min_password_length;
848 $min_length = 3 if not $min_length or $min_length < 3;
850 my $password_length = length($password);
851 Koha::Exceptions::Password::TooShort->throw(
852 length => $password_length, min_length => $min_length );
854 elsif ( $error eq 'has_whitespaces' ) {
855 Koha::Exceptions::Password::WhitespaceCharacters->throw();
857 elsif ( $error eq 'too_weak' ) {
858 Koha::Exceptions::Password::TooWeak->throw();
863 if ( C4::Context->config("enable_plugins") ) {
864 # Call any check_password plugins
865 my @plugins = Koha::Plugins->new()->GetPlugins({
866 method => 'check_password',
868 foreach my $plugin ( @plugins ) {
869 # This plugin hook will also be used by a plugin for the Norwegian national
870 # patron database. This is why we need to pass both the password and the
871 # borrowernumber to the plugin.
872 my $ret = $plugin->check_password(
874 password => $password,
875 borrowernumber => $self->borrowernumber
878 # This plugin hook will also be used by a plugin for the Norwegian national
879 # patron database. This is why we need to call the actual plugins and then
880 # check skip_validation afterwards.
881 if ( $ret->{'error'} == 1 && !$args->{skip_validation} ) {
882 Koha::Exceptions::Password::Plugin->throw();
887 my $digest = Koha::AuthUtils::hash_password($password);
889 # We do not want to call $self->store and retrieve password from DB
890 $self->password($digest);
891 $self->login_attempts(0);
894 logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" )
895 if C4::Context->preference("BorrowersLog");
903 my $new_expiry_date = $patron->renew_account
905 Extending the subscription to the expiry date.
912 if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
913 $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
916 C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
917 ? dt_from_string( $self->dateexpiry )
920 my $expiry_date = $self->category->get_expiry_date($date);
922 $self->dateexpiry($expiry_date);
923 $self->date_renewed( dt_from_string() );
926 $self->add_enrolment_fee_if_needed(1);
928 logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
929 return dt_from_string( $expiry_date )->truncate( to => 'day' );
934 my $has_overdues = $patron->has_overdues;
936 Returns the number of patron's overdues
942 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
943 return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
948 $patron->track_login;
949 $patron->track_login({ force => 1 });
951 Tracks a (successful) login attempt.
952 The preference TrackLastPatronActivity must be enabled. Or you
953 should pass the force parameter.
958 my ( $self, $params ) = @_;
961 !C4::Context->preference('TrackLastPatronActivity');
962 $self->lastseen( dt_from_string() )->store;
965 =head3 move_to_deleted
967 my $is_moved = $patron->move_to_deleted;
969 Move a patron to the deletedborrowers table.
970 This can be done before deleting a patron, to make sure the data are not completely deleted.
974 sub move_to_deleted {
976 my $patron_infos = $self->unblessed;
977 delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
978 return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
981 =head3 can_request_article
983 if ( $patron->can_request_article( $library->id ) ) { ... }
985 Returns true if the patron can request articles. As limits apply for the patron
986 on the same day, those completed the same day are considered as current.
988 A I<library_id> can be passed as parameter, falling back to userenv if absent.
992 sub can_request_article {
993 my ($self, $library_id) = @_;
995 $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
997 my $rule = Koha::CirculationRules->get_effective_rule(
999 branchcode => $library_id,
1000 categorycode => $self->categorycode,
1001 rule_name => 'open_article_requests_limit'
1005 my $limit = ($rule) ? $rule->rule_value : undef;
1007 return 1 unless defined $limit;
1009 my $count = Koha::ArticleRequests->search(
1010 [ { borrowernumber => $self->borrowernumber, status => [ 'REQUESTED', 'PENDING', 'PROCESSING' ] },
1011 { borrowernumber => $self->borrowernumber, status => 'COMPLETED', updated_on => { '>=' => \'CAST(NOW() AS DATE)' } },
1014 return $count < $limit ? 1 : 0;
1017 =head3 article_request_fee
1019 my $fee = $patron->article_request_fee(
1021 [ library_id => $library->id, ]
1025 Returns the fee to be charged to the patron when it places an article request.
1027 A I<library_id> can be passed as parameter, falling back to userenv if absent.
1031 sub article_request_fee {
1032 my ($self, $params) = @_;
1034 my $library_id = $params->{library_id};
1036 $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1038 my $rule = Koha::CirculationRules->get_effective_rule(
1040 branchcode => $library_id,
1041 categorycode => $self->categorycode,
1042 rule_name => 'article_request_fee'
1046 my $fee = ($rule) ? $rule->rule_value + 0 : 0;
1051 =head3 add_article_request_fee_if_needed
1053 my $fee = $patron->add_article_request_fee_if_needed(
1055 [ item_id => $item->id,
1056 library_id => $library->id, ]
1060 If an article request fee needs to be charged, it adds a debit to the patron's
1063 Returns the fee line.
1065 A I<library_id> can be passed as parameter, falling back to userenv if absent.
1069 sub add_article_request_fee_if_needed {
1070 my ($self, $params) = @_;
1072 my $library_id = $params->{library_id};
1073 my $item_id = $params->{item_id};
1075 $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1077 my $amount = $self->article_request_fee(
1079 library_id => $library_id,
1085 if ( $amount > 0 ) {
1086 $debit_line = $self->account->add_debit(
1089 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
1090 interface => C4::Context->interface,
1091 library_id => $library_id,
1092 type => 'ARTICLE_REQUEST',
1093 item_id => $item_id,
1101 =head3 article_requests
1103 my $article_requests = $patron->article_requests;
1105 Returns the patron article requests.
1109 sub article_requests {
1112 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
1115 =head3 add_enrolment_fee_if_needed
1117 my $enrolment_fee = $patron->add_enrolment_fee_if_needed($renewal);
1119 Add enrolment fee for a patron if needed.
1121 $renewal - boolean denoting whether this is an account renewal or not
1125 sub add_enrolment_fee_if_needed {
1126 my ($self, $renewal) = @_;
1127 my $enrolment_fee = $self->category->enrolmentfee;
1128 if ( $enrolment_fee && $enrolment_fee > 0 ) {
1129 my $type = $renewal ? 'ACCOUNT_RENEW' : 'ACCOUNT';
1130 $self->account->add_debit(
1132 amount => $enrolment_fee,
1133 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
1134 interface => C4::Context->interface,
1135 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
1140 return $enrolment_fee || 0;
1145 my $checkouts = $patron->checkouts
1151 my $checkouts = $self->_result->issues;
1152 return Koha::Checkouts->_new_from_dbic( $checkouts );
1155 =head3 pending_checkouts
1157 my $pending_checkouts = $patron->pending_checkouts
1159 This method will return the same as $self->checkouts, but with a prefetch on
1160 items, biblio and biblioitems.
1162 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
1164 It should not be used directly, prefer to access fields you need instead of
1165 retrieving all these fields in one go.
1169 sub pending_checkouts {
1171 my $checkouts = $self->_result->issues->search(
1175 { -desc => 'me.timestamp' },
1176 { -desc => 'issuedate' },
1177 { -desc => 'issue_id' }, # Sort by issue_id should be enough
1179 prefetch => { item => { biblio => 'biblioitems' } },
1182 return Koha::Checkouts->_new_from_dbic( $checkouts );
1185 =head3 old_checkouts
1187 my $old_checkouts = $patron->old_checkouts
1193 my $old_checkouts = $self->_result->old_issues;
1194 return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
1199 my $overdue_items = $patron->get_overdues
1201 Return the overdue items
1207 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
1208 return $self->checkouts->search(
1210 'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
1213 prefetch => { item => { biblio => 'biblioitems' } },
1218 sub overdues { my $self = shift; return $self->get_overdues(@_); }
1220 =head3 get_routing_lists
1222 my $routinglists = $patron->get_routing_lists
1224 Returns the routing lists a patron is subscribed to.
1228 sub get_routing_lists {
1230 my $routing_list_rs = $self->_result->subscriptionroutinglists;
1231 return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
1236 my $age = $patron->get_age
1238 Return the age of the patron
1245 return unless $self->dateofbirth;
1247 my $date_of_birth = dt_from_string( $self->dateofbirth );
1248 my $today = dt_from_string->truncate( to => 'day' );
1250 return $today->subtract_datetime( $date_of_birth )->years;
1255 my $is_valid = $patron->is_valid_age
1257 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
1263 my $age = $self->get_age;
1265 my $patroncategory = $self->category;
1266 my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
1268 return (defined($age) && (($high && ($age > $high)) or ($low && ($age < $low)))) ? 0 : 1;
1273 my $account = $patron->account
1279 return Koha::Account->new( { patron_id => $self->borrowernumber } );
1284 my $holds = $patron->holds
1286 Return all the holds placed by this patron
1292 my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1293 return Koha::Holds->_new_from_dbic($holds_rs);
1298 my $old_holds = $patron->old_holds
1300 Return all the historical holds for this patron
1306 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1307 return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1310 =head3 return_claims
1312 my $return_claims = $patron->return_claims
1318 my $return_claims = $self->_result->return_claims_borrowernumbers;
1319 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $return_claims );
1322 =head3 notice_email_address
1324 my $email = $patron->notice_email_address;
1326 Return the email address of patron used for notices.
1327 Returns the empty string if no email address.
1331 sub notice_email_address{
1334 my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1335 # if syspref is set to 'first valid' (value == OFF), look up email address
1336 if ( $which_address eq 'OFF' ) {
1337 return $self->first_valid_email_address;
1340 return $self->$which_address || '';
1343 =head3 first_valid_email_address
1345 my $first_valid_email_address = $patron->first_valid_email_address
1347 Return the first valid email address for a patron.
1348 For now, the order is defined as email, emailpro, B_email.
1349 Returns the empty string if the borrower has no email addresses.
1353 sub first_valid_email_address {
1356 return $self->email() || $self->emailpro() || $self->B_email() || q{};
1359 =head3 get_club_enrollments
1363 sub get_club_enrollments {
1366 return Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1369 =head3 get_enrollable_clubs
1373 sub get_enrollable_clubs {
1374 my ( $self, $is_enrollable_from_opac ) = @_;
1377 $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1378 if $is_enrollable_from_opac;
1379 $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1381 $params->{borrower} = $self;
1383 return Koha::Clubs->get_enrollable($params);
1386 =head3 account_locked
1388 my $is_locked = $patron->account_locked
1390 Return true if the patron has reached the maximum number of login attempts
1391 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1392 as an administrative lockout (independent of FailedLoginAttempts; see also
1393 Koha::Patron->lock).
1394 Otherwise return false.
1395 If the pref is not set (empty string, null or 0), the feature is considered as
1400 sub account_locked {
1402 my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1403 return 1 if $FailedLoginAttempts
1404 and $self->login_attempts
1405 and $self->login_attempts >= $FailedLoginAttempts;
1406 return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1410 =head3 can_see_patron_infos
1412 my $can_see = $patron->can_see_patron_infos( $patron );
1414 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1418 sub can_see_patron_infos {
1419 my ( $self, $patron ) = @_;
1420 return unless $patron;
1421 return $self->can_see_patrons_from( $patron->branchcode );
1424 =head3 can_see_patrons_from
1426 my $can_see = $patron->can_see_patrons_from( $branchcode );
1428 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1432 sub can_see_patrons_from {
1433 my ( $self, $branchcode ) = @_;
1435 if ( $self->branchcode eq $branchcode ) {
1437 } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1439 } elsif ( my $library_groups = $self->library->library_groups ) {
1440 while ( my $library_group = $library_groups->next ) {
1441 if ( $library_group->parent->has_child( $branchcode ) ) {
1452 my $can_log_into = $patron->can_log_into( $library );
1454 Given a I<Koha::Library> object, it returns a boolean representing
1455 the fact the patron can log into a the library.
1460 my ( $self, $library ) = @_;
1464 if ( C4::Context->preference('IndependentBranches') ) {
1466 if $self->is_superlibrarian
1467 or $self->branchcode eq $library->id;
1477 =head3 libraries_where_can_see_patrons
1479 my $libraries = $patron-libraries_where_can_see_patrons;
1481 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1482 The branchcodes are arbitrarily returned sorted.
1483 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1485 An empty array means no restriction, the patron can see patron's infos from any libraries.
1489 sub libraries_where_can_see_patrons {
1491 my $userenv = C4::Context->userenv;
1493 return () unless $userenv; # For tests, but userenv should be defined in tests...
1495 my @restricted_branchcodes;
1496 if (C4::Context::only_my_library) {
1497 push @restricted_branchcodes, $self->branchcode;
1501 $self->has_permission(
1502 { borrowers => 'view_borrower_infos_from_any_libraries' }
1506 my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1507 if ( $library_groups->count )
1509 while ( my $library_group = $library_groups->next ) {
1510 my $parent = $library_group->parent;
1511 if ( $parent->has_child( $self->branchcode ) ) {
1512 push @restricted_branchcodes, $parent->children->get_column('branchcode');
1517 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1521 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1522 @restricted_branchcodes = uniq(@restricted_branchcodes);
1523 @restricted_branchcodes = sort(@restricted_branchcodes);
1524 return @restricted_branchcodes;
1527 =head3 has_permission
1529 my $permission = $patron->has_permission($required);
1531 See C4::Auth::haspermission for details of syntax for $required
1535 sub has_permission {
1536 my ( $self, $flagsrequired ) = @_;
1537 return unless $self->userid;
1538 # TODO code from haspermission needs to be moved here!
1539 return C4::Auth::haspermission( $self->userid, $flagsrequired );
1542 =head3 is_superlibrarian
1544 my $is_superlibrarian = $patron->is_superlibrarian;
1546 Return true if the patron is a superlibrarian.
1550 sub is_superlibrarian {
1552 return $self->has_permission( { superlibrarian => 1 } ) ? 1 : 0;
1557 my $is_adult = $patron->is_adult
1559 Return true if the patron has a category with a type Adult (A) or Organization (I)
1565 return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1570 my $is_child = $patron->is_child
1572 Return true if the patron has a category with a type Child (C)
1578 return $self->category->category_type eq 'C' ? 1 : 0;
1581 =head3 has_valid_userid
1583 my $patron = Koha::Patrons->find(42);
1584 $patron->userid( $new_userid );
1585 my $has_a_valid_userid = $patron->has_valid_userid
1587 my $patron = Koha::Patron->new( $params );
1588 my $has_a_valid_userid = $patron->has_valid_userid
1590 Return true if the current userid of this patron is valid/unique, otherwise false.
1592 Note that this should be done in $self->store instead and raise an exception if needed.
1596 sub has_valid_userid {
1599 return 0 unless $self->userid;
1601 return 0 if ( $self->userid eq C4::Context->config('user') ); # DB user
1603 my $already_exists = Koha::Patrons->search(
1605 userid => $self->userid,
1608 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1613 return $already_exists ? 0 : 1;
1616 =head3 generate_userid
1618 my $patron = Koha::Patron->new( $params );
1619 $patron->generate_userid
1621 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1623 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).
1627 sub generate_userid {
1630 my $firstname = $self->firstname // q{};
1631 my $surname = $self->surname // q{};
1632 #The script will "do" the following code and increment the $offset until the generated userid is unique
1634 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1635 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1636 my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1637 $userid = NFKD( $userid );
1638 $userid =~ s/\p{NonspacingMark}//g;
1639 $userid .= $offset unless $offset == 0;
1640 $self->userid( $userid );
1642 } while (! $self->has_valid_userid );
1647 =head3 add_extended_attribute
1651 sub add_extended_attribute {
1652 my ($self, $attribute) = @_;
1654 return Koha::Patron::Attribute->new(
1657 ( borrowernumber => $self->borrowernumber ),
1663 =head3 extended_attributes
1665 Return object of Koha::Patron::Attributes type with all attributes set for this patron
1671 sub extended_attributes {
1672 my ( $self, $attributes ) = @_;
1673 if ($attributes) { # setter
1674 my $schema = $self->_result->result_source->schema;
1677 # Remove the existing one
1678 $self->extended_attributes->filter_by_branch_limitations->delete;
1680 # Insert the new ones
1682 for my $attribute (@$attributes) {
1683 $self->add_extended_attribute($attribute);
1684 $new_types->{$attribute->{code}} = 1;
1687 # Check globally mandatory types
1688 my @required_attribute_types =
1689 Koha::Patron::Attribute::Types->search(
1692 'borrower_attribute_types_branches.b_branchcode' =>
1695 { join => 'borrower_attribute_types_branches' }
1696 )->get_column('code');
1697 for my $type ( @required_attribute_types ) {
1698 Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute->throw(
1700 ) if !$new_types->{$type};
1706 my $rs = $self->_result->borrower_attributes;
1707 # We call search to use the filters in Koha::Patron::Attributes->search
1708 return Koha::Patron::Attributes->_new_from_dbic($rs)->search;
1713 my $messages = $patron->messages;
1715 Return the message attached to the patron.
1721 my $messages_rs = $self->_result->messages_borrowernumbers->search;
1722 return Koha::Patron::Messages->_new_from_dbic($messages_rs);
1727 Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
1729 Lock and optionally expire a patron account.
1730 Remove holds and article requests if remove flag set.
1731 In order to distinguish from locking by entering a wrong password, let's
1732 call this an administrative lockout.
1737 my ( $self, $params ) = @_;
1738 $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
1739 if( $params->{expire} ) {
1740 $self->dateexpiry( dt_from_string->subtract(days => 1) );
1743 if( $params->{remove} ) {
1744 $self->holds->delete;
1745 $self->article_requests->delete;
1752 Koha::Patrons->find($id)->anonymize;
1754 Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
1755 are randomized, other personal data is cleared too.
1756 Patrons with issues are skipped.
1762 if( $self->_result->issues->count ) {
1763 warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
1766 # Mandatory fields come from the corresponding pref, but email fields
1767 # are removed since scrambled email addresses only generate errors
1768 my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
1769 split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
1770 $mandatory->{userid} = 1; # needed since sub store does not clear field
1771 my @columns = $self->_result->result_source->columns;
1772 @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized/ } @columns;
1773 push @columns, 'dateofbirth'; # add this date back in
1774 foreach my $col (@columns) {
1775 $self->_anonymize_column($col, $mandatory->{lc $col} );
1777 $self->anonymized(1)->store;
1780 sub _anonymize_column {
1781 my ( $self, $col, $mandatory ) = @_;
1782 my $col_info = $self->_result->result_source->column_info($col);
1783 my $type = $col_info->{data_type};
1784 my $nullable = $col_info->{is_nullable};
1786 if( $type =~ /char|text/ ) {
1788 ? Koha::Token->new->generate({ pattern => '\w{10}' })
1792 } elsif( $type =~ /integer|int$|float|dec|double/ ) {
1793 $val = $nullable ? undef : 0;
1794 } elsif( $type =~ /date|time/ ) {
1795 $val = $nullable ? undef : dt_from_string;
1800 =head3 add_guarantor
1802 my $relationship = $patron->add_guarantor(
1804 borrowernumber => $borrowernumber,
1805 relationships => $relationship,
1809 Adds a new guarantor to a patron.
1814 my ( $self, $params ) = @_;
1816 my $guarantor_id = $params->{guarantor_id};
1817 my $relationship = $params->{relationship};
1819 return Koha::Patron::Relationship->new(
1821 guarantee_id => $self->id,
1822 guarantor_id => $guarantor_id,
1823 relationship => $relationship
1828 =head3 get_extended_attribute
1830 my $attribute_value = $patron->get_extended_attribute( $code );
1832 Return the attribute for the code passed in parameter.
1834 It not exist it returns undef
1836 Note that this will not work for repeatable attribute types.
1838 Maybe you certainly not want to use this method, it is actually only used for SHOW_BARCODE
1839 (which should be a real patron's attribute (not extended)
1843 sub get_extended_attribute {
1844 my ( $self, $code, $value ) = @_;
1845 my $rs = $self->_result->borrower_attributes;
1847 my $attribute = $rs->search({ code => $code, ( $value ? ( attribute => $value ) : () ) });
1848 return unless $attribute->count;
1849 return $attribute->next;
1854 my $json = $patron->to_api;
1856 Overloaded method that returns a JSON representation of the Koha::Patron object,
1857 suitable for API output.
1862 my ( $self, $params ) = @_;
1864 my $json_patron = $self->SUPER::to_api( $params );
1866 $json_patron->{restricted} = ( $self->is_debarred )
1868 : Mojo::JSON->false;
1870 return $json_patron;
1873 =head3 to_api_mapping
1875 This method returns the mapping for representing a Koha::Patron object
1880 sub to_api_mapping {
1882 borrowernotes => 'staff_notes',
1883 borrowernumber => 'patron_id',
1884 branchcode => 'library_id',
1885 categorycode => 'category_id',
1886 checkprevcheckout => 'check_previous_checkout',
1887 contactfirstname => undef, # Unused
1888 contactname => undef, # Unused
1889 contactnote => 'altaddress_notes',
1890 contacttitle => undef, # Unused
1891 dateenrolled => 'date_enrolled',
1892 dateexpiry => 'expiry_date',
1893 dateofbirth => 'date_of_birth',
1894 debarred => undef, # replaced by 'restricted'
1895 debarredcomment => undef, # calculated, API consumers will use /restrictions instead
1896 emailpro => 'secondary_email',
1897 flags => undef, # permissions manipulation handled in /permissions
1898 gonenoaddress => 'incorrect_address',
1899 guarantorid => 'guarantor_id',
1900 lastseen => 'last_seen',
1901 lost => 'patron_card_lost',
1902 opacnote => 'opac_notes',
1903 othernames => 'other_name',
1904 password => undef, # password manipulation handled in /password
1905 phonepro => 'secondary_phone',
1906 relationship => 'relationship_type',
1908 smsalertnumber => 'sms_number',
1909 sort1 => 'statistics_1',
1910 sort2 => 'statistics_2',
1911 autorenew_checkouts => 'autorenew_checkouts',
1912 streetnumber => 'street_number',
1913 streettype => 'street_type',
1914 zipcode => 'postal_code',
1915 B_address => 'altaddress_address',
1916 B_address2 => 'altaddress_address2',
1917 B_city => 'altaddress_city',
1918 B_country => 'altaddress_country',
1919 B_email => 'altaddress_email',
1920 B_phone => 'altaddress_phone',
1921 B_state => 'altaddress_state',
1922 B_streetnumber => 'altaddress_street_number',
1923 B_streettype => 'altaddress_street_type',
1924 B_zipcode => 'altaddress_postal_code',
1925 altcontactaddress1 => 'altcontact_address',
1926 altcontactaddress2 => 'altcontact_address2',
1927 altcontactaddress3 => 'altcontact_city',
1928 altcontactcountry => 'altcontact_country',
1929 altcontactfirstname => 'altcontact_firstname',
1930 altcontactphone => 'altcontact_phone',
1931 altcontactsurname => 'altcontact_surname',
1932 altcontactstate => 'altcontact_state',
1933 altcontactzipcode => 'altcontact_postal_code',
1934 primary_contact_method => undef,
1940 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_name => 'DUE'});
1941 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports });
1942 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports, test_mode => 1 });
1944 Queue messages to a patron. Can pass a message that is part of the message_attributes
1945 table or supply the transport to use.
1947 If passed a message name we retrieve the patrons preferences for transports
1948 Otherwise we use the supplied transport. In the case of email or sms we fall back to print if
1949 we have no address/number for sending
1951 $letter_params is a hashref of the values to be passed to GetPreparedLetter
1953 test_mode will only report which notices would be sent, but nothing will be queued
1958 my ( $self, $params ) = @_;
1959 my $letter_params = $params->{letter_params};
1960 my $test_mode = $params->{test_mode};
1962 return unless $letter_params;
1963 return unless exists $params->{message_name} xor $params->{message_transports}; # We only want one of these
1965 my $library = Koha::Libraries->find( $letter_params->{branchcode} );
1966 my $from_email_address = $library->from_email_address;
1968 my @message_transports;
1970 $letter_code = $letter_params->{letter_code};
1971 if( $params->{message_name} ){
1972 my $messaging_prefs = C4::Members::Messaging::GetMessagingPreferences( {
1973 borrowernumber => $letter_params->{borrowernumber},
1974 message_name => $params->{message_name}
1976 @message_transports = ( keys %{ $messaging_prefs->{transports} } );
1977 $letter_code = $messaging_prefs->{transports}->{$message_transports[0]} unless $letter_code;
1979 @message_transports = @{$params->{message_transports}};
1981 return unless defined $letter_code;
1982 $letter_params->{letter_code} = $letter_code;
1985 foreach my $mtt (@message_transports){
1986 next if ($mtt eq 'itiva' and C4::Context->preference('TalkingTechItivaPhoneNotification') );
1987 # Notice is handled by TalkingTech_itiva_outbound.pl
1988 if ( ( $mtt eq 'email' and not $self->notice_email_address )
1989 or ( $mtt eq 'sms' and not $self->smsalertnumber )
1990 or ( $mtt eq 'phone' and not $self->phone ) )
1992 push @{ $return{fallback} }, $mtt;
1995 next if $mtt eq 'print' && $print_sent;
1996 $letter_params->{message_transport_type} = $mtt;
1997 my $letter = C4::Letters::GetPreparedLetter( %$letter_params );
1998 C4::Letters::EnqueueLetter({
2000 borrowernumber => $self->borrowernumber,
2001 from_address => $from_email_address,
2002 message_transport_type => $mtt
2003 }) unless $test_mode;
2004 push @{$return{sent}}, $mtt;
2005 $print_sent = 1 if $mtt eq 'print';
2010 =head3 safe_to_delete
2012 my $result = $patron->safe_to_delete;
2013 if ( $result eq 'has_guarantees' ) { ... }
2014 elsif ( $result ) { ... }
2015 else { # cannot delete }
2017 This method tells if the Koha:Patron object can be deleted. Possible return values
2023 =item 'has_checkouts'
2027 =item 'has_guarantees'
2029 =item 'is_anonymous_patron'
2035 sub safe_to_delete {
2038 my $anonymous_patron = C4::Context->preference('AnonymousPatron');
2042 if ( $anonymous_patron && $self->id eq $anonymous_patron ) {
2043 $error = 'is_anonymous_patron';
2045 elsif ( $self->checkouts->count ) {
2046 $error = 'has_checkouts';
2048 elsif ( $self->account->outstanding_debits->total_outstanding > 0 ) {
2049 $error = 'has_debt';
2051 elsif ( $self->guarantee_relationships->count ) {
2052 $error = 'has_guarantees';
2056 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
2059 return Koha::Result::Boolean->new(1);
2064 my $recalls = $patron->recalls;
2066 Return the patron's recalls.
2073 return Koha::Recalls->search({ borrowernumber => $self->borrowernumber });
2076 =head3 account_balance
2078 my $balance = $patron->account_balance
2080 Return the patron's account balance
2084 sub account_balance {
2086 return $self->account->balance;
2090 =head3 has_messaging_preference
2092 my $bool = $patron->has_messaging_preference({
2093 message_name => $message_name, # A value from message_attributes.message_name
2094 message_transport_type => $message_transport_type, # email, sms, phone, itiva, etc...
2095 wants_digest => $wants_digest, # 1 if you are looking for the digest version, don't pass if you just want either
2100 sub has_messaging_preference {
2101 my ( $self, $params ) = @_;
2103 my $message_name = $params->{message_name};
2104 my $message_transport_type = $params->{message_transport_type};
2105 my $wants_digest = $params->{wants_digest};
2107 return $self->_result->search_related_rs(
2108 'borrower_message_preferences',
2112 [ 'borrower_message_transport_preferences', 'message_attribute' ]
2117 =head3 can_patron_change_staff_only_lists
2119 $patron->can_patron_change_staff_only_lists;
2121 Return 1 if a patron has 'Superlibrarian' or 'Catalogue' permission.
2122 Otherwise, return 0.
2126 sub can_patron_change_staff_only_lists {
2127 my ( $self, $params ) = @_;
2128 return 1 if C4::Auth::haspermission( $self->userid, { 'catalogue' => 1 });
2132 =head2 Internal methods
2144 Kyle M Hall <kyle@bywatersolutions.com>
2145 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
2146 Martin Renvoize <martin.renvoize@ptfs-europe.com>