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::Messages;
48 use Koha::Patron::Modifications;
49 use Koha::Patron::Relationships;
53 use Koha::Result::Boolean;
54 use Koha::Subscription::Routinglists;
56 use Koha::Virtualshelves;
58 use base qw(Koha::Object);
60 use constant ADMINISTRATIVE_LOCKOUT => -1;
62 our $RESULTSET_PATRON_ID_MAPPING = {
63 Accountline => 'borrowernumber',
64 Aqbasketuser => 'borrowernumber',
65 Aqbudget => 'budget_owner_id',
66 Aqbudgetborrower => 'borrowernumber',
67 ArticleRequest => 'borrowernumber',
68 BorrowerDebarment => 'borrowernumber',
69 BorrowerFile => 'borrowernumber',
70 BorrowerModification => 'borrowernumber',
71 ClubEnrollment => 'borrowernumber',
72 Issue => 'borrowernumber',
73 ItemsLastBorrower => 'borrowernumber',
74 Linktracker => 'borrowernumber',
75 Message => 'borrowernumber',
76 MessageQueue => 'borrowernumber',
77 OldIssue => 'borrowernumber',
78 OldReserve => 'borrowernumber',
79 Rating => 'borrowernumber',
80 Reserve => 'borrowernumber',
81 Review => 'borrowernumber',
82 SearchHistory => 'userid',
83 Statistic => 'borrowernumber',
84 Suggestion => 'suggestedby',
85 TagAll => 'borrowernumber',
86 Virtualshelfcontent => 'borrowernumber',
87 Virtualshelfshare => 'borrowernumber',
88 Virtualshelve => 'owner',
93 Koha::Patron - Koha Patron Object class
104 my ( $class, $params ) = @_;
106 return $class->SUPER::new($params);
109 =head3 fixup_cardnumber
111 Autogenerate next cardnumber from highest value found in database
115 sub fixup_cardnumber {
118 my $max = $self->cardnumber;
119 Koha::Plugins->call( 'patron_barcode_transform', \$max );
121 $max ||= Koha::Patrons->search({
122 cardnumber => {-regexp => '^-?[0-9]+$'}
124 select => \'CAST(cardnumber AS SIGNED)',
125 as => ['cast_cardnumber']
126 })->_resultset->get_column('cast_cardnumber')->max;
127 $self->cardnumber(($max || 0) +1);
130 =head3 trim_whitespace
132 trim whitespace from data which has some non-whitespace in it.
133 Could be moved to Koha::Object if need to be reused
137 sub trim_whitespaces {
140 my $schema = Koha::Database->new->schema;
141 my @columns = $schema->source($self->_type)->columns;
143 for my $column( @columns ) {
144 my $value = $self->$column;
145 if ( defined $value ) {
146 $value =~ s/^\s*|\s*$//g;
147 $self->$column($value);
153 =head3 plain_text_password
155 $patron->plain_text_password( $password );
157 stores a copy of the unencrypted password in the object
158 for use in code before encrypting for db
162 sub plain_text_password {
163 my ( $self, $password ) = @_;
165 $self->{_plain_text_password} = $password;
168 return $self->{_plain_text_password}
169 if $self->{_plain_text_password};
176 Patron specific store method to cleanup record
177 and do other necessary things before saving
185 $self->_result->result_source->schema->txn_do(
188 C4::Context->preference("autoMemberNum")
189 and ( not defined $self->cardnumber
190 or $self->cardnumber eq '' )
193 # Warning: The caller is responsible for locking the members table in write
194 # mode, to avoid database corruption.
195 # We are in a transaction but the table is not locked
196 $self->fixup_cardnumber;
199 unless( $self->category->in_storage ) {
200 Koha::Exceptions::Object::FKConstraint->throw(
201 broken_fk => 'categorycode',
202 value => $self->categorycode,
206 $self->trim_whitespaces;
208 my $new_cardnumber = $self->cardnumber;
209 Koha::Plugins->call( 'patron_barcode_transform', \$new_cardnumber );
210 $self->cardnumber( $new_cardnumber );
212 # Set surname to uppercase if uppercasesurname is true
213 $self->surname( uc($self->surname) )
214 if C4::Context->preference("uppercasesurnames");
216 $self->relationship(undef) # We do not want to store an empty string in this field
217 if defined $self->relationship
218 and $self->relationship eq "";
220 unless ( $self->in_storage ) { #AddMember
222 # Generate a valid userid/login if needed
223 $self->generate_userid
224 if not $self->userid or not $self->has_valid_userid;
226 # Add expiration date if it isn't already there
227 unless ( $self->dateexpiry ) {
228 $self->dateexpiry( $self->category->get_expiry_date );
231 # Add enrollment date if it isn't already there
232 unless ( $self->dateenrolled ) {
233 $self->dateenrolled(dt_from_string);
236 # Set the privacy depending on the patron's category
237 my $default_privacy = $self->category->default_privacy || q{};
239 $default_privacy eq 'default' ? 1
240 : $default_privacy eq 'never' ? 2
241 : $default_privacy eq 'forever' ? 0
243 $self->privacy($default_privacy);
245 # Call any check_password plugins if password is passed
246 if ( C4::Context->config("enable_plugins") && $self->password ) {
247 my @plugins = Koha::Plugins->new()->GetPlugins({
248 method => 'check_password',
250 foreach my $plugin ( @plugins ) {
251 # This plugin hook will also be used by a plugin for the Norwegian national
252 # patron database. This is why we need to pass both the password and the
253 # borrowernumber to the plugin.
254 my $ret = $plugin->check_password(
256 password => $self->password,
257 borrowernumber => $self->borrowernumber
260 if ( $ret->{'error'} == 1 ) {
261 Koha::Exceptions::Password::Plugin->throw();
266 # Make a copy of the plain text password for later use
267 $self->plain_text_password( $self->password );
269 # Create a disabled account if no password provided
270 $self->password( $self->password
271 ? Koha::AuthUtils::hash_password( $self->password )
274 $self->borrowernumber(undef);
276 $self = $self->SUPER::store;
278 $self->add_enrolment_fee_if_needed(0);
280 logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
281 if C4::Context->preference("BorrowersLog");
285 my $self_from_storage = $self->get_from_storage;
286 # FIXME We should not deal with that here, callers have to do this job
287 # Moved from ModMember to prevent regressions
288 unless ( $self->userid ) {
289 my $stored_userid = $self_from_storage->userid;
290 $self->userid($stored_userid);
293 # Password must be updated using $self->set_password
294 $self->password($self_from_storage->password);
296 if ( $self->category->categorycode ne
297 $self_from_storage->category->categorycode )
299 # Add enrolement fee on category change if required
300 $self->add_enrolment_fee_if_needed(1)
301 if C4::Context->preference('FeeOnChangePatronCategory');
303 # Clean up guarantors on category change if required
304 $self->guarantor_relationships->delete
305 if ( $self->category->category_type ne 'C'
306 && $self->category->category_type ne 'P' );
311 if ( C4::Context->preference("BorrowersLog") ) {
313 my $from_storage = $self_from_storage->unblessed;
314 my $from_object = $self->unblessed;
315 my @skip_fields = (qw/lastseen updated_on/);
316 for my $key ( keys %{$from_storage} ) {
317 next if any { /$key/ } @skip_fields;
320 !defined( $from_storage->{$key} )
321 && defined( $from_object->{$key} )
323 || ( defined( $from_storage->{$key} )
324 && !defined( $from_object->{$key} ) )
326 defined( $from_storage->{$key} )
327 && defined( $from_object->{$key} )
328 && ( $from_storage->{$key} ne
329 $from_object->{$key} )
334 before => $from_storage->{$key},
335 after => $from_object->{$key}
340 if ( defined($info) ) {
344 $self->borrowernumber,
347 { utf8 => 1, pretty => 1, canonical => 1 }
354 $self = $self->SUPER::store;
365 Delete patron's holds, lists and finally the patron.
367 Lists owned by the borrower are deleted, but entries from the borrower to
368 other lists are kept.
375 my $anonymous_patron = C4::Context->preference("AnonymousPatron");
376 Koha::Exceptions::Patron::FailedDeleteAnonymousPatron->throw() if $anonymous_patron && $self->id eq $anonymous_patron;
378 $self->_result->result_source->schema->txn_do(
380 # Cancel Patron's holds
381 my $holds = $self->holds;
382 while( my $hold = $holds->next ){
386 # Delete all lists and all shares of this borrower
387 # Consistent with the approach Koha uses on deleting individual lists
388 # Note that entries in virtualshelfcontents added by this borrower to
389 # lists of others will be handled by a table constraint: the borrower
390 # is set to NULL in those entries.
392 # We could handle the above deletes via a constraint too.
393 # But a new BZ report 11889 has been opened to discuss another approach.
394 # Instead of deleting we could also disown lists (based on a pref).
395 # In that way we could save shared and public lists.
396 # The current table constraints support that idea now.
397 # This pref should then govern the results of other routines/methods such as
398 # Koha::Virtualshelf->new->delete too.
399 # FIXME Could be $patron->get_lists
400 $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } )->as_list;
402 # We cannot have a FK on borrower_modifications.borrowernumber, the table is also used
404 $_->delete for Koha::Patron::Modifications->search( { borrowernumber => $self->borrowernumber } )->as_list;
406 $self->SUPER::delete;
408 logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
417 my $patron_category = $patron->category
419 Return the patron category for this patron
425 return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
435 return Koha::Patron::Images->find( $self->borrowernumber );
440 Returns a Koha::Library object representing the patron's home library.
446 return Koha::Library->_new_from_dbic($self->_result->branchcode);
451 Returns a Koha::SMS::Provider object representing the patron's SMS provider.
457 my $sms_provider_rs = $self->_result->sms_provider;
458 return unless $sms_provider_rs;
459 return Koha::SMS::Provider->_new_from_dbic($sms_provider_rs);
462 =head3 guarantor_relationships
464 Returns Koha::Patron::Relationships object for this patron's guarantors
466 Returns the set of relationships for the patrons that are guarantors for this patron.
468 This is returned instead of a Koha::Patron object because the guarantor
469 may not exist as a patron in Koha. If this is true, the guarantors name
470 exists in the Koha::Patron::Relationship object and will have no guarantor_id.
474 sub guarantor_relationships {
477 return Koha::Patron::Relationships->search( { guarantee_id => $self->id } );
480 =head3 guarantee_relationships
482 Returns Koha::Patron::Relationships object for this patron's guarantors
484 Returns the set of relationships for the patrons that are guarantees for this patron.
486 The method returns Koha::Patron::Relationship objects for the sake
487 of consistency with the guantors method.
488 A guarantee by definition must exist as a patron in Koha.
492 sub guarantee_relationships {
495 return Koha::Patron::Relationships->search(
496 { guarantor_id => $self->id },
498 prefetch => 'guarantee',
499 order_by => { -asc => [ 'guarantee.surname', 'guarantee.firstname' ] },
504 =head3 relationships_debt
506 Returns the amount owed by the patron's guarantors *and* the other guarantees of those guarantors
510 sub relationships_debt {
511 my ($self, $params) = @_;
513 my $include_guarantors = $params->{include_guarantors};
514 my $only_this_guarantor = $params->{only_this_guarantor};
515 my $include_this_patron = $params->{include_this_patron};
518 if ( $only_this_guarantor ) {
519 @guarantors = $self->guarantee_relationships->count ? ( $self ) : ();
520 Koha::Exceptions::BadParameter->throw( { parameter => 'only_this_guarantor' } ) unless @guarantors;
521 } elsif ( $self->guarantor_relationships->count ) {
522 # I am a guarantee, just get all my guarantors
523 @guarantors = $self->guarantor_relationships->guarantors->as_list;
525 # I am a guarantor, I need to get all the guarantors of all my guarantees
526 @guarantors = map { $_->guarantor_relationships->guarantors->as_list } $self->guarantee_relationships->guarantees->as_list;
529 my $non_issues_charges = 0;
530 my $seen = $include_this_patron ? {} : { $self->id => 1 }; # For tracking members already added to the total
531 foreach my $guarantor (@guarantors) {
532 $non_issues_charges += $guarantor->account->non_issues_charges if $include_guarantors && !$seen->{ $guarantor->id };
534 # We've added what the guarantor owes, not added in that guarantor's guarantees as well
535 my @guarantees = map { $_->guarantee } $guarantor->guarantee_relationships->as_list;
536 my $guarantees_non_issues_charges = 0;
537 foreach my $guarantee (@guarantees) {
538 next if $seen->{ $guarantee->id };
539 $guarantees_non_issues_charges += $guarantee->account->non_issues_charges;
540 # Mark this guarantee as seen so we don't double count a guarantee linked to multiple guarantors
541 $seen->{ $guarantee->id } = 1;
544 $non_issues_charges += $guarantees_non_issues_charges;
545 $seen->{ $guarantor->id } = 1;
548 return $non_issues_charges;
551 =head3 housebound_profile
553 Returns the HouseboundProfile associated with this patron.
557 sub housebound_profile {
559 my $profile = $self->_result->housebound_profile;
560 return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
565 =head3 housebound_role
567 Returns the HouseboundRole associated with this patron.
571 sub housebound_role {
574 my $role = $self->_result->housebound_role;
575 return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
581 Returns the siblings of this patron.
588 my @guarantors = $self->guarantor_relationships()->guarantors()->as_list;
590 return unless @guarantors;
593 map { $_->guarantee_relationships()->guarantees()->as_list } @guarantors;
595 return unless @siblings;
599 grep { !$seen{ $_->id }++ && ( $_->id != $self->id ) } @siblings;
601 return Koha::Patrons->search( { borrowernumber => { -in => [ map { $_->id } @siblings ] } } );
606 my $patron = Koha::Patrons->find($id);
607 $patron->merge_with( \@patron_ids );
609 This subroutine merges a list of patrons into the patron record. This is accomplished by finding
610 all related patron ids for the patrons to be merged in other tables and changing the ids to be that
611 of the keeper patron.
616 my ( $self, $patron_ids ) = @_;
618 my $anonymous_patron = C4::Context->preference("AnonymousPatron");
619 return if $anonymous_patron && $self->id eq $anonymous_patron;
621 my @patron_ids = @{ $patron_ids };
623 # Ensure the keeper isn't in the list of patrons to merge
624 @patron_ids = grep { $_ ne $self->id } @patron_ids;
626 my $schema = Koha::Database->new()->schema();
630 $self->_result->result_source->schema->txn_do( sub {
631 foreach my $patron_id (@patron_ids) {
633 next if $patron_id eq $anonymous_patron;
635 my $patron = Koha::Patrons->find( $patron_id );
639 # Unbless for safety, the patron will end up being deleted
640 $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
642 my $attributes = $patron->extended_attributes;
643 my $new_attributes = [
644 map { { code => $_->code, attribute => $_->attribute } }
647 $attributes->delete; # We need to delete before trying to merge them to prevent exception on unique and repeatable
648 for my $attribute ( @$new_attributes ) {
649 $self->add_extended_attribute($attribute);
652 while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
653 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
654 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
655 $rs->update({ $field => $self->id });
656 if ( $r eq 'BorrowerDebarment' ) {
657 Koha::Patron::Debarments::UpdateBorrowerDebarmentFlags($self->id);
661 $patron->move_to_deleted();
671 =head3 wants_check_for_previous_checkout
673 $wants_check = $patron->wants_check_for_previous_checkout;
675 Return 1 if Koha needs to perform PrevIssue checking, else 0.
679 sub wants_check_for_previous_checkout {
681 my $syspref = C4::Context->preference("checkPrevCheckout");
684 ## Hard syspref trumps all
685 return 1 if ($syspref eq 'hardyes');
686 return 0 if ($syspref eq 'hardno');
687 ## Now, patron pref trumps all
688 return 1 if ($self->checkprevcheckout eq 'yes');
689 return 0 if ($self->checkprevcheckout eq 'no');
691 # More complex: patron inherits -> determine category preference
692 my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
693 return 1 if ($checkPrevCheckoutByCat eq 'yes');
694 return 0 if ($checkPrevCheckoutByCat eq 'no');
696 # Finally: category preference is inherit, default to 0
697 if ($syspref eq 'softyes') {
704 =head3 do_check_for_previous_checkout
706 $do_check = $patron->do_check_for_previous_checkout($item);
708 Return 1 if the bib associated with $ITEM has previously been checked out to
709 $PATRON, 0 otherwise.
713 sub do_check_for_previous_checkout {
714 my ( $self, $item ) = @_;
717 my $biblio = Koha::Biblios->find( $item->{biblionumber} );
718 if ( $biblio->is_serial ) {
719 push @item_nos, $item->{itemnumber};
721 # Get all itemnumbers for given bibliographic record.
722 @item_nos = $biblio->items->get_column( 'itemnumber' );
725 # Create (old)issues search criteria
727 borrowernumber => $self->borrowernumber,
728 itemnumber => \@item_nos,
731 my $delay = C4::Context->preference('CheckPrevCheckoutDelay') || 0;
733 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
734 my $newer_than = dt_from_string()->subtract( days => $delay );
735 $criteria->{'returndate'} = { '>' => $dtf->format_datetime($newer_than), };
738 # Check current issues table
739 my $issues = Koha::Checkouts->search($criteria);
740 return 1 if $issues->count; # 0 || N
742 # Check old issues table
743 my $old_issues = Koha::Old::Checkouts->search($criteria);
744 return $old_issues->count; # 0 || N
749 my $debarment_expiration = $patron->is_debarred;
751 Returns the date a patron debarment will expire, or undef if the patron is not
759 return unless $self->debarred;
760 return $self->debarred
761 if $self->debarred =~ '^9999'
762 or dt_from_string( $self->debarred ) > dt_from_string;
768 my $is_expired = $patron->is_expired;
770 Returns 1 if the patron is expired or 0;
776 return 0 unless $self->dateexpiry;
777 return 0 if $self->dateexpiry =~ '^9999';
778 return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
782 =head3 is_going_to_expire
784 my $is_going_to_expire = $patron->is_going_to_expire;
786 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
790 sub is_going_to_expire {
793 my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
795 return 0 unless $delay;
796 return 0 unless $self->dateexpiry;
797 return 0 if $self->dateexpiry =~ '^9999';
798 return 1 if dt_from_string( $self->dateexpiry, undef, 'floating' )->subtract( days => $delay ) < dt_from_string(undef, undef, 'floating')->truncate( to => 'day' );
804 $patron->set_password({ password => $plain_text_password [, skip_validation => 1 ] });
806 Set the patron's password.
810 The passed string is validated against the current password enforcement policy.
811 Validation can be skipped by passing the I<skip_validation> parameter.
813 Exceptions are thrown if the password is not good enough.
817 =item Koha::Exceptions::Password::TooShort
819 =item Koha::Exceptions::Password::WhitespaceCharacters
821 =item Koha::Exceptions::Password::TooWeak
823 =item Koha::Exceptions::Password::Plugin (if a "check password" plugin is enabled)
830 my ( $self, $args ) = @_;
832 my $password = $args->{password};
834 unless ( $args->{skip_validation} ) {
835 my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password, $self->category );
838 if ( $error eq 'too_short' ) {
839 my $min_length = $self->category->effective_min_password_length;
840 $min_length = 3 if not $min_length or $min_length < 3;
842 my $password_length = length($password);
843 Koha::Exceptions::Password::TooShort->throw(
844 length => $password_length, min_length => $min_length );
846 elsif ( $error eq 'has_whitespaces' ) {
847 Koha::Exceptions::Password::WhitespaceCharacters->throw();
849 elsif ( $error eq 'too_weak' ) {
850 Koha::Exceptions::Password::TooWeak->throw();
855 if ( C4::Context->config("enable_plugins") ) {
856 # Call any check_password plugins
857 my @plugins = Koha::Plugins->new()->GetPlugins({
858 method => 'check_password',
860 foreach my $plugin ( @plugins ) {
861 # This plugin hook will also be used by a plugin for the Norwegian national
862 # patron database. This is why we need to pass both the password and the
863 # borrowernumber to the plugin.
864 my $ret = $plugin->check_password(
866 password => $password,
867 borrowernumber => $self->borrowernumber
870 # This plugin hook will also be used by a plugin for the Norwegian national
871 # patron database. This is why we need to call the actual plugins and then
872 # check skip_validation afterwards.
873 if ( $ret->{'error'} == 1 && !$args->{skip_validation} ) {
874 Koha::Exceptions::Password::Plugin->throw();
879 my $digest = Koha::AuthUtils::hash_password($password);
881 # We do not want to call $self->store and retrieve password from DB
882 $self->password($digest);
883 $self->login_attempts(0);
886 logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" )
887 if C4::Context->preference("BorrowersLog");
895 my $new_expiry_date = $patron->renew_account
897 Extending the subscription to the expiry date.
904 if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
905 $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
908 C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
909 ? dt_from_string( $self->dateexpiry )
912 my $expiry_date = $self->category->get_expiry_date($date);
914 $self->dateexpiry($expiry_date);
915 $self->date_renewed( dt_from_string() );
918 $self->add_enrolment_fee_if_needed(1);
920 logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
921 return dt_from_string( $expiry_date )->truncate( to => 'day' );
926 my $has_overdues = $patron->has_overdues;
928 Returns the number of patron's overdues
934 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
935 return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
940 $patron->track_login;
941 $patron->track_login({ force => 1 });
943 Tracks a (successful) login attempt.
944 The preference TrackLastPatronActivity must be enabled. Or you
945 should pass the force parameter.
950 my ( $self, $params ) = @_;
953 !C4::Context->preference('TrackLastPatronActivity');
954 $self->lastseen( dt_from_string() )->store;
957 =head3 move_to_deleted
959 my $is_moved = $patron->move_to_deleted;
961 Move a patron to the deletedborrowers table.
962 This can be done before deleting a patron, to make sure the data are not completely deleted.
966 sub move_to_deleted {
968 my $patron_infos = $self->unblessed;
969 delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
970 return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
973 =head3 can_request_article
975 if ( $patron->can_request_article( $library->id ) ) { ... }
977 Returns true if the patron can request articles. As limits apply for the patron
978 on the same day, those completed the same day are considered as current.
980 A I<library_id> can be passed as parameter, falling back to userenv if absent.
984 sub can_request_article {
985 my ($self, $library_id) = @_;
987 $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
989 my $rule = Koha::CirculationRules->get_effective_rule(
991 branchcode => $library_id,
992 categorycode => $self->categorycode,
993 rule_name => 'open_article_requests_limit'
997 my $limit = ($rule) ? $rule->rule_value : undef;
999 return 1 unless defined $limit;
1001 my $count = Koha::ArticleRequests->search(
1002 [ { borrowernumber => $self->borrowernumber, status => [ 'REQUESTED', 'PENDING', 'PROCESSING' ] },
1003 { borrowernumber => $self->borrowernumber, status => 'COMPLETED', updated_on => { '>=' => \'CAST(NOW() AS DATE)' } },
1006 return $count < $limit ? 1 : 0;
1009 =head3 article_request_fee
1011 my $fee = $patron->article_request_fee(
1013 [ library_id => $library->id, ]
1017 Returns the fee to be charged to the patron when it places an article request.
1019 A I<library_id> can be passed as parameter, falling back to userenv if absent.
1023 sub article_request_fee {
1024 my ($self, $params) = @_;
1026 my $library_id = $params->{library_id};
1028 $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1030 my $rule = Koha::CirculationRules->get_effective_rule(
1032 branchcode => $library_id,
1033 categorycode => $self->categorycode,
1034 rule_name => 'article_request_fee'
1038 my $fee = ($rule) ? $rule->rule_value + 0 : 0;
1043 =head3 add_article_request_fee_if_needed
1045 my $fee = $patron->add_article_request_fee_if_needed(
1047 [ item_id => $item->id,
1048 library_id => $library->id, ]
1052 If an article request fee needs to be charged, it adds a debit to the patron's
1055 Returns the fee line.
1057 A I<library_id> can be passed as parameter, falling back to userenv if absent.
1061 sub add_article_request_fee_if_needed {
1062 my ($self, $params) = @_;
1064 my $library_id = $params->{library_id};
1065 my $item_id = $params->{item_id};
1067 $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1069 my $amount = $self->article_request_fee(
1071 library_id => $library_id,
1077 if ( $amount > 0 ) {
1078 $debit_line = $self->account->add_debit(
1081 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
1082 interface => C4::Context->interface,
1083 library_id => $library_id,
1084 type => 'ARTICLE_REQUEST',
1085 item_id => $item_id,
1093 =head3 article_requests
1095 my $article_requests = $patron->article_requests;
1097 Returns the patron article requests.
1101 sub article_requests {
1104 return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
1107 =head3 add_enrolment_fee_if_needed
1109 my $enrolment_fee = $patron->add_enrolment_fee_if_needed($renewal);
1111 Add enrolment fee for a patron if needed.
1113 $renewal - boolean denoting whether this is an account renewal or not
1117 sub add_enrolment_fee_if_needed {
1118 my ($self, $renewal) = @_;
1119 my $enrolment_fee = $self->category->enrolmentfee;
1120 if ( $enrolment_fee && $enrolment_fee > 0 ) {
1121 my $type = $renewal ? 'ACCOUNT_RENEW' : 'ACCOUNT';
1122 $self->account->add_debit(
1124 amount => $enrolment_fee,
1125 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
1126 interface => C4::Context->interface,
1127 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
1132 return $enrolment_fee || 0;
1137 my $checkouts = $patron->checkouts
1143 my $checkouts = $self->_result->issues;
1144 return Koha::Checkouts->_new_from_dbic( $checkouts );
1147 =head3 pending_checkouts
1149 my $pending_checkouts = $patron->pending_checkouts
1151 This method will return the same as $self->checkouts, but with a prefetch on
1152 items, biblio and biblioitems.
1154 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
1156 It should not be used directly, prefer to access fields you need instead of
1157 retrieving all these fields in one go.
1161 sub pending_checkouts {
1163 my $checkouts = $self->_result->issues->search(
1167 { -desc => 'me.timestamp' },
1168 { -desc => 'issuedate' },
1169 { -desc => 'issue_id' }, # Sort by issue_id should be enough
1171 prefetch => { item => { biblio => 'biblioitems' } },
1174 return Koha::Checkouts->_new_from_dbic( $checkouts );
1177 =head3 old_checkouts
1179 my $old_checkouts = $patron->old_checkouts
1185 my $old_checkouts = $self->_result->old_issues;
1186 return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
1191 my $overdue_items = $patron->get_overdues
1193 Return the overdue items
1199 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
1200 return $self->checkouts->search(
1202 'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
1205 prefetch => { item => { biblio => 'biblioitems' } },
1210 =head3 get_routing_lists
1212 my $routinglists = $patron->get_routing_lists
1214 Returns the routing lists a patron is subscribed to.
1218 sub get_routing_lists {
1220 my $routing_list_rs = $self->_result->subscriptionroutinglists;
1221 return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
1226 my $age = $patron->get_age
1228 Return the age of the patron
1235 return unless $self->dateofbirth;
1237 my $date_of_birth = dt_from_string( $self->dateofbirth );
1238 my $today = dt_from_string->truncate( to => 'day' );
1240 return $today->subtract_datetime( $date_of_birth )->years;
1245 my $is_valid = $patron->is_valid_age
1247 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
1253 my $age = $self->get_age;
1255 my $patroncategory = $self->category;
1256 my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
1258 return (defined($age) && (($high && ($age > $high)) or ($low && ($age < $low)))) ? 0 : 1;
1263 my $account = $patron->account
1269 return Koha::Account->new( { patron_id => $self->borrowernumber } );
1274 my $holds = $patron->holds
1276 Return all the holds placed by this patron
1282 my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1283 return Koha::Holds->_new_from_dbic($holds_rs);
1288 my $old_holds = $patron->old_holds
1290 Return all the historical holds for this patron
1296 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1297 return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1300 =head3 return_claims
1302 my $return_claims = $patron->return_claims
1308 my $return_claims = $self->_result->return_claims_borrowernumbers;
1309 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $return_claims );
1312 =head3 notice_email_address
1314 my $email = $patron->notice_email_address;
1316 Return the email address of patron used for notices.
1317 Returns the empty string if no email address.
1321 sub notice_email_address{
1324 my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1325 # if syspref is set to 'first valid' (value == OFF), look up email address
1326 if ( $which_address eq 'OFF' ) {
1327 return $self->first_valid_email_address;
1330 return $self->$which_address || '';
1333 =head3 first_valid_email_address
1335 my $first_valid_email_address = $patron->first_valid_email_address
1337 Return the first valid email address for a patron.
1338 For now, the order is defined as email, emailpro, B_email.
1339 Returns the empty string if the borrower has no email addresses.
1343 sub first_valid_email_address {
1346 return $self->email() || $self->emailpro() || $self->B_email() || q{};
1349 =head3 get_club_enrollments
1353 sub get_club_enrollments {
1356 return Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1359 =head3 get_enrollable_clubs
1363 sub get_enrollable_clubs {
1364 my ( $self, $is_enrollable_from_opac ) = @_;
1367 $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1368 if $is_enrollable_from_opac;
1369 $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1371 $params->{borrower} = $self;
1373 return Koha::Clubs->get_enrollable($params);
1376 =head3 account_locked
1378 my $is_locked = $patron->account_locked
1380 Return true if the patron has reached the maximum number of login attempts
1381 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1382 as an administrative lockout (independent of FailedLoginAttempts; see also
1383 Koha::Patron->lock).
1384 Otherwise return false.
1385 If the pref is not set (empty string, null or 0), the feature is considered as
1390 sub account_locked {
1392 my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1393 return 1 if $FailedLoginAttempts
1394 and $self->login_attempts
1395 and $self->login_attempts >= $FailedLoginAttempts;
1396 return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1400 =head3 can_see_patron_infos
1402 my $can_see = $patron->can_see_patron_infos( $patron );
1404 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1408 sub can_see_patron_infos {
1409 my ( $self, $patron ) = @_;
1410 return unless $patron;
1411 return $self->can_see_patrons_from( $patron->branchcode );
1414 =head3 can_see_patrons_from
1416 my $can_see = $patron->can_see_patrons_from( $branchcode );
1418 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1422 sub can_see_patrons_from {
1423 my ( $self, $branchcode ) = @_;
1425 if ( $self->branchcode eq $branchcode ) {
1427 } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1429 } elsif ( my $library_groups = $self->library->library_groups ) {
1430 while ( my $library_group = $library_groups->next ) {
1431 if ( $library_group->parent->has_child( $branchcode ) ) {
1442 my $can_log_into = $patron->can_log_into( $library );
1444 Given a I<Koha::Library> object, it returns a boolean representing
1445 the fact the patron can log into a the library.
1450 my ( $self, $library ) = @_;
1454 if ( C4::Context->preference('IndependentBranches') ) {
1456 if $self->is_superlibrarian
1457 or $self->branchcode eq $library->id;
1467 =head3 libraries_where_can_see_patrons
1469 my $libraries = $patron-libraries_where_can_see_patrons;
1471 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1472 The branchcodes are arbitrarily returned sorted.
1473 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1475 An empty array means no restriction, the patron can see patron's infos from any libraries.
1479 sub libraries_where_can_see_patrons {
1481 my $userenv = C4::Context->userenv;
1483 return () unless $userenv; # For tests, but userenv should be defined in tests...
1485 my @restricted_branchcodes;
1486 if (C4::Context::only_my_library) {
1487 push @restricted_branchcodes, $self->branchcode;
1491 $self->has_permission(
1492 { borrowers => 'view_borrower_infos_from_any_libraries' }
1496 my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1497 if ( $library_groups->count )
1499 while ( my $library_group = $library_groups->next ) {
1500 my $parent = $library_group->parent;
1501 if ( $parent->has_child( $self->branchcode ) ) {
1502 push @restricted_branchcodes, $parent->children->get_column('branchcode');
1507 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1511 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1512 @restricted_branchcodes = uniq(@restricted_branchcodes);
1513 @restricted_branchcodes = sort(@restricted_branchcodes);
1514 return @restricted_branchcodes;
1517 =head3 has_permission
1519 my $permission = $patron->has_permission($required);
1521 See C4::Auth::haspermission for details of syntax for $required
1525 sub has_permission {
1526 my ( $self, $flagsrequired ) = @_;
1527 return unless $self->userid;
1528 # TODO code from haspermission needs to be moved here!
1529 return C4::Auth::haspermission( $self->userid, $flagsrequired );
1532 =head3 is_superlibrarian
1534 my $is_superlibrarian = $patron->is_superlibrarian;
1536 Return true if the patron is a superlibrarian.
1540 sub is_superlibrarian {
1542 return $self->has_permission( { superlibrarian => 1 } ) ? 1 : 0;
1547 my $is_adult = $patron->is_adult
1549 Return true if the patron has a category with a type Adult (A) or Organization (I)
1555 return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1560 my $is_child = $patron->is_child
1562 Return true if the patron has a category with a type Child (C)
1568 return $self->category->category_type eq 'C' ? 1 : 0;
1571 =head3 has_valid_userid
1573 my $patron = Koha::Patrons->find(42);
1574 $patron->userid( $new_userid );
1575 my $has_a_valid_userid = $patron->has_valid_userid
1577 my $patron = Koha::Patron->new( $params );
1578 my $has_a_valid_userid = $patron->has_valid_userid
1580 Return true if the current userid of this patron is valid/unique, otherwise false.
1582 Note that this should be done in $self->store instead and raise an exception if needed.
1586 sub has_valid_userid {
1589 return 0 unless $self->userid;
1591 return 0 if ( $self->userid eq C4::Context->config('user') ); # DB user
1593 my $already_exists = Koha::Patrons->search(
1595 userid => $self->userid,
1598 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1603 return $already_exists ? 0 : 1;
1606 =head3 generate_userid
1608 my $patron = Koha::Patron->new( $params );
1609 $patron->generate_userid
1611 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1613 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).
1617 sub generate_userid {
1620 my $firstname = $self->firstname // q{};
1621 my $surname = $self->surname // q{};
1622 #The script will "do" the following code and increment the $offset until the generated userid is unique
1624 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1625 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1626 my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1627 $userid = NFKD( $userid );
1628 $userid =~ s/\p{NonspacingMark}//g;
1629 $userid .= $offset unless $offset == 0;
1630 $self->userid( $userid );
1632 } while (! $self->has_valid_userid );
1637 =head3 add_extended_attribute
1641 sub add_extended_attribute {
1642 my ($self, $attribute) = @_;
1644 return Koha::Patron::Attribute->new(
1647 ( borrowernumber => $self->borrowernumber ),
1653 =head3 extended_attributes
1655 Return object of Koha::Patron::Attributes type with all attributes set for this patron
1661 sub extended_attributes {
1662 my ( $self, $attributes ) = @_;
1663 if ($attributes) { # setter
1664 my $schema = $self->_result->result_source->schema;
1667 # Remove the existing one
1668 $self->extended_attributes->filter_by_branch_limitations->delete;
1670 # Insert the new ones
1672 for my $attribute (@$attributes) {
1673 $self->add_extended_attribute($attribute);
1674 $new_types->{$attribute->{code}} = 1;
1677 # Check globally mandatory types
1678 my @required_attribute_types =
1679 Koha::Patron::Attribute::Types->search(
1682 'borrower_attribute_types_branches.b_branchcode' =>
1685 { join => 'borrower_attribute_types_branches' }
1686 )->get_column('code');
1687 for my $type ( @required_attribute_types ) {
1688 Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute->throw(
1690 ) if !$new_types->{$type};
1696 my $rs = $self->_result->borrower_attributes;
1697 # We call search to use the filters in Koha::Patron::Attributes->search
1698 return Koha::Patron::Attributes->_new_from_dbic($rs)->search;
1703 my $messages = $patron->messages;
1705 Return the message attached to the patron.
1711 my $messages_rs = $self->_result->messages_borrowernumbers->search;
1712 return Koha::Patron::Messages->_new_from_dbic($messages_rs);
1717 Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
1719 Lock and optionally expire a patron account.
1720 Remove holds and article requests if remove flag set.
1721 In order to distinguish from locking by entering a wrong password, let's
1722 call this an administrative lockout.
1727 my ( $self, $params ) = @_;
1728 $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
1729 if( $params->{expire} ) {
1730 $self->dateexpiry( dt_from_string->subtract(days => 1) );
1733 if( $params->{remove} ) {
1734 $self->holds->delete;
1735 $self->article_requests->delete;
1742 Koha::Patrons->find($id)->anonymize;
1744 Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
1745 are randomized, other personal data is cleared too.
1746 Patrons with issues are skipped.
1752 if( $self->_result->issues->count ) {
1753 warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
1756 # Mandatory fields come from the corresponding pref, but email fields
1757 # are removed since scrambled email addresses only generate errors
1758 my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
1759 split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
1760 $mandatory->{userid} = 1; # needed since sub store does not clear field
1761 my @columns = $self->_result->result_source->columns;
1762 @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized/ } @columns;
1763 push @columns, 'dateofbirth'; # add this date back in
1764 foreach my $col (@columns) {
1765 $self->_anonymize_column($col, $mandatory->{lc $col} );
1767 $self->anonymized(1)->store;
1770 sub _anonymize_column {
1771 my ( $self, $col, $mandatory ) = @_;
1772 my $col_info = $self->_result->result_source->column_info($col);
1773 my $type = $col_info->{data_type};
1774 my $nullable = $col_info->{is_nullable};
1776 if( $type =~ /char|text/ ) {
1778 ? Koha::Token->new->generate({ pattern => '\w{10}' })
1782 } elsif( $type =~ /integer|int$|float|dec|double/ ) {
1783 $val = $nullable ? undef : 0;
1784 } elsif( $type =~ /date|time/ ) {
1785 $val = $nullable ? undef : dt_from_string;
1790 =head3 add_guarantor
1792 my $relationship = $patron->add_guarantor(
1794 borrowernumber => $borrowernumber,
1795 relationships => $relationship,
1799 Adds a new guarantor to a patron.
1804 my ( $self, $params ) = @_;
1806 my $guarantor_id = $params->{guarantor_id};
1807 my $relationship = $params->{relationship};
1809 return Koha::Patron::Relationship->new(
1811 guarantee_id => $self->id,
1812 guarantor_id => $guarantor_id,
1813 relationship => $relationship
1818 =head3 get_extended_attribute
1820 my $attribute_value = $patron->get_extended_attribute( $code );
1822 Return the attribute for the code passed in parameter.
1824 It not exist it returns undef
1826 Note that this will not work for repeatable attribute types.
1828 Maybe you certainly not want to use this method, it is actually only used for SHOW_BARCODE
1829 (which should be a real patron's attribute (not extended)
1833 sub get_extended_attribute {
1834 my ( $self, $code, $value ) = @_;
1835 my $rs = $self->_result->borrower_attributes;
1837 my $attribute = $rs->search({ code => $code, ( $value ? ( attribute => $value ) : () ) });
1838 return unless $attribute->count;
1839 return $attribute->next;
1844 my $json = $patron->to_api;
1846 Overloaded method that returns a JSON representation of the Koha::Patron object,
1847 suitable for API output.
1852 my ( $self, $params ) = @_;
1854 my $json_patron = $self->SUPER::to_api( $params );
1856 $json_patron->{restricted} = ( $self->is_debarred )
1858 : Mojo::JSON->false;
1860 return $json_patron;
1863 =head3 to_api_mapping
1865 This method returns the mapping for representing a Koha::Patron object
1870 sub to_api_mapping {
1872 borrowernotes => 'staff_notes',
1873 borrowernumber => 'patron_id',
1874 branchcode => 'library_id',
1875 categorycode => 'category_id',
1876 checkprevcheckout => 'check_previous_checkout',
1877 contactfirstname => undef, # Unused
1878 contactname => undef, # Unused
1879 contactnote => 'altaddress_notes',
1880 contacttitle => undef, # Unused
1881 dateenrolled => 'date_enrolled',
1882 dateexpiry => 'expiry_date',
1883 dateofbirth => 'date_of_birth',
1884 debarred => undef, # replaced by 'restricted'
1885 debarredcomment => undef, # calculated, API consumers will use /restrictions instead
1886 emailpro => 'secondary_email',
1887 flags => undef, # permissions manipulation handled in /permissions
1888 gonenoaddress => 'incorrect_address',
1889 guarantorid => 'guarantor_id',
1890 lastseen => 'last_seen',
1891 lost => 'patron_card_lost',
1892 opacnote => 'opac_notes',
1893 othernames => 'other_name',
1894 password => undef, # password manipulation handled in /password
1895 phonepro => 'secondary_phone',
1896 relationship => 'relationship_type',
1898 smsalertnumber => 'sms_number',
1899 sort1 => 'statistics_1',
1900 sort2 => 'statistics_2',
1901 autorenew_checkouts => 'autorenew_checkouts',
1902 streetnumber => 'street_number',
1903 streettype => 'street_type',
1904 zipcode => 'postal_code',
1905 B_address => 'altaddress_address',
1906 B_address2 => 'altaddress_address2',
1907 B_city => 'altaddress_city',
1908 B_country => 'altaddress_country',
1909 B_email => 'altaddress_email',
1910 B_phone => 'altaddress_phone',
1911 B_state => 'altaddress_state',
1912 B_streetnumber => 'altaddress_street_number',
1913 B_streettype => 'altaddress_street_type',
1914 B_zipcode => 'altaddress_postal_code',
1915 altcontactaddress1 => 'altcontact_address',
1916 altcontactaddress2 => 'altcontact_address2',
1917 altcontactaddress3 => 'altcontact_city',
1918 altcontactcountry => 'altcontact_country',
1919 altcontactfirstname => 'altcontact_firstname',
1920 altcontactphone => 'altcontact_phone',
1921 altcontactsurname => 'altcontact_surname',
1922 altcontactstate => 'altcontact_state',
1923 altcontactzipcode => 'altcontact_postal_code',
1924 primary_contact_method => undef,
1930 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_name => 'DUE'});
1931 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports });
1932 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports, test_mode => 1 });
1934 Queue messages to a patron. Can pass a message that is part of the message_attributes
1935 table or supply the transport to use.
1937 If passed a message name we retrieve the patrons preferences for transports
1938 Otherwise we use the supplied transport. In the case of email or sms we fall back to print if
1939 we have no address/number for sending
1941 $letter_params is a hashref of the values to be passed to GetPreparedLetter
1943 test_mode will only report which notices would be sent, but nothing will be queued
1948 my ( $self, $params ) = @_;
1949 my $letter_params = $params->{letter_params};
1950 my $test_mode = $params->{test_mode};
1952 return unless $letter_params;
1953 return unless exists $params->{message_name} xor $params->{message_transports}; # We only want one of these
1955 my $library = Koha::Libraries->find( $letter_params->{branchcode} );
1956 my $from_email_address = $library->from_email_address;
1958 my @message_transports;
1960 $letter_code = $letter_params->{letter_code};
1961 if( $params->{message_name} ){
1962 my $messaging_prefs = C4::Members::Messaging::GetMessagingPreferences( {
1963 borrowernumber => $letter_params->{borrowernumber},
1964 message_name => $params->{message_name}
1966 @message_transports = ( keys %{ $messaging_prefs->{transports} } );
1967 $letter_code = $messaging_prefs->{transports}->{$message_transports[0]} unless $letter_code;
1969 @message_transports = @{$params->{message_transports}};
1971 return unless defined $letter_code;
1972 $letter_params->{letter_code} = $letter_code;
1975 foreach my $mtt (@message_transports){
1976 next if ($mtt eq 'itiva' and C4::Context->preference('TalkingTechItivaPhoneNotification') );
1977 # Notice is handled by TalkingTech_itiva_outbound.pl
1978 if ( ( $mtt eq 'email' and not $self->notice_email_address )
1979 or ( $mtt eq 'sms' and not $self->smsalertnumber )
1980 or ( $mtt eq 'phone' and not $self->phone ) )
1982 push @{ $return{fallback} }, $mtt;
1985 next if $mtt eq 'print' && $print_sent;
1986 $letter_params->{message_transport_type} = $mtt;
1987 my $letter = C4::Letters::GetPreparedLetter( %$letter_params );
1988 C4::Letters::EnqueueLetter({
1990 borrowernumber => $self->borrowernumber,
1991 from_address => $from_email_address,
1992 message_transport_type => $mtt
1993 }) unless $test_mode;
1994 push @{$return{sent}}, $mtt;
1995 $print_sent = 1 if $mtt eq 'print';
2000 =head3 safe_to_delete
2002 my $result = $patron->safe_to_delete;
2003 if ( $result eq 'has_guarantees' ) { ... }
2004 elsif ( $result ) { ... }
2005 else { # cannot delete }
2007 This method tells if the Koha:Patron object can be deleted. Possible return values
2013 =item 'has_checkouts'
2017 =item 'has_guarantees'
2019 =item 'is_anonymous_patron'
2025 sub safe_to_delete {
2028 my $anonymous_patron = C4::Context->preference('AnonymousPatron');
2032 if ( $anonymous_patron && $self->id eq $anonymous_patron ) {
2033 $error = 'is_anonymous_patron';
2035 elsif ( $self->checkouts->count ) {
2036 $error = 'has_checkouts';
2038 elsif ( $self->account->outstanding_debits->total_outstanding > 0 ) {
2039 $error = 'has_debt';
2041 elsif ( $self->guarantee_relationships->count ) {
2042 $error = 'has_guarantees';
2046 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
2049 return Koha::Result::Boolean->new(1);
2054 my $recalls = $patron->recalls;
2056 Return the patron's recalls.
2063 return Koha::Recalls->search({ borrowernumber => $self->borrowernumber });
2066 =head2 Internal methods
2078 Kyle M Hall <kyle@bywatersolutions.com>
2079 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
2080 Martin Renvoize <martin.renvoize@ptfs-europe.com>