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 sub overdues { my $self = shift; return $self->get_overdues(@_); }
1212 =head3 get_routing_lists
1214 my $routinglists = $patron->get_routing_lists
1216 Returns the routing lists a patron is subscribed to.
1220 sub get_routing_lists {
1222 my $routing_list_rs = $self->_result->subscriptionroutinglists;
1223 return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
1228 my $age = $patron->get_age
1230 Return the age of the patron
1237 return unless $self->dateofbirth;
1239 my $date_of_birth = dt_from_string( $self->dateofbirth );
1240 my $today = dt_from_string->truncate( to => 'day' );
1242 return $today->subtract_datetime( $date_of_birth )->years;
1247 my $is_valid = $patron->is_valid_age
1249 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
1255 my $age = $self->get_age;
1257 my $patroncategory = $self->category;
1258 my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
1260 return (defined($age) && (($high && ($age > $high)) or ($low && ($age < $low)))) ? 0 : 1;
1265 my $account = $patron->account
1271 return Koha::Account->new( { patron_id => $self->borrowernumber } );
1276 my $holds = $patron->holds
1278 Return all the holds placed by this patron
1284 my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1285 return Koha::Holds->_new_from_dbic($holds_rs);
1290 my $old_holds = $patron->old_holds
1292 Return all the historical holds for this patron
1298 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1299 return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1302 =head3 return_claims
1304 my $return_claims = $patron->return_claims
1310 my $return_claims = $self->_result->return_claims_borrowernumbers;
1311 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $return_claims );
1314 =head3 notice_email_address
1316 my $email = $patron->notice_email_address;
1318 Return the email address of patron used for notices.
1319 Returns the empty string if no email address.
1323 sub notice_email_address{
1326 my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1327 # if syspref is set to 'first valid' (value == OFF), look up email address
1328 if ( $which_address eq 'OFF' ) {
1329 return $self->first_valid_email_address;
1332 return $self->$which_address || '';
1335 =head3 first_valid_email_address
1337 my $first_valid_email_address = $patron->first_valid_email_address
1339 Return the first valid email address for a patron.
1340 For now, the order is defined as email, emailpro, B_email.
1341 Returns the empty string if the borrower has no email addresses.
1345 sub first_valid_email_address {
1348 return $self->email() || $self->emailpro() || $self->B_email() || q{};
1351 =head3 get_club_enrollments
1355 sub get_club_enrollments {
1358 return Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1361 =head3 get_enrollable_clubs
1365 sub get_enrollable_clubs {
1366 my ( $self, $is_enrollable_from_opac ) = @_;
1369 $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1370 if $is_enrollable_from_opac;
1371 $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1373 $params->{borrower} = $self;
1375 return Koha::Clubs->get_enrollable($params);
1378 =head3 account_locked
1380 my $is_locked = $patron->account_locked
1382 Return true if the patron has reached the maximum number of login attempts
1383 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1384 as an administrative lockout (independent of FailedLoginAttempts; see also
1385 Koha::Patron->lock).
1386 Otherwise return false.
1387 If the pref is not set (empty string, null or 0), the feature is considered as
1392 sub account_locked {
1394 my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1395 return 1 if $FailedLoginAttempts
1396 and $self->login_attempts
1397 and $self->login_attempts >= $FailedLoginAttempts;
1398 return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1402 =head3 can_see_patron_infos
1404 my $can_see = $patron->can_see_patron_infos( $patron );
1406 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1410 sub can_see_patron_infos {
1411 my ( $self, $patron ) = @_;
1412 return unless $patron;
1413 return $self->can_see_patrons_from( $patron->branchcode );
1416 =head3 can_see_patrons_from
1418 my $can_see = $patron->can_see_patrons_from( $branchcode );
1420 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1424 sub can_see_patrons_from {
1425 my ( $self, $branchcode ) = @_;
1427 if ( $self->branchcode eq $branchcode ) {
1429 } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1431 } elsif ( my $library_groups = $self->library->library_groups ) {
1432 while ( my $library_group = $library_groups->next ) {
1433 if ( $library_group->parent->has_child( $branchcode ) ) {
1444 my $can_log_into = $patron->can_log_into( $library );
1446 Given a I<Koha::Library> object, it returns a boolean representing
1447 the fact the patron can log into a the library.
1452 my ( $self, $library ) = @_;
1456 if ( C4::Context->preference('IndependentBranches') ) {
1458 if $self->is_superlibrarian
1459 or $self->branchcode eq $library->id;
1469 =head3 libraries_where_can_see_patrons
1471 my $libraries = $patron-libraries_where_can_see_patrons;
1473 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1474 The branchcodes are arbitrarily returned sorted.
1475 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1477 An empty array means no restriction, the patron can see patron's infos from any libraries.
1481 sub libraries_where_can_see_patrons {
1483 my $userenv = C4::Context->userenv;
1485 return () unless $userenv; # For tests, but userenv should be defined in tests...
1487 my @restricted_branchcodes;
1488 if (C4::Context::only_my_library) {
1489 push @restricted_branchcodes, $self->branchcode;
1493 $self->has_permission(
1494 { borrowers => 'view_borrower_infos_from_any_libraries' }
1498 my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1499 if ( $library_groups->count )
1501 while ( my $library_group = $library_groups->next ) {
1502 my $parent = $library_group->parent;
1503 if ( $parent->has_child( $self->branchcode ) ) {
1504 push @restricted_branchcodes, $parent->children->get_column('branchcode');
1509 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1513 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1514 @restricted_branchcodes = uniq(@restricted_branchcodes);
1515 @restricted_branchcodes = sort(@restricted_branchcodes);
1516 return @restricted_branchcodes;
1519 =head3 has_permission
1521 my $permission = $patron->has_permission($required);
1523 See C4::Auth::haspermission for details of syntax for $required
1527 sub has_permission {
1528 my ( $self, $flagsrequired ) = @_;
1529 return unless $self->userid;
1530 # TODO code from haspermission needs to be moved here!
1531 return C4::Auth::haspermission( $self->userid, $flagsrequired );
1534 =head3 is_superlibrarian
1536 my $is_superlibrarian = $patron->is_superlibrarian;
1538 Return true if the patron is a superlibrarian.
1542 sub is_superlibrarian {
1544 return $self->has_permission( { superlibrarian => 1 } ) ? 1 : 0;
1549 my $is_adult = $patron->is_adult
1551 Return true if the patron has a category with a type Adult (A) or Organization (I)
1557 return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1562 my $is_child = $patron->is_child
1564 Return true if the patron has a category with a type Child (C)
1570 return $self->category->category_type eq 'C' ? 1 : 0;
1573 =head3 has_valid_userid
1575 my $patron = Koha::Patrons->find(42);
1576 $patron->userid( $new_userid );
1577 my $has_a_valid_userid = $patron->has_valid_userid
1579 my $patron = Koha::Patron->new( $params );
1580 my $has_a_valid_userid = $patron->has_valid_userid
1582 Return true if the current userid of this patron is valid/unique, otherwise false.
1584 Note that this should be done in $self->store instead and raise an exception if needed.
1588 sub has_valid_userid {
1591 return 0 unless $self->userid;
1593 return 0 if ( $self->userid eq C4::Context->config('user') ); # DB user
1595 my $already_exists = Koha::Patrons->search(
1597 userid => $self->userid,
1600 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1605 return $already_exists ? 0 : 1;
1608 =head3 generate_userid
1610 my $patron = Koha::Patron->new( $params );
1611 $patron->generate_userid
1613 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1615 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).
1619 sub generate_userid {
1622 my $firstname = $self->firstname // q{};
1623 my $surname = $self->surname // q{};
1624 #The script will "do" the following code and increment the $offset until the generated userid is unique
1626 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1627 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1628 my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1629 $userid = NFKD( $userid );
1630 $userid =~ s/\p{NonspacingMark}//g;
1631 $userid .= $offset unless $offset == 0;
1632 $self->userid( $userid );
1634 } while (! $self->has_valid_userid );
1639 =head3 add_extended_attribute
1643 sub add_extended_attribute {
1644 my ($self, $attribute) = @_;
1646 return Koha::Patron::Attribute->new(
1649 ( borrowernumber => $self->borrowernumber ),
1655 =head3 extended_attributes
1657 Return object of Koha::Patron::Attributes type with all attributes set for this patron
1663 sub extended_attributes {
1664 my ( $self, $attributes ) = @_;
1665 if ($attributes) { # setter
1666 my $schema = $self->_result->result_source->schema;
1669 # Remove the existing one
1670 $self->extended_attributes->filter_by_branch_limitations->delete;
1672 # Insert the new ones
1674 for my $attribute (@$attributes) {
1675 $self->add_extended_attribute($attribute);
1676 $new_types->{$attribute->{code}} = 1;
1679 # Check globally mandatory types
1680 my @required_attribute_types =
1681 Koha::Patron::Attribute::Types->search(
1684 'borrower_attribute_types_branches.b_branchcode' =>
1687 { join => 'borrower_attribute_types_branches' }
1688 )->get_column('code');
1689 for my $type ( @required_attribute_types ) {
1690 Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute->throw(
1692 ) if !$new_types->{$type};
1698 my $rs = $self->_result->borrower_attributes;
1699 # We call search to use the filters in Koha::Patron::Attributes->search
1700 return Koha::Patron::Attributes->_new_from_dbic($rs)->search;
1705 my $messages = $patron->messages;
1707 Return the message attached to the patron.
1713 my $messages_rs = $self->_result->messages_borrowernumbers->search;
1714 return Koha::Patron::Messages->_new_from_dbic($messages_rs);
1719 Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
1721 Lock and optionally expire a patron account.
1722 Remove holds and article requests if remove flag set.
1723 In order to distinguish from locking by entering a wrong password, let's
1724 call this an administrative lockout.
1729 my ( $self, $params ) = @_;
1730 $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
1731 if( $params->{expire} ) {
1732 $self->dateexpiry( dt_from_string->subtract(days => 1) );
1735 if( $params->{remove} ) {
1736 $self->holds->delete;
1737 $self->article_requests->delete;
1744 Koha::Patrons->find($id)->anonymize;
1746 Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
1747 are randomized, other personal data is cleared too.
1748 Patrons with issues are skipped.
1754 if( $self->_result->issues->count ) {
1755 warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
1758 # Mandatory fields come from the corresponding pref, but email fields
1759 # are removed since scrambled email addresses only generate errors
1760 my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
1761 split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
1762 $mandatory->{userid} = 1; # needed since sub store does not clear field
1763 my @columns = $self->_result->result_source->columns;
1764 @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized/ } @columns;
1765 push @columns, 'dateofbirth'; # add this date back in
1766 foreach my $col (@columns) {
1767 $self->_anonymize_column($col, $mandatory->{lc $col} );
1769 $self->anonymized(1)->store;
1772 sub _anonymize_column {
1773 my ( $self, $col, $mandatory ) = @_;
1774 my $col_info = $self->_result->result_source->column_info($col);
1775 my $type = $col_info->{data_type};
1776 my $nullable = $col_info->{is_nullable};
1778 if( $type =~ /char|text/ ) {
1780 ? Koha::Token->new->generate({ pattern => '\w{10}' })
1784 } elsif( $type =~ /integer|int$|float|dec|double/ ) {
1785 $val = $nullable ? undef : 0;
1786 } elsif( $type =~ /date|time/ ) {
1787 $val = $nullable ? undef : dt_from_string;
1792 =head3 add_guarantor
1794 my $relationship = $patron->add_guarantor(
1796 borrowernumber => $borrowernumber,
1797 relationships => $relationship,
1801 Adds a new guarantor to a patron.
1806 my ( $self, $params ) = @_;
1808 my $guarantor_id = $params->{guarantor_id};
1809 my $relationship = $params->{relationship};
1811 return Koha::Patron::Relationship->new(
1813 guarantee_id => $self->id,
1814 guarantor_id => $guarantor_id,
1815 relationship => $relationship
1820 =head3 get_extended_attribute
1822 my $attribute_value = $patron->get_extended_attribute( $code );
1824 Return the attribute for the code passed in parameter.
1826 It not exist it returns undef
1828 Note that this will not work for repeatable attribute types.
1830 Maybe you certainly not want to use this method, it is actually only used for SHOW_BARCODE
1831 (which should be a real patron's attribute (not extended)
1835 sub get_extended_attribute {
1836 my ( $self, $code, $value ) = @_;
1837 my $rs = $self->_result->borrower_attributes;
1839 my $attribute = $rs->search({ code => $code, ( $value ? ( attribute => $value ) : () ) });
1840 return unless $attribute->count;
1841 return $attribute->next;
1846 my $json = $patron->to_api;
1848 Overloaded method that returns a JSON representation of the Koha::Patron object,
1849 suitable for API output.
1854 my ( $self, $params ) = @_;
1856 my $json_patron = $self->SUPER::to_api( $params );
1858 $json_patron->{restricted} = ( $self->is_debarred )
1860 : Mojo::JSON->false;
1862 return $json_patron;
1865 =head3 to_api_mapping
1867 This method returns the mapping for representing a Koha::Patron object
1872 sub to_api_mapping {
1874 borrowernotes => 'staff_notes',
1875 borrowernumber => 'patron_id',
1876 branchcode => 'library_id',
1877 categorycode => 'category_id',
1878 checkprevcheckout => 'check_previous_checkout',
1879 contactfirstname => undef, # Unused
1880 contactname => undef, # Unused
1881 contactnote => 'altaddress_notes',
1882 contacttitle => undef, # Unused
1883 dateenrolled => 'date_enrolled',
1884 dateexpiry => 'expiry_date',
1885 dateofbirth => 'date_of_birth',
1886 debarred => undef, # replaced by 'restricted'
1887 debarredcomment => undef, # calculated, API consumers will use /restrictions instead
1888 emailpro => 'secondary_email',
1889 flags => undef, # permissions manipulation handled in /permissions
1890 gonenoaddress => 'incorrect_address',
1891 guarantorid => 'guarantor_id',
1892 lastseen => 'last_seen',
1893 lost => 'patron_card_lost',
1894 opacnote => 'opac_notes',
1895 othernames => 'other_name',
1896 password => undef, # password manipulation handled in /password
1897 phonepro => 'secondary_phone',
1898 relationship => 'relationship_type',
1900 smsalertnumber => 'sms_number',
1901 sort1 => 'statistics_1',
1902 sort2 => 'statistics_2',
1903 autorenew_checkouts => 'autorenew_checkouts',
1904 streetnumber => 'street_number',
1905 streettype => 'street_type',
1906 zipcode => 'postal_code',
1907 B_address => 'altaddress_address',
1908 B_address2 => 'altaddress_address2',
1909 B_city => 'altaddress_city',
1910 B_country => 'altaddress_country',
1911 B_email => 'altaddress_email',
1912 B_phone => 'altaddress_phone',
1913 B_state => 'altaddress_state',
1914 B_streetnumber => 'altaddress_street_number',
1915 B_streettype => 'altaddress_street_type',
1916 B_zipcode => 'altaddress_postal_code',
1917 altcontactaddress1 => 'altcontact_address',
1918 altcontactaddress2 => 'altcontact_address2',
1919 altcontactaddress3 => 'altcontact_city',
1920 altcontactcountry => 'altcontact_country',
1921 altcontactfirstname => 'altcontact_firstname',
1922 altcontactphone => 'altcontact_phone',
1923 altcontactsurname => 'altcontact_surname',
1924 altcontactstate => 'altcontact_state',
1925 altcontactzipcode => 'altcontact_postal_code',
1926 primary_contact_method => undef,
1932 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_name => 'DUE'});
1933 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports });
1934 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports, test_mode => 1 });
1936 Queue messages to a patron. Can pass a message that is part of the message_attributes
1937 table or supply the transport to use.
1939 If passed a message name we retrieve the patrons preferences for transports
1940 Otherwise we use the supplied transport. In the case of email or sms we fall back to print if
1941 we have no address/number for sending
1943 $letter_params is a hashref of the values to be passed to GetPreparedLetter
1945 test_mode will only report which notices would be sent, but nothing will be queued
1950 my ( $self, $params ) = @_;
1951 my $letter_params = $params->{letter_params};
1952 my $test_mode = $params->{test_mode};
1954 return unless $letter_params;
1955 return unless exists $params->{message_name} xor $params->{message_transports}; # We only want one of these
1957 my $library = Koha::Libraries->find( $letter_params->{branchcode} );
1958 my $from_email_address = $library->from_email_address;
1960 my @message_transports;
1962 $letter_code = $letter_params->{letter_code};
1963 if( $params->{message_name} ){
1964 my $messaging_prefs = C4::Members::Messaging::GetMessagingPreferences( {
1965 borrowernumber => $letter_params->{borrowernumber},
1966 message_name => $params->{message_name}
1968 @message_transports = ( keys %{ $messaging_prefs->{transports} } );
1969 $letter_code = $messaging_prefs->{transports}->{$message_transports[0]} unless $letter_code;
1971 @message_transports = @{$params->{message_transports}};
1973 return unless defined $letter_code;
1974 $letter_params->{letter_code} = $letter_code;
1977 foreach my $mtt (@message_transports){
1978 next if ($mtt eq 'itiva' and C4::Context->preference('TalkingTechItivaPhoneNotification') );
1979 # Notice is handled by TalkingTech_itiva_outbound.pl
1980 if ( ( $mtt eq 'email' and not $self->notice_email_address )
1981 or ( $mtt eq 'sms' and not $self->smsalertnumber )
1982 or ( $mtt eq 'phone' and not $self->phone ) )
1984 push @{ $return{fallback} }, $mtt;
1987 next if $mtt eq 'print' && $print_sent;
1988 $letter_params->{message_transport_type} = $mtt;
1989 my $letter = C4::Letters::GetPreparedLetter( %$letter_params );
1990 C4::Letters::EnqueueLetter({
1992 borrowernumber => $self->borrowernumber,
1993 from_address => $from_email_address,
1994 message_transport_type => $mtt
1995 }) unless $test_mode;
1996 push @{$return{sent}}, $mtt;
1997 $print_sent = 1 if $mtt eq 'print';
2002 =head3 safe_to_delete
2004 my $result = $patron->safe_to_delete;
2005 if ( $result eq 'has_guarantees' ) { ... }
2006 elsif ( $result ) { ... }
2007 else { # cannot delete }
2009 This method tells if the Koha:Patron object can be deleted. Possible return values
2015 =item 'has_checkouts'
2019 =item 'has_guarantees'
2021 =item 'is_anonymous_patron'
2027 sub safe_to_delete {
2030 my $anonymous_patron = C4::Context->preference('AnonymousPatron');
2034 if ( $anonymous_patron && $self->id eq $anonymous_patron ) {
2035 $error = 'is_anonymous_patron';
2037 elsif ( $self->checkouts->count ) {
2038 $error = 'has_checkouts';
2040 elsif ( $self->account->outstanding_debits->total_outstanding > 0 ) {
2041 $error = 'has_debt';
2043 elsif ( $self->guarantee_relationships->count ) {
2044 $error = 'has_guarantees';
2048 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
2051 return Koha::Result::Boolean->new(1);
2056 my $recalls = $patron->recalls;
2058 Return the patron's recalls.
2065 return Koha::Recalls->search({ borrowernumber => $self->borrowernumber });
2068 =head3 account_balance
2070 my $balance = $patron->account_balance
2072 Return the patron's account balance
2076 sub account_balance {
2078 return $self->account->balance;
2082 =head2 Internal methods
2094 Kyle M Hall <kyle@bywatersolutions.com>
2095 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
2096 Martin Renvoize <martin.renvoize@ptfs-europe.com>