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( 'barcode_generate', 'patron', $self );
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( 'patron_barcode_transform', $self->cardnumber ) || $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 @requests = $borrower->article_requests();
1007 my $requests = $borrower->article_requests();
1009 Returns either a list of ArticleRequests objects,
1010 or an ArtitleRequests object, depending on the
1015 sub article_requests {
1018 $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
1020 return $self->{_article_requests};
1023 =head3 article_requests_current
1025 my @requests = $patron->article_requests_current
1027 Returns the article requests associated with this patron that are incomplete
1031 sub article_requests_current {
1034 $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
1036 borrowernumber => $self->id(),
1038 { status => Koha::ArticleRequest::Status::Requested },
1039 { status => Koha::ArticleRequest::Status::Pending },
1040 { status => Koha::ArticleRequest::Status::Processing }
1045 return $self->{_article_requests_current};
1048 =head3 article_requests_finished
1050 my @requests = $biblio->article_requests_finished
1052 Returns the article requests associated with this patron that are completed
1056 sub article_requests_finished {
1057 my ( $self, $borrower ) = @_;
1059 $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
1061 borrowernumber => $self->id(),
1063 { status => Koha::ArticleRequest::Status::Completed },
1064 { status => Koha::ArticleRequest::Status::Canceled }
1069 return $self->{_article_requests_finished};
1072 =head3 add_enrolment_fee_if_needed
1074 my $enrolment_fee = $patron->add_enrolment_fee_if_needed($renewal);
1076 Add enrolment fee for a patron if needed.
1078 $renewal - boolean denoting whether this is an account renewal or not
1082 sub add_enrolment_fee_if_needed {
1083 my ($self, $renewal) = @_;
1084 my $enrolment_fee = $self->category->enrolmentfee;
1085 if ( $enrolment_fee && $enrolment_fee > 0 ) {
1086 my $type = $renewal ? 'ACCOUNT_RENEW' : 'ACCOUNT';
1087 $self->account->add_debit(
1089 amount => $enrolment_fee,
1090 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
1091 interface => C4::Context->interface,
1092 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
1097 return $enrolment_fee || 0;
1102 my $checkouts = $patron->checkouts
1108 my $checkouts = $self->_result->issues;
1109 return Koha::Checkouts->_new_from_dbic( $checkouts );
1112 =head3 pending_checkouts
1114 my $pending_checkouts = $patron->pending_checkouts
1116 This method will return the same as $self->checkouts, but with a prefetch on
1117 items, biblio and biblioitems.
1119 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
1121 It should not be used directly, prefer to access fields you need instead of
1122 retrieving all these fields in one go.
1126 sub pending_checkouts {
1128 my $checkouts = $self->_result->issues->search(
1132 { -desc => 'me.timestamp' },
1133 { -desc => 'issuedate' },
1134 { -desc => 'issue_id' }, # Sort by issue_id should be enough
1136 prefetch => { item => { biblio => 'biblioitems' } },
1139 return Koha::Checkouts->_new_from_dbic( $checkouts );
1142 =head3 old_checkouts
1144 my $old_checkouts = $patron->old_checkouts
1150 my $old_checkouts = $self->_result->old_issues;
1151 return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
1156 my $overdue_items = $patron->get_overdues
1158 Return the overdue items
1164 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
1165 return $self->checkouts->search(
1167 'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
1170 prefetch => { item => { biblio => 'biblioitems' } },
1175 =head3 get_routing_lists
1177 my @routinglists = $patron->get_routing_lists
1179 Returns the routing lists a patron is subscribed to.
1183 sub get_routing_lists {
1185 my $routing_list_rs = $self->_result->subscriptionroutinglists;
1186 return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
1191 my $age = $patron->get_age
1193 Return the age of the patron
1199 my $today_str = dt_from_string->strftime("%Y-%m-%d");
1200 return unless $self->dateofbirth;
1201 my $dob_str = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
1203 my ( $dob_y, $dob_m, $dob_d ) = split /-/, $dob_str;
1204 my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
1206 my $age = $today_y - $dob_y;
1207 if ( $dob_m . $dob_d > $today_m . $today_d ) {
1216 my $is_valid = $patron->is_valid_age
1218 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
1224 my $age = $self->get_age;
1226 my $patroncategory = $self->category;
1227 my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
1229 return (defined($age) && (($high && ($age > $high)) or ($low && ($age < $low)))) ? 0 : 1;
1234 my $account = $patron->account
1240 return Koha::Account->new( { patron_id => $self->borrowernumber } );
1245 my $holds = $patron->holds
1247 Return all the holds placed by this patron
1253 my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1254 return Koha::Holds->_new_from_dbic($holds_rs);
1259 my $old_holds = $patron->old_holds
1261 Return all the historical holds for this patron
1267 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1268 return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1271 =head3 return_claims
1273 my $return_claims = $patron->return_claims
1279 my $return_claims = $self->_result->return_claims_borrowernumbers;
1280 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $return_claims );
1283 =head3 notice_email_address
1285 my $email = $patron->notice_email_address;
1287 Return the email address of patron used for notices.
1288 Returns the empty string if no email address.
1292 sub notice_email_address{
1295 my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1296 # if syspref is set to 'first valid' (value == OFF), look up email address
1297 if ( $which_address eq 'OFF' ) {
1298 return $self->first_valid_email_address;
1301 return $self->$which_address || '';
1304 =head3 first_valid_email_address
1306 my $first_valid_email_address = $patron->first_valid_email_address
1308 Return the first valid email address for a patron.
1309 For now, the order is defined as email, emailpro, B_email.
1310 Returns the empty string if the borrower has no email addresses.
1314 sub first_valid_email_address {
1317 return $self->email() || $self->emailpro() || $self->B_email() || q{};
1320 =head3 get_club_enrollments
1324 sub get_club_enrollments {
1325 my ( $self, $return_scalar ) = @_;
1327 my $e = Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1329 return $e if $return_scalar;
1331 return wantarray ? $e->as_list : $e;
1334 =head3 get_enrollable_clubs
1338 sub get_enrollable_clubs {
1339 my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
1342 $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1343 if $is_enrollable_from_opac;
1344 $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1346 $params->{borrower} = $self;
1348 my $e = Koha::Clubs->get_enrollable($params);
1350 return $e if $return_scalar;
1352 return wantarray ? $e->as_list : $e;
1355 =head3 account_locked
1357 my $is_locked = $patron->account_locked
1359 Return true if the patron has reached the maximum number of login attempts
1360 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1361 as an administrative lockout (independent of FailedLoginAttempts; see also
1362 Koha::Patron->lock).
1363 Otherwise return false.
1364 If the pref is not set (empty string, null or 0), the feature is considered as
1369 sub account_locked {
1371 my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1372 return 1 if $FailedLoginAttempts
1373 and $self->login_attempts
1374 and $self->login_attempts >= $FailedLoginAttempts;
1375 return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1379 =head3 can_see_patron_infos
1381 my $can_see = $patron->can_see_patron_infos( $patron );
1383 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1387 sub can_see_patron_infos {
1388 my ( $self, $patron ) = @_;
1389 return unless $patron;
1390 return $self->can_see_patrons_from( $patron->library->branchcode );
1393 =head3 can_see_patrons_from
1395 my $can_see = $patron->can_see_patrons_from( $branchcode );
1397 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1401 sub can_see_patrons_from {
1402 my ( $self, $branchcode ) = @_;
1404 if ( $self->branchcode eq $branchcode ) {
1406 } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1408 } elsif ( my $library_groups = $self->library->library_groups ) {
1409 while ( my $library_group = $library_groups->next ) {
1410 if ( $library_group->parent->has_child( $branchcode ) ) {
1421 my $can_log_into = $patron->can_log_into( $library );
1423 Given a I<Koha::Library> object, it returns a boolean representing
1424 the fact the patron can log into a the library.
1429 my ( $self, $library ) = @_;
1433 if ( C4::Context->preference('IndependentBranches') ) {
1435 if $self->is_superlibrarian
1436 or $self->branchcode eq $library->id;
1446 =head3 libraries_where_can_see_patrons
1448 my $libraries = $patron-libraries_where_can_see_patrons;
1450 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1451 The branchcodes are arbitrarily returned sorted.
1452 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1454 An empty array means no restriction, the patron can see patron's infos from any libraries.
1458 sub libraries_where_can_see_patrons {
1460 my $userenv = C4::Context->userenv;
1462 return () unless $userenv; # For tests, but userenv should be defined in tests...
1464 my @restricted_branchcodes;
1465 if (C4::Context::only_my_library) {
1466 push @restricted_branchcodes, $self->branchcode;
1470 $self->has_permission(
1471 { borrowers => 'view_borrower_infos_from_any_libraries' }
1475 my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1476 if ( $library_groups->count )
1478 while ( my $library_group = $library_groups->next ) {
1479 my $parent = $library_group->parent;
1480 if ( $parent->has_child( $self->branchcode ) ) {
1481 push @restricted_branchcodes, $parent->children->get_column('branchcode');
1486 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1490 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1491 @restricted_branchcodes = uniq(@restricted_branchcodes);
1492 @restricted_branchcodes = sort(@restricted_branchcodes);
1493 return @restricted_branchcodes;
1496 =head3 has_permission
1498 my $permission = $patron->has_permission($required);
1500 See C4::Auth::haspermission for details of syntax for $required
1504 sub has_permission {
1505 my ( $self, $flagsrequired ) = @_;
1506 return unless $self->userid;
1507 # TODO code from haspermission needs to be moved here!
1508 return C4::Auth::haspermission( $self->userid, $flagsrequired );
1511 =head3 is_superlibrarian
1513 my $is_superlibrarian = $patron->is_superlibrarian;
1515 Return true if the patron is a superlibrarian.
1519 sub is_superlibrarian {
1521 return $self->has_permission( { superlibrarian => 1 } ) ? 1 : 0;
1526 my $is_adult = $patron->is_adult
1528 Return true if the patron has a category with a type Adult (A) or Organization (I)
1534 return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1539 my $is_child = $patron->is_child
1541 Return true if the patron has a category with a type Child (C)
1547 return $self->category->category_type eq 'C' ? 1 : 0;
1550 =head3 has_valid_userid
1552 my $patron = Koha::Patrons->find(42);
1553 $patron->userid( $new_userid );
1554 my $has_a_valid_userid = $patron->has_valid_userid
1556 my $patron = Koha::Patron->new( $params );
1557 my $has_a_valid_userid = $patron->has_valid_userid
1559 Return true if the current userid of this patron is valid/unique, otherwise false.
1561 Note that this should be done in $self->store instead and raise an exception if needed.
1565 sub has_valid_userid {
1568 return 0 unless $self->userid;
1570 return 0 if ( $self->userid eq C4::Context->config('user') ); # DB user
1572 my $already_exists = Koha::Patrons->search(
1574 userid => $self->userid,
1577 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1582 return $already_exists ? 0 : 1;
1585 =head3 generate_userid
1587 my $patron = Koha::Patron->new( $params );
1588 $patron->generate_userid
1590 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1592 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).
1596 sub generate_userid {
1599 my $firstname = $self->firstname // q{};
1600 my $surname = $self->surname // q{};
1601 #The script will "do" the following code and increment the $offset until the generated userid is unique
1603 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1604 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1605 my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1606 $userid = NFKD( $userid );
1607 $userid =~ s/\p{NonspacingMark}//g;
1608 $userid .= $offset unless $offset == 0;
1609 $self->userid( $userid );
1611 } while (! $self->has_valid_userid );
1616 =head3 add_extended_attribute
1620 sub add_extended_attribute {
1621 my ($self, $attribute) = @_;
1623 return Koha::Patron::Attribute->new(
1626 ( borrowernumber => $self->borrowernumber ),
1632 =head3 extended_attributes
1634 Return object of Koha::Patron::Attributes type with all attributes set for this patron
1640 sub extended_attributes {
1641 my ( $self, $attributes ) = @_;
1642 if ($attributes) { # setter
1643 my $schema = $self->_result->result_source->schema;
1646 # Remove the existing one
1647 $self->extended_attributes->filter_by_branch_limitations->delete;
1649 # Insert the new ones
1651 for my $attribute (@$attributes) {
1652 $self->add_extended_attribute($attribute);
1653 $new_types->{$attribute->{code}} = 1;
1656 # Check globally mandatory types
1657 my @required_attribute_types =
1658 Koha::Patron::Attribute::Types->search(
1661 'borrower_attribute_types_branches.b_branchcode' =>
1664 { join => 'borrower_attribute_types_branches' }
1665 )->get_column('code');
1666 for my $type ( @required_attribute_types ) {
1667 Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute->throw(
1669 ) if !$new_types->{$type};
1675 my $rs = $self->_result->borrower_attributes;
1676 # We call search to use the filters in Koha::Patron::Attributes->search
1677 return Koha::Patron::Attributes->_new_from_dbic($rs)->search;
1682 Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
1684 Lock and optionally expire a patron account.
1685 Remove holds and article requests if remove flag set.
1686 In order to distinguish from locking by entering a wrong password, let's
1687 call this an administrative lockout.
1692 my ( $self, $params ) = @_;
1693 $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
1694 if( $params->{expire} ) {
1695 $self->dateexpiry( dt_from_string->subtract(days => 1) );
1698 if( $params->{remove} ) {
1699 $self->holds->delete;
1700 $self->article_requests->delete;
1707 Koha::Patrons->find($id)->anonymize;
1709 Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
1710 are randomized, other personal data is cleared too.
1711 Patrons with issues are skipped.
1717 if( $self->_result->issues->count ) {
1718 warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
1721 # Mandatory fields come from the corresponding pref, but email fields
1722 # are removed since scrambled email addresses only generate errors
1723 my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
1724 split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
1725 $mandatory->{userid} = 1; # needed since sub store does not clear field
1726 my @columns = $self->_result->result_source->columns;
1727 @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized/ } @columns;
1728 push @columns, 'dateofbirth'; # add this date back in
1729 foreach my $col (@columns) {
1730 $self->_anonymize_column($col, $mandatory->{lc $col} );
1732 $self->anonymized(1)->store;
1735 sub _anonymize_column {
1736 my ( $self, $col, $mandatory ) = @_;
1737 my $col_info = $self->_result->result_source->column_info($col);
1738 my $type = $col_info->{data_type};
1739 my $nullable = $col_info->{is_nullable};
1741 if( $type =~ /char|text/ ) {
1743 ? Koha::Token->new->generate({ pattern => '\w{10}' })
1747 } elsif( $type =~ /integer|int$|float|dec|double/ ) {
1748 $val = $nullable ? undef : 0;
1749 } elsif( $type =~ /date|time/ ) {
1750 $val = $nullable ? undef : dt_from_string;
1755 =head3 add_guarantor
1757 my @relationships = $patron->add_guarantor(
1759 borrowernumber => $borrowernumber,
1760 relationships => $relationship,
1764 Adds a new guarantor to a patron.
1769 my ( $self, $params ) = @_;
1771 my $guarantor_id = $params->{guarantor_id};
1772 my $relationship = $params->{relationship};
1774 return Koha::Patron::Relationship->new(
1776 guarantee_id => $self->id,
1777 guarantor_id => $guarantor_id,
1778 relationship => $relationship
1783 =head3 get_extended_attribute
1785 my $attribute_value = $patron->get_extended_attribute( $code );
1787 Return the attribute for the code passed in parameter.
1789 It not exist it returns undef
1791 Note that this will not work for repeatable attribute types.
1793 Maybe you certainly not want to use this method, it is actually only used for SHOW_BARCODE
1794 (which should be a real patron's attribute (not extended)
1798 sub get_extended_attribute {
1799 my ( $self, $code, $value ) = @_;
1800 my $rs = $self->_result->borrower_attributes;
1802 my $attribute = $rs->search({ code => $code, ( $value ? ( attribute => $value ) : () ) });
1803 return unless $attribute->count;
1804 return $attribute->next;
1809 my $json = $patron->to_api;
1811 Overloaded method that returns a JSON representation of the Koha::Patron object,
1812 suitable for API output.
1817 my ( $self, $params ) = @_;
1819 my $json_patron = $self->SUPER::to_api( $params );
1821 $json_patron->{restricted} = ( $self->is_debarred )
1823 : Mojo::JSON->false;
1825 return $json_patron;
1828 =head3 to_api_mapping
1830 This method returns the mapping for representing a Koha::Patron object
1835 sub to_api_mapping {
1837 borrowernotes => 'staff_notes',
1838 borrowernumber => 'patron_id',
1839 branchcode => 'library_id',
1840 categorycode => 'category_id',
1841 checkprevcheckout => 'check_previous_checkout',
1842 contactfirstname => undef, # Unused
1843 contactname => undef, # Unused
1844 contactnote => 'altaddress_notes',
1845 contacttitle => undef, # Unused
1846 dateenrolled => 'date_enrolled',
1847 dateexpiry => 'expiry_date',
1848 dateofbirth => 'date_of_birth',
1849 debarred => undef, # replaced by 'restricted'
1850 debarredcomment => undef, # calculated, API consumers will use /restrictions instead
1851 emailpro => 'secondary_email',
1852 flags => undef, # permissions manipulation handled in /permissions
1853 gonenoaddress => 'incorrect_address',
1854 guarantorid => 'guarantor_id',
1855 lastseen => 'last_seen',
1856 lost => 'patron_card_lost',
1857 opacnote => 'opac_notes',
1858 othernames => 'other_name',
1859 password => undef, # password manipulation handled in /password
1860 phonepro => 'secondary_phone',
1861 relationship => 'relationship_type',
1863 smsalertnumber => 'sms_number',
1864 sort1 => 'statistics_1',
1865 sort2 => 'statistics_2',
1866 autorenew_checkouts => 'autorenew_checkouts',
1867 streetnumber => 'street_number',
1868 streettype => 'street_type',
1869 zipcode => 'postal_code',
1870 B_address => 'altaddress_address',
1871 B_address2 => 'altaddress_address2',
1872 B_city => 'altaddress_city',
1873 B_country => 'altaddress_country',
1874 B_email => 'altaddress_email',
1875 B_phone => 'altaddress_phone',
1876 B_state => 'altaddress_state',
1877 B_streetnumber => 'altaddress_street_number',
1878 B_streettype => 'altaddress_street_type',
1879 B_zipcode => 'altaddress_postal_code',
1880 altcontactaddress1 => 'altcontact_address',
1881 altcontactaddress2 => 'altcontact_address2',
1882 altcontactaddress3 => 'altcontact_city',
1883 altcontactcountry => 'altcontact_country',
1884 altcontactfirstname => 'altcontact_firstname',
1885 altcontactphone => 'altcontact_phone',
1886 altcontactsurname => 'altcontact_surname',
1887 altcontactstate => 'altcontact_state',
1888 altcontactzipcode => 'altcontact_postal_code',
1889 primary_contact_method => undef,
1895 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_name => 'DUE'});
1896 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports });
1897 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports, test_mode => 1 });
1899 Queue messages to a patron. Can pass a message that is part of the message_attributes
1900 table or supply the transport to use.
1902 If passed a message name we retrieve the patrons preferences for transports
1903 Otherwise we use the supplied transport. In the case of email or sms we fall back to print if
1904 we have no address/number for sending
1906 $letter_params is a hashref of the values to be passed to GetPreparedLetter
1908 test_mode will only report which notices would be sent, but nothing will be queued
1913 my ( $self, $params ) = @_;
1914 my $letter_params = $params->{letter_params};
1915 my $test_mode = $params->{test_mode};
1917 return unless $letter_params;
1918 return unless exists $params->{message_name} xor $params->{message_transports}; # We only want one of these
1920 my $library = Koha::Libraries->find( $letter_params->{branchcode} );
1921 my $from_email_address = $library->from_email_address;
1923 my @message_transports;
1925 $letter_code = $letter_params->{letter_code};
1926 if( $params->{message_name} ){
1927 my $messaging_prefs = C4::Members::Messaging::GetMessagingPreferences( {
1928 borrowernumber => $letter_params->{borrowernumber},
1929 message_name => $params->{message_name}
1931 @message_transports = ( keys %{ $messaging_prefs->{transports} } );
1932 $letter_code = $messaging_prefs->{transports}->{$message_transports[0]} unless $letter_code;
1934 @message_transports = @{$params->{message_transports}};
1936 return unless defined $letter_code;
1937 $letter_params->{letter_code} = $letter_code;
1940 foreach my $mtt (@message_transports){
1941 next if ($mtt eq 'itiva' and C4::Context->preference('TalkingTechItivaPhoneNotification') );
1942 # Notice is handled by TalkingTech_itiva_outbound.pl
1943 if ( ( $mtt eq 'email' and not $self->notice_email_address )
1944 or ( $mtt eq 'sms' and not $self->smsalertnumber )
1945 or ( $mtt eq 'phone' and not $self->phone ) )
1947 push @{ $return{fallback} }, $mtt;
1950 next if $mtt eq 'print' && $print_sent;
1951 $letter_params->{message_transport_type} = $mtt;
1952 my $letter = C4::Letters::GetPreparedLetter( %$letter_params );
1953 C4::Letters::EnqueueLetter({
1955 borrowernumber => $self->borrowernumber,
1956 from_address => $from_email_address,
1957 message_transport_type => $mtt
1958 }) unless $test_mode;
1959 push @{$return{sent}}, $mtt;
1960 $print_sent = 1 if $mtt eq 'print';
1965 =head2 Internal methods
1977 Kyle M Hall <kyle@bywatersolutions.com>
1978 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1979 Martin Renvoize <martin.renvoize@ptfs-europe.com>