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 );
28 use C4::Log qw( logaction );
30 use Koha::ArticleRequests;
34 use Koha::CirculationRules;
35 use Koha::Club::Enrollments;
37 use Koha::DateUtils qw( dt_from_string );
38 use Koha::Exceptions::Password;
40 use Koha::Old::Checkouts;
41 use Koha::Patron::Attributes;
42 use Koha::Patron::Categories;
43 use Koha::Patron::Debarments;
44 use Koha::Patron::HouseboundProfile;
45 use Koha::Patron::HouseboundRole;
46 use Koha::Patron::Images;
47 use Koha::Patron::Modifications;
48 use Koha::Patron::Relationships;
51 use Koha::Subscription::Routinglists;
53 use Koha::Virtualshelves;
55 use base qw(Koha::Object);
57 use constant ADMINISTRATIVE_LOCKOUT => -1;
59 our $RESULTSET_PATRON_ID_MAPPING = {
60 Accountline => 'borrowernumber',
61 Aqbasketuser => 'borrowernumber',
62 Aqbudget => 'budget_owner_id',
63 Aqbudgetborrower => 'borrowernumber',
64 ArticleRequest => 'borrowernumber',
65 BorrowerDebarment => 'borrowernumber',
66 BorrowerFile => 'borrowernumber',
67 BorrowerModification => 'borrowernumber',
68 ClubEnrollment => 'borrowernumber',
69 Issue => 'borrowernumber',
70 ItemsLastBorrower => 'borrowernumber',
71 Linktracker => 'borrowernumber',
72 Message => 'borrowernumber',
73 MessageQueue => 'borrowernumber',
74 OldIssue => 'borrowernumber',
75 OldReserve => 'borrowernumber',
76 Rating => 'borrowernumber',
77 Reserve => 'borrowernumber',
78 Review => 'borrowernumber',
79 SearchHistory => 'userid',
80 Statistic => 'borrowernumber',
81 Suggestion => 'suggestedby',
82 TagAll => 'borrowernumber',
83 Virtualshelfcontent => 'borrowernumber',
84 Virtualshelfshare => 'borrowernumber',
85 Virtualshelve => 'owner',
90 Koha::Patron - Koha Patron Object class
101 my ( $class, $params ) = @_;
103 return $class->SUPER::new($params);
106 =head3 fixup_cardnumber
108 Autogenerate next cardnumber from highest value found in database
112 sub fixup_cardnumber {
115 my ( $max ) = Koha::Plugins->call_recursive( 'patron_barcode_transform', $self->cardnumber );
117 $max ||= Koha::Patrons->search({
118 cardnumber => {-regexp => '^-?[0-9]+$'}
120 select => \'CAST(cardnumber AS SIGNED)',
121 as => ['cast_cardnumber']
122 })->_resultset->get_column('cast_cardnumber')->max;
123 $self->cardnumber(($max || 0) +1);
126 =head3 trim_whitespace
128 trim whitespace from data which has some non-whitespace in it.
129 Could be moved to Koha::Object if need to be reused
133 sub trim_whitespaces {
136 my $schema = Koha::Database->new->schema;
137 my @columns = $schema->source($self->_type)->columns;
139 for my $column( @columns ) {
140 my $value = $self->$column;
141 if ( defined $value ) {
142 $value =~ s/^\s*|\s*$//g;
143 $self->$column($value);
149 =head3 plain_text_password
151 $patron->plain_text_password( $password );
153 stores a copy of the unencrypted password in the object
154 for use in code before encrypting for db
158 sub plain_text_password {
159 my ( $self, $password ) = @_;
161 $self->{_plain_text_password} = $password;
164 return $self->{_plain_text_password}
165 if $self->{_plain_text_password};
172 Patron specific store method to cleanup record
173 and do other necessary things before saving
181 $self->_result->result_source->schema->txn_do(
184 C4::Context->preference("autoMemberNum")
185 and ( not defined $self->cardnumber
186 or $self->cardnumber eq '' )
189 # Warning: The caller is responsible for locking the members table in write
190 # mode, to avoid database corruption.
191 # We are in a transaction but the table is not locked
192 $self->fixup_cardnumber;
195 unless( $self->category->in_storage ) {
196 Koha::Exceptions::Object::FKConstraint->throw(
197 broken_fk => 'categorycode',
198 value => $self->categorycode,
202 $self->trim_whitespaces;
204 my ( $new_cardnumber ) = Koha::Plugins->call_recursive( 'patron_barcode_transform', $self->cardnumber );
205 $self->cardnumber( $new_cardnumber );
207 # Set surname to uppercase if uppercasesurname is true
208 $self->surname( uc($self->surname) )
209 if C4::Context->preference("uppercasesurnames");
211 $self->relationship(undef) # We do not want to store an empty string in this field
212 if defined $self->relationship
213 and $self->relationship eq "";
215 unless ( $self->in_storage ) { #AddMember
217 # Generate a valid userid/login if needed
218 $self->generate_userid
219 if not $self->userid or not $self->has_valid_userid;
221 # Add expiration date if it isn't already there
222 unless ( $self->dateexpiry ) {
223 $self->dateexpiry( $self->category->get_expiry_date );
226 # Add enrollment date if it isn't already there
227 unless ( $self->dateenrolled ) {
228 $self->dateenrolled(dt_from_string);
231 # Set the privacy depending on the patron's category
232 my $default_privacy = $self->category->default_privacy || q{};
234 $default_privacy eq 'default' ? 1
235 : $default_privacy eq 'never' ? 2
236 : $default_privacy eq 'forever' ? 0
238 $self->privacy($default_privacy);
240 # Call any check_password plugins if password is passed
241 if ( C4::Context->config("enable_plugins") && $self->password ) {
242 my @plugins = Koha::Plugins->new()->GetPlugins({
243 method => 'check_password',
245 foreach my $plugin ( @plugins ) {
246 # This plugin hook will also be used by a plugin for the Norwegian national
247 # patron database. This is why we need to pass both the password and the
248 # borrowernumber to the plugin.
249 my $ret = $plugin->check_password(
251 password => $self->password,
252 borrowernumber => $self->borrowernumber
255 if ( $ret->{'error'} == 1 ) {
256 Koha::Exceptions::Password::Plugin->throw();
261 # Make a copy of the plain text password for later use
262 $self->plain_text_password( $self->password );
264 # Create a disabled account if no password provided
265 $self->password( $self->password
266 ? Koha::AuthUtils::hash_password( $self->password )
269 $self->borrowernumber(undef);
271 $self = $self->SUPER::store;
273 $self->add_enrolment_fee_if_needed(0);
275 logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
276 if C4::Context->preference("BorrowersLog");
280 my $self_from_storage = $self->get_from_storage;
281 # FIXME We should not deal with that here, callers have to do this job
282 # Moved from ModMember to prevent regressions
283 unless ( $self->userid ) {
284 my $stored_userid = $self_from_storage->userid;
285 $self->userid($stored_userid);
288 # Password must be updated using $self->set_password
289 $self->password($self_from_storage->password);
291 if ( $self->category->categorycode ne
292 $self_from_storage->category->categorycode )
294 # Add enrolement fee on category change if required
295 $self->add_enrolment_fee_if_needed(1)
296 if C4::Context->preference('FeeOnChangePatronCategory');
298 # Clean up guarantors on category change if required
299 $self->guarantor_relationships->delete
300 if ( $self->category->category_type ne 'C'
301 && $self->category->category_type ne 'P' );
306 if ( C4::Context->preference("BorrowersLog") ) {
308 my $from_storage = $self_from_storage->unblessed;
309 my $from_object = $self->unblessed;
310 my @skip_fields = (qw/lastseen updated_on/);
311 for my $key ( keys %{$from_storage} ) {
312 next if any { /$key/ } @skip_fields;
315 !defined( $from_storage->{$key} )
316 && defined( $from_object->{$key} )
318 || ( defined( $from_storage->{$key} )
319 && !defined( $from_object->{$key} ) )
321 defined( $from_storage->{$key} )
322 && defined( $from_object->{$key} )
323 && ( $from_storage->{$key} ne
324 $from_object->{$key} )
329 before => $from_storage->{$key},
330 after => $from_object->{$key}
335 if ( defined($info) ) {
339 $self->borrowernumber,
342 { utf8 => 1, pretty => 1, canonical => 1 }
349 $self = $self->SUPER::store;
360 Delete patron's holds, lists and finally the patron.
362 Lists owned by the borrower are deleted, but entries from the borrower to
363 other lists are kept.
370 my $anonymous_patron = C4::Context->preference("AnonymousPatron");
371 Koha::Exceptions::Patron::FailedDeleteAnonymousPatron->throw() if $anonymous_patron && $self->id eq $anonymous_patron;
373 $self->_result->result_source->schema->txn_do(
375 # Cancel Patron's holds
376 my $holds = $self->holds;
377 while( my $hold = $holds->next ){
381 # Delete all lists and all shares of this borrower
382 # Consistent with the approach Koha uses on deleting individual lists
383 # Note that entries in virtualshelfcontents added by this borrower to
384 # lists of others will be handled by a table constraint: the borrower
385 # is set to NULL in those entries.
387 # We could handle the above deletes via a constraint too.
388 # But a new BZ report 11889 has been opened to discuss another approach.
389 # Instead of deleting we could also disown lists (based on a pref).
390 # In that way we could save shared and public lists.
391 # The current table constraints support that idea now.
392 # This pref should then govern the results of other routines/methods such as
393 # Koha::Virtualshelf->new->delete too.
394 # FIXME Could be $patron->get_lists
395 $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
397 # We cannot have a FK on borrower_modifications.borrowernumber, the table is also used
399 $_->delete for Koha::Patron::Modifications->search( { borrowernumber => $self->borrowernumber } );
401 $self->SUPER::delete;
403 logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
412 my $patron_category = $patron->category
414 Return the patron category for this patron
420 return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
430 return Koha::Patron::Images->find( $self->borrowernumber );
435 Returns a Koha::Library object representing the patron's home library.
441 return Koha::Library->_new_from_dbic($self->_result->branchcode);
446 Returns a Koha::SMS::Provider object representing the patron's SMS provider.
452 my $sms_provider_rs = $self->_result->sms_provider;
453 return unless $sms_provider_rs;
454 return Koha::SMS::Provider->_new_from_dbic($sms_provider_rs);
457 =head3 guarantor_relationships
459 Returns Koha::Patron::Relationships object for this patron's guarantors
461 Returns the set of relationships for the patrons that are guarantors for this patron.
463 This is returned instead of a Koha::Patron object because the guarantor
464 may not exist as a patron in Koha. If this is true, the guarantors name
465 exists in the Koha::Patron::Relationship object and will have no guarantor_id.
469 sub guarantor_relationships {
472 return Koha::Patron::Relationships->search( { guarantee_id => $self->id } );
475 =head3 guarantee_relationships
477 Returns Koha::Patron::Relationships object for this patron's guarantors
479 Returns the set of relationships for the patrons that are guarantees for this patron.
481 The method returns Koha::Patron::Relationship objects for the sake
482 of consistency with the guantors method.
483 A guarantee by definition must exist as a patron in Koha.
487 sub guarantee_relationships {
490 return Koha::Patron::Relationships->search(
491 { guarantor_id => $self->id },
493 prefetch => 'guarantee',
494 order_by => { -asc => [ 'guarantee.surname', 'guarantee.firstname' ] },
499 =head3 relationships_debt
501 Returns the amount owed by the patron's guarantors *and* the other guarantees of those guarantors
505 sub relationships_debt {
506 my ($self, $params) = @_;
508 my $include_guarantors = $params->{include_guarantors};
509 my $only_this_guarantor = $params->{only_this_guarantor};
510 my $include_this_patron = $params->{include_this_patron};
513 if ( $only_this_guarantor ) {
514 @guarantors = $self->guarantee_relationships->count ? ( $self ) : ();
515 Koha::Exceptions::BadParameter->throw( { parameter => 'only_this_guarantor' } ) unless @guarantors;
516 } elsif ( $self->guarantor_relationships->count ) {
517 # I am a guarantee, just get all my guarantors
518 @guarantors = $self->guarantor_relationships->guarantors;
520 # I am a guarantor, I need to get all the guarantors of all my guarantees
521 @guarantors = map { $_->guarantor_relationships->guarantors } $self->guarantee_relationships->guarantees;
524 my $non_issues_charges = 0;
525 my $seen = $include_this_patron ? {} : { $self->id => 1 }; # For tracking members already added to the total
526 foreach my $guarantor (@guarantors) {
527 $non_issues_charges += $guarantor->account->non_issues_charges if $include_guarantors && !$seen->{ $guarantor->id };
529 # We've added what the guarantor owes, not added in that guarantor's guarantees as well
530 my @guarantees = map { $_->guarantee } $guarantor->guarantee_relationships();
531 my $guarantees_non_issues_charges = 0;
532 foreach my $guarantee (@guarantees) {
533 next if $seen->{ $guarantee->id };
534 $guarantees_non_issues_charges += $guarantee->account->non_issues_charges;
535 # Mark this guarantee as seen so we don't double count a guarantee linked to multiple guarantors
536 $seen->{ $guarantee->id } = 1;
539 $non_issues_charges += $guarantees_non_issues_charges;
540 $seen->{ $guarantor->id } = 1;
543 return $non_issues_charges;
546 =head3 housebound_profile
548 Returns the HouseboundProfile associated with this patron.
552 sub housebound_profile {
554 my $profile = $self->_result->housebound_profile;
555 return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
560 =head3 housebound_role
562 Returns the HouseboundRole associated with this patron.
566 sub housebound_role {
569 my $role = $self->_result->housebound_role;
570 return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
576 Returns the siblings of this patron.
583 my @guarantors = $self->guarantor_relationships()->guarantors();
585 return unless @guarantors;
588 map { $_->guarantee_relationships()->guarantees() } @guarantors;
590 return unless @siblings;
594 grep { !$seen{ $_->id }++ && ( $_->id != $self->id ) } @siblings;
596 return wantarray ? @siblings : Koha::Patrons->search( { borrowernumber => { -in => [ map { $_->id } @siblings ] } } );
601 my $patron = Koha::Patrons->find($id);
602 $patron->merge_with( \@patron_ids );
604 This subroutine merges a list of patrons into the patron record. This is accomplished by finding
605 all related patron ids for the patrons to be merged in other tables and changing the ids to be that
606 of the keeper patron.
611 my ( $self, $patron_ids ) = @_;
613 my $anonymous_patron = C4::Context->preference("AnonymousPatron");
614 return if $anonymous_patron && $self->id eq $anonymous_patron;
616 my @patron_ids = @{ $patron_ids };
618 # Ensure the keeper isn't in the list of patrons to merge
619 @patron_ids = grep { $_ ne $self->id } @patron_ids;
621 my $schema = Koha::Database->new()->schema();
625 $self->_result->result_source->schema->txn_do( sub {
626 foreach my $patron_id (@patron_ids) {
628 next if $patron_id eq $anonymous_patron;
630 my $patron = Koha::Patrons->find( $patron_id );
634 # Unbless for safety, the patron will end up being deleted
635 $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
637 my $attributes = $patron->extended_attributes;
638 my $new_attributes = [
639 map { { code => $_->code, attribute => $_->attribute } }
642 $attributes->delete; # We need to delete before trying to merge them to prevent exception on unique and repeatable
643 for my $attribute ( @$new_attributes ) {
644 $self->add_extended_attribute($attribute);
647 while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
648 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
649 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
650 $rs->update({ $field => $self->id });
651 if ( $r eq 'BorrowerDebarment' ) {
652 Koha::Patron::Debarments::UpdateBorrowerDebarmentFlags($self->id);
656 $patron->move_to_deleted();
666 =head3 wants_check_for_previous_checkout
668 $wants_check = $patron->wants_check_for_previous_checkout;
670 Return 1 if Koha needs to perform PrevIssue checking, else 0.
674 sub wants_check_for_previous_checkout {
676 my $syspref = C4::Context->preference("checkPrevCheckout");
679 ## Hard syspref trumps all
680 return 1 if ($syspref eq 'hardyes');
681 return 0 if ($syspref eq 'hardno');
682 ## Now, patron pref trumps all
683 return 1 if ($self->checkprevcheckout eq 'yes');
684 return 0 if ($self->checkprevcheckout eq 'no');
686 # More complex: patron inherits -> determine category preference
687 my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
688 return 1 if ($checkPrevCheckoutByCat eq 'yes');
689 return 0 if ($checkPrevCheckoutByCat eq 'no');
691 # Finally: category preference is inherit, default to 0
692 if ($syspref eq 'softyes') {
699 =head3 do_check_for_previous_checkout
701 $do_check = $patron->do_check_for_previous_checkout($item);
703 Return 1 if the bib associated with $ITEM has previously been checked out to
704 $PATRON, 0 otherwise.
708 sub do_check_for_previous_checkout {
709 my ( $self, $item ) = @_;
712 my $biblio = Koha::Biblios->find( $item->{biblionumber} );
713 if ( $biblio->is_serial ) {
714 push @item_nos, $item->{itemnumber};
716 # Get all itemnumbers for given bibliographic record.
717 @item_nos = $biblio->items->get_column( 'itemnumber' );
720 # Create (old)issues search criteria
722 borrowernumber => $self->borrowernumber,
723 itemnumber => \@item_nos,
726 my $delay = C4::Context->preference('CheckPrevCheckoutDelay') || 0;
728 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
729 my $newer_than = dt_from_string()->subtract( days => $delay );
730 $criteria->{'returndate'} = { '>' => $dtf->format_datetime($newer_than), };
733 # Check current issues table
734 my $issues = Koha::Checkouts->search($criteria);
735 return 1 if $issues->count; # 0 || N
737 # Check old issues table
738 my $old_issues = Koha::Old::Checkouts->search($criteria);
739 return $old_issues->count; # 0 || N
744 my $debarment_expiration = $patron->is_debarred;
746 Returns the date a patron debarment will expire, or undef if the patron is not
754 return unless $self->debarred;
755 return $self->debarred
756 if $self->debarred =~ '^9999'
757 or dt_from_string( $self->debarred ) > dt_from_string;
763 my $is_expired = $patron->is_expired;
765 Returns 1 if the patron is expired or 0;
771 return 0 unless $self->dateexpiry;
772 return 0 if $self->dateexpiry =~ '^9999';
773 return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
777 =head3 is_going_to_expire
779 my $is_going_to_expire = $patron->is_going_to_expire;
781 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
785 sub is_going_to_expire {
788 my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
790 return 0 unless $delay;
791 return 0 unless $self->dateexpiry;
792 return 0 if $self->dateexpiry =~ '^9999';
793 return 1 if dt_from_string( $self->dateexpiry, undef, 'floating' )->subtract( days => $delay ) < dt_from_string(undef, undef, 'floating')->truncate( to => 'day' );
799 $patron->set_password({ password => $plain_text_password [, skip_validation => 1 ] });
801 Set the patron's password.
805 The passed string is validated against the current password enforcement policy.
806 Validation can be skipped by passing the I<skip_validation> parameter.
808 Exceptions are thrown if the password is not good enough.
812 =item Koha::Exceptions::Password::TooShort
814 =item Koha::Exceptions::Password::WhitespaceCharacters
816 =item Koha::Exceptions::Password::TooWeak
818 =item Koha::Exceptions::Password::Plugin (if a "check password" plugin is enabled)
825 my ( $self, $args ) = @_;
827 my $password = $args->{password};
829 unless ( $args->{skip_validation} ) {
830 my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password, $self->category );
833 if ( $error eq 'too_short' ) {
834 my $min_length = $self->category->effective_min_password_length;
835 $min_length = 3 if not $min_length or $min_length < 3;
837 my $password_length = length($password);
838 Koha::Exceptions::Password::TooShort->throw(
839 length => $password_length, min_length => $min_length );
841 elsif ( $error eq 'has_whitespaces' ) {
842 Koha::Exceptions::Password::WhitespaceCharacters->throw();
844 elsif ( $error eq 'too_weak' ) {
845 Koha::Exceptions::Password::TooWeak->throw();
850 if ( C4::Context->config("enable_plugins") ) {
851 # Call any check_password plugins
852 my @plugins = Koha::Plugins->new()->GetPlugins({
853 method => 'check_password',
855 foreach my $plugin ( @plugins ) {
856 # This plugin hook will also be used by a plugin for the Norwegian national
857 # patron database. This is why we need to pass both the password and the
858 # borrowernumber to the plugin.
859 my $ret = $plugin->check_password(
861 password => $password,
862 borrowernumber => $self->borrowernumber
865 # This plugin hook will also be used by a plugin for the Norwegian national
866 # patron database. This is why we need to call the actual plugins and then
867 # check skip_validation afterwards.
868 if ( $ret->{'error'} == 1 && !$args->{skip_validation} ) {
869 Koha::Exceptions::Password::Plugin->throw();
874 my $digest = Koha::AuthUtils::hash_password($password);
876 # We do not want to call $self->store and retrieve password from DB
877 $self->password($digest);
878 $self->login_attempts(0);
881 logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" )
882 if C4::Context->preference("BorrowersLog");
890 my $new_expiry_date = $patron->renew_account
892 Extending the subscription to the expiry date.
899 if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
900 $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
903 C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
904 ? dt_from_string( $self->dateexpiry )
907 my $expiry_date = $self->category->get_expiry_date($date);
909 $self->dateexpiry($expiry_date);
910 $self->date_renewed( dt_from_string() );
913 $self->add_enrolment_fee_if_needed(1);
915 logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
916 return dt_from_string( $expiry_date )->truncate( to => 'day' );
921 my $has_overdues = $patron->has_overdues;
923 Returns the number of patron's overdues
929 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
930 return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
935 $patron->track_login;
936 $patron->track_login({ force => 1 });
938 Tracks a (successful) login attempt.
939 The preference TrackLastPatronActivity must be enabled. Or you
940 should pass the force parameter.
945 my ( $self, $params ) = @_;
948 !C4::Context->preference('TrackLastPatronActivity');
949 $self->lastseen( dt_from_string() )->store;
952 =head3 move_to_deleted
954 my $is_moved = $patron->move_to_deleted;
956 Move a patron to the deletedborrowers table.
957 This can be done before deleting a patron, to make sure the data are not completely deleted.
961 sub move_to_deleted {
963 my $patron_infos = $self->unblessed;
964 delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
965 return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
968 =head3 can_request_article
970 if ( $patron->can_request_article( $library->id ) ) { ... }
972 Returns true if the patron can request articles. As limits apply for the patron
973 on the same day, those completed the same day are considered as current.
975 A I<library_id> can be passed as parameter, falling back to userenv if absent.
979 sub can_request_article {
980 my ($self, $library_id) = @_;
982 $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
984 my $rule = Koha::CirculationRules->get_effective_rule(
986 branchcode => $library_id,
987 categorycode => $self->categorycode,
988 rule_name => 'open_article_requests_limit'
992 my $limit = ($rule) ? $rule->rule_value : undef;
994 return 1 unless defined $limit;
996 my $count = Koha::ArticleRequests->search(
997 [ { borrowernumber => $self->borrowernumber, status => [ 'REQUESTED', 'PENDING', 'PROCESSING' ] },
998 { borrowernumber => $self->borrowernumber, status => 'COMPLETED', updated_on => { '>=' => \'CAST(NOW() AS DATE)' } },
1001 return $count < $limit ? 1 : 0;
1004 =head3 article_requests
1006 my $article_requests = $patron->article_requests;
1008 Returns the patron article requests.
1012 sub article_requests {
1015 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
1018 =head3 add_enrolment_fee_if_needed
1020 my $enrolment_fee = $patron->add_enrolment_fee_if_needed($renewal);
1022 Add enrolment fee for a patron if needed.
1024 $renewal - boolean denoting whether this is an account renewal or not
1028 sub add_enrolment_fee_if_needed {
1029 my ($self, $renewal) = @_;
1030 my $enrolment_fee = $self->category->enrolmentfee;
1031 if ( $enrolment_fee && $enrolment_fee > 0 ) {
1032 my $type = $renewal ? 'ACCOUNT_RENEW' : 'ACCOUNT';
1033 $self->account->add_debit(
1035 amount => $enrolment_fee,
1036 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
1037 interface => C4::Context->interface,
1038 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
1043 return $enrolment_fee || 0;
1048 my $checkouts = $patron->checkouts
1054 my $checkouts = $self->_result->issues;
1055 return Koha::Checkouts->_new_from_dbic( $checkouts );
1058 =head3 pending_checkouts
1060 my $pending_checkouts = $patron->pending_checkouts
1062 This method will return the same as $self->checkouts, but with a prefetch on
1063 items, biblio and biblioitems.
1065 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
1067 It should not be used directly, prefer to access fields you need instead of
1068 retrieving all these fields in one go.
1072 sub pending_checkouts {
1074 my $checkouts = $self->_result->issues->search(
1078 { -desc => 'me.timestamp' },
1079 { -desc => 'issuedate' },
1080 { -desc => 'issue_id' }, # Sort by issue_id should be enough
1082 prefetch => { item => { biblio => 'biblioitems' } },
1085 return Koha::Checkouts->_new_from_dbic( $checkouts );
1088 =head3 old_checkouts
1090 my $old_checkouts = $patron->old_checkouts
1096 my $old_checkouts = $self->_result->old_issues;
1097 return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
1102 my $overdue_items = $patron->get_overdues
1104 Return the overdue items
1110 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
1111 return $self->checkouts->search(
1113 'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
1116 prefetch => { item => { biblio => 'biblioitems' } },
1121 =head3 get_routing_lists
1123 my @routinglists = $patron->get_routing_lists
1125 Returns the routing lists a patron is subscribed to.
1129 sub get_routing_lists {
1131 my $routing_list_rs = $self->_result->subscriptionroutinglists;
1132 return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
1137 my $age = $patron->get_age
1139 Return the age of the patron
1145 my $today_str = dt_from_string->strftime("%Y-%m-%d");
1146 return unless $self->dateofbirth;
1147 my $dob_str = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
1149 my ( $dob_y, $dob_m, $dob_d ) = split /-/, $dob_str;
1150 my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
1152 my $age = $today_y - $dob_y;
1153 if ( $dob_m . $dob_d > $today_m . $today_d ) {
1162 my $is_valid = $patron->is_valid_age
1164 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
1170 my $age = $self->get_age;
1172 my $patroncategory = $self->category;
1173 my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
1175 return (defined($age) && (($high && ($age > $high)) or ($low && ($age < $low)))) ? 0 : 1;
1180 my $account = $patron->account
1186 return Koha::Account->new( { patron_id => $self->borrowernumber } );
1191 my $holds = $patron->holds
1193 Return all the holds placed by this patron
1199 my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1200 return Koha::Holds->_new_from_dbic($holds_rs);
1205 my $old_holds = $patron->old_holds
1207 Return all the historical holds for this patron
1213 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1214 return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1217 =head3 return_claims
1219 my $return_claims = $patron->return_claims
1225 my $return_claims = $self->_result->return_claims_borrowernumbers;
1226 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $return_claims );
1229 =head3 notice_email_address
1231 my $email = $patron->notice_email_address;
1233 Return the email address of patron used for notices.
1234 Returns the empty string if no email address.
1238 sub notice_email_address{
1241 my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1242 # if syspref is set to 'first valid' (value == OFF), look up email address
1243 if ( $which_address eq 'OFF' ) {
1244 return $self->first_valid_email_address;
1247 return $self->$which_address || '';
1250 =head3 first_valid_email_address
1252 my $first_valid_email_address = $patron->first_valid_email_address
1254 Return the first valid email address for a patron.
1255 For now, the order is defined as email, emailpro, B_email.
1256 Returns the empty string if the borrower has no email addresses.
1260 sub first_valid_email_address {
1263 return $self->email() || $self->emailpro() || $self->B_email() || q{};
1266 =head3 get_club_enrollments
1270 sub get_club_enrollments {
1271 my ( $self, $return_scalar ) = @_;
1273 my $e = Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1275 return $e if $return_scalar;
1277 return wantarray ? $e->as_list : $e;
1280 =head3 get_enrollable_clubs
1284 sub get_enrollable_clubs {
1285 my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
1288 $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1289 if $is_enrollable_from_opac;
1290 $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1292 $params->{borrower} = $self;
1294 my $e = Koha::Clubs->get_enrollable($params);
1296 return $e if $return_scalar;
1298 return wantarray ? $e->as_list : $e;
1301 =head3 account_locked
1303 my $is_locked = $patron->account_locked
1305 Return true if the patron has reached the maximum number of login attempts
1306 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1307 as an administrative lockout (independent of FailedLoginAttempts; see also
1308 Koha::Patron->lock).
1309 Otherwise return false.
1310 If the pref is not set (empty string, null or 0), the feature is considered as
1315 sub account_locked {
1317 my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1318 return 1 if $FailedLoginAttempts
1319 and $self->login_attempts
1320 and $self->login_attempts >= $FailedLoginAttempts;
1321 return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1325 =head3 can_see_patron_infos
1327 my $can_see = $patron->can_see_patron_infos( $patron );
1329 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1333 sub can_see_patron_infos {
1334 my ( $self, $patron ) = @_;
1335 return unless $patron;
1336 return $self->can_see_patrons_from( $patron->library->branchcode );
1339 =head3 can_see_patrons_from
1341 my $can_see = $patron->can_see_patrons_from( $branchcode );
1343 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1347 sub can_see_patrons_from {
1348 my ( $self, $branchcode ) = @_;
1350 if ( $self->branchcode eq $branchcode ) {
1352 } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1354 } elsif ( my $library_groups = $self->library->library_groups ) {
1355 while ( my $library_group = $library_groups->next ) {
1356 if ( $library_group->parent->has_child( $branchcode ) ) {
1367 my $can_log_into = $patron->can_log_into( $library );
1369 Given a I<Koha::Library> object, it returns a boolean representing
1370 the fact the patron can log into a the library.
1375 my ( $self, $library ) = @_;
1379 if ( C4::Context->preference('IndependentBranches') ) {
1381 if $self->is_superlibrarian
1382 or $self->branchcode eq $library->id;
1392 =head3 libraries_where_can_see_patrons
1394 my $libraries = $patron-libraries_where_can_see_patrons;
1396 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1397 The branchcodes are arbitrarily returned sorted.
1398 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1400 An empty array means no restriction, the patron can see patron's infos from any libraries.
1404 sub libraries_where_can_see_patrons {
1406 my $userenv = C4::Context->userenv;
1408 return () unless $userenv; # For tests, but userenv should be defined in tests...
1410 my @restricted_branchcodes;
1411 if (C4::Context::only_my_library) {
1412 push @restricted_branchcodes, $self->branchcode;
1416 $self->has_permission(
1417 { borrowers => 'view_borrower_infos_from_any_libraries' }
1421 my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1422 if ( $library_groups->count )
1424 while ( my $library_group = $library_groups->next ) {
1425 my $parent = $library_group->parent;
1426 if ( $parent->has_child( $self->branchcode ) ) {
1427 push @restricted_branchcodes, $parent->children->get_column('branchcode');
1432 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1436 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1437 @restricted_branchcodes = uniq(@restricted_branchcodes);
1438 @restricted_branchcodes = sort(@restricted_branchcodes);
1439 return @restricted_branchcodes;
1442 =head3 has_permission
1444 my $permission = $patron->has_permission($required);
1446 See C4::Auth::haspermission for details of syntax for $required
1450 sub has_permission {
1451 my ( $self, $flagsrequired ) = @_;
1452 return unless $self->userid;
1453 # TODO code from haspermission needs to be moved here!
1454 return C4::Auth::haspermission( $self->userid, $flagsrequired );
1457 =head3 is_superlibrarian
1459 my $is_superlibrarian = $patron->is_superlibrarian;
1461 Return true if the patron is a superlibrarian.
1465 sub is_superlibrarian {
1467 return $self->has_permission( { superlibrarian => 1 } ) ? 1 : 0;
1472 my $is_adult = $patron->is_adult
1474 Return true if the patron has a category with a type Adult (A) or Organization (I)
1480 return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1485 my $is_child = $patron->is_child
1487 Return true if the patron has a category with a type Child (C)
1493 return $self->category->category_type eq 'C' ? 1 : 0;
1496 =head3 has_valid_userid
1498 my $patron = Koha::Patrons->find(42);
1499 $patron->userid( $new_userid );
1500 my $has_a_valid_userid = $patron->has_valid_userid
1502 my $patron = Koha::Patron->new( $params );
1503 my $has_a_valid_userid = $patron->has_valid_userid
1505 Return true if the current userid of this patron is valid/unique, otherwise false.
1507 Note that this should be done in $self->store instead and raise an exception if needed.
1511 sub has_valid_userid {
1514 return 0 unless $self->userid;
1516 return 0 if ( $self->userid eq C4::Context->config('user') ); # DB user
1518 my $already_exists = Koha::Patrons->search(
1520 userid => $self->userid,
1523 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1528 return $already_exists ? 0 : 1;
1531 =head3 generate_userid
1533 my $patron = Koha::Patron->new( $params );
1534 $patron->generate_userid
1536 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1538 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).
1542 sub generate_userid {
1545 my $firstname = $self->firstname // q{};
1546 my $surname = $self->surname // q{};
1547 #The script will "do" the following code and increment the $offset until the generated userid is unique
1549 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1550 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1551 my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1552 $userid = NFKD( $userid );
1553 $userid =~ s/\p{NonspacingMark}//g;
1554 $userid .= $offset unless $offset == 0;
1555 $self->userid( $userid );
1557 } while (! $self->has_valid_userid );
1562 =head3 add_extended_attribute
1566 sub add_extended_attribute {
1567 my ($self, $attribute) = @_;
1569 return Koha::Patron::Attribute->new(
1572 ( borrowernumber => $self->borrowernumber ),
1578 =head3 extended_attributes
1580 Return object of Koha::Patron::Attributes type with all attributes set for this patron
1586 sub extended_attributes {
1587 my ( $self, $attributes ) = @_;
1588 if ($attributes) { # setter
1589 my $schema = $self->_result->result_source->schema;
1592 # Remove the existing one
1593 $self->extended_attributes->filter_by_branch_limitations->delete;
1595 # Insert the new ones
1597 for my $attribute (@$attributes) {
1598 $self->add_extended_attribute($attribute);
1599 $new_types->{$attribute->{code}} = 1;
1602 # Check globally mandatory types
1603 my @required_attribute_types =
1604 Koha::Patron::Attribute::Types->search(
1607 'borrower_attribute_types_branches.b_branchcode' =>
1610 { join => 'borrower_attribute_types_branches' }
1611 )->get_column('code');
1612 for my $type ( @required_attribute_types ) {
1613 Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute->throw(
1615 ) if !$new_types->{$type};
1621 my $rs = $self->_result->borrower_attributes;
1622 # We call search to use the filters in Koha::Patron::Attributes->search
1623 return Koha::Patron::Attributes->_new_from_dbic($rs)->search;
1628 Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
1630 Lock and optionally expire a patron account.
1631 Remove holds and article requests if remove flag set.
1632 In order to distinguish from locking by entering a wrong password, let's
1633 call this an administrative lockout.
1638 my ( $self, $params ) = @_;
1639 $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
1640 if( $params->{expire} ) {
1641 $self->dateexpiry( dt_from_string->subtract(days => 1) );
1644 if( $params->{remove} ) {
1645 $self->holds->delete;
1646 $self->article_requests->delete;
1653 Koha::Patrons->find($id)->anonymize;
1655 Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
1656 are randomized, other personal data is cleared too.
1657 Patrons with issues are skipped.
1663 if( $self->_result->issues->count ) {
1664 warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
1667 # Mandatory fields come from the corresponding pref, but email fields
1668 # are removed since scrambled email addresses only generate errors
1669 my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
1670 split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
1671 $mandatory->{userid} = 1; # needed since sub store does not clear field
1672 my @columns = $self->_result->result_source->columns;
1673 @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized/ } @columns;
1674 push @columns, 'dateofbirth'; # add this date back in
1675 foreach my $col (@columns) {
1676 $self->_anonymize_column($col, $mandatory->{lc $col} );
1678 $self->anonymized(1)->store;
1681 sub _anonymize_column {
1682 my ( $self, $col, $mandatory ) = @_;
1683 my $col_info = $self->_result->result_source->column_info($col);
1684 my $type = $col_info->{data_type};
1685 my $nullable = $col_info->{is_nullable};
1687 if( $type =~ /char|text/ ) {
1689 ? Koha::Token->new->generate({ pattern => '\w{10}' })
1693 } elsif( $type =~ /integer|int$|float|dec|double/ ) {
1694 $val = $nullable ? undef : 0;
1695 } elsif( $type =~ /date|time/ ) {
1696 $val = $nullable ? undef : dt_from_string;
1701 =head3 add_guarantor
1703 my @relationships = $patron->add_guarantor(
1705 borrowernumber => $borrowernumber,
1706 relationships => $relationship,
1710 Adds a new guarantor to a patron.
1715 my ( $self, $params ) = @_;
1717 my $guarantor_id = $params->{guarantor_id};
1718 my $relationship = $params->{relationship};
1720 return Koha::Patron::Relationship->new(
1722 guarantee_id => $self->id,
1723 guarantor_id => $guarantor_id,
1724 relationship => $relationship
1729 =head3 get_extended_attribute
1731 my $attribute_value = $patron->get_extended_attribute( $code );
1733 Return the attribute for the code passed in parameter.
1735 It not exist it returns undef
1737 Note that this will not work for repeatable attribute types.
1739 Maybe you certainly not want to use this method, it is actually only used for SHOW_BARCODE
1740 (which should be a real patron's attribute (not extended)
1744 sub get_extended_attribute {
1745 my ( $self, $code, $value ) = @_;
1746 my $rs = $self->_result->borrower_attributes;
1748 my $attribute = $rs->search({ code => $code, ( $value ? ( attribute => $value ) : () ) });
1749 return unless $attribute->count;
1750 return $attribute->next;
1755 my $json = $patron->to_api;
1757 Overloaded method that returns a JSON representation of the Koha::Patron object,
1758 suitable for API output.
1763 my ( $self, $params ) = @_;
1765 my $json_patron = $self->SUPER::to_api( $params );
1767 $json_patron->{restricted} = ( $self->is_debarred )
1769 : Mojo::JSON->false;
1771 return $json_patron;
1774 =head3 to_api_mapping
1776 This method returns the mapping for representing a Koha::Patron object
1781 sub to_api_mapping {
1783 borrowernotes => 'staff_notes',
1784 borrowernumber => 'patron_id',
1785 branchcode => 'library_id',
1786 categorycode => 'category_id',
1787 checkprevcheckout => 'check_previous_checkout',
1788 contactfirstname => undef, # Unused
1789 contactname => undef, # Unused
1790 contactnote => 'altaddress_notes',
1791 contacttitle => undef, # Unused
1792 dateenrolled => 'date_enrolled',
1793 dateexpiry => 'expiry_date',
1794 dateofbirth => 'date_of_birth',
1795 debarred => undef, # replaced by 'restricted'
1796 debarredcomment => undef, # calculated, API consumers will use /restrictions instead
1797 emailpro => 'secondary_email',
1798 flags => undef, # permissions manipulation handled in /permissions
1799 gonenoaddress => 'incorrect_address',
1800 guarantorid => 'guarantor_id',
1801 lastseen => 'last_seen',
1802 lost => 'patron_card_lost',
1803 opacnote => 'opac_notes',
1804 othernames => 'other_name',
1805 password => undef, # password manipulation handled in /password
1806 phonepro => 'secondary_phone',
1807 relationship => 'relationship_type',
1809 smsalertnumber => 'sms_number',
1810 sort1 => 'statistics_1',
1811 sort2 => 'statistics_2',
1812 autorenew_checkouts => 'autorenew_checkouts',
1813 streetnumber => 'street_number',
1814 streettype => 'street_type',
1815 zipcode => 'postal_code',
1816 B_address => 'altaddress_address',
1817 B_address2 => 'altaddress_address2',
1818 B_city => 'altaddress_city',
1819 B_country => 'altaddress_country',
1820 B_email => 'altaddress_email',
1821 B_phone => 'altaddress_phone',
1822 B_state => 'altaddress_state',
1823 B_streetnumber => 'altaddress_street_number',
1824 B_streettype => 'altaddress_street_type',
1825 B_zipcode => 'altaddress_postal_code',
1826 altcontactaddress1 => 'altcontact_address',
1827 altcontactaddress2 => 'altcontact_address2',
1828 altcontactaddress3 => 'altcontact_city',
1829 altcontactcountry => 'altcontact_country',
1830 altcontactfirstname => 'altcontact_firstname',
1831 altcontactphone => 'altcontact_phone',
1832 altcontactsurname => 'altcontact_surname',
1833 altcontactstate => 'altcontact_state',
1834 altcontactzipcode => 'altcontact_postal_code',
1835 primary_contact_method => undef,
1841 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_name => 'DUE'});
1842 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports });
1843 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports, test_mode => 1 });
1845 Queue messages to a patron. Can pass a message that is part of the message_attributes
1846 table or supply the transport to use.
1848 If passed a message name we retrieve the patrons preferences for transports
1849 Otherwise we use the supplied transport. In the case of email or sms we fall back to print if
1850 we have no address/number for sending
1852 $letter_params is a hashref of the values to be passed to GetPreparedLetter
1854 test_mode will only report which notices would be sent, but nothing will be queued
1859 my ( $self, $params ) = @_;
1860 my $letter_params = $params->{letter_params};
1861 my $test_mode = $params->{test_mode};
1863 return unless $letter_params;
1864 return unless exists $params->{message_name} xor $params->{message_transports}; # We only want one of these
1866 my $library = Koha::Libraries->find( $letter_params->{branchcode} );
1867 my $from_email_address = $library->from_email_address;
1869 my @message_transports;
1871 $letter_code = $letter_params->{letter_code};
1872 if( $params->{message_name} ){
1873 my $messaging_prefs = C4::Members::Messaging::GetMessagingPreferences( {
1874 borrowernumber => $letter_params->{borrowernumber},
1875 message_name => $params->{message_name}
1877 @message_transports = ( keys %{ $messaging_prefs->{transports} } );
1878 $letter_code = $messaging_prefs->{transports}->{$message_transports[0]} unless $letter_code;
1880 @message_transports = @{$params->{message_transports}};
1882 return unless defined $letter_code;
1883 $letter_params->{letter_code} = $letter_code;
1886 foreach my $mtt (@message_transports){
1887 next if ($mtt eq 'itiva' and C4::Context->preference('TalkingTechItivaPhoneNotification') );
1888 # Notice is handled by TalkingTech_itiva_outbound.pl
1889 if ( ( $mtt eq 'email' and not $self->notice_email_address )
1890 or ( $mtt eq 'sms' and not $self->smsalertnumber )
1891 or ( $mtt eq 'phone' and not $self->phone ) )
1893 push @{ $return{fallback} }, $mtt;
1896 next if $mtt eq 'print' && $print_sent;
1897 $letter_params->{message_transport_type} = $mtt;
1898 my $letter = C4::Letters::GetPreparedLetter( %$letter_params );
1899 C4::Letters::EnqueueLetter({
1901 borrowernumber => $self->borrowernumber,
1902 from_address => $from_email_address,
1903 message_transport_type => $mtt
1904 }) unless $test_mode;
1905 push @{$return{sent}}, $mtt;
1906 $print_sent = 1 if $mtt eq 'print';
1911 =head2 Internal methods
1923 Kyle M Hall <kyle@bywatersolutions.com>
1924 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1925 Martin Renvoize <martin.renvoize@ptfs-europe.com>