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
1234 my $today_str = dt_from_string->strftime("%Y-%m-%d");
1235 return unless $self->dateofbirth;
1236 my $dob_str = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
1238 my ( $dob_y, $dob_m, $dob_d ) = split /-/, $dob_str;
1239 my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
1241 my $age = $today_y - $dob_y;
1242 if ( $dob_m . $dob_d > $today_m . $today_d ) {
1251 my $is_valid = $patron->is_valid_age
1253 Return 1 if patron's age is between allowed limits, returns 0 if it's not.
1259 my $age = $self->get_age;
1261 my $patroncategory = $self->category;
1262 my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
1264 return (defined($age) && (($high && ($age > $high)) or ($low && ($age < $low)))) ? 0 : 1;
1269 my $account = $patron->account
1275 return Koha::Account->new( { patron_id => $self->borrowernumber } );
1280 my $holds = $patron->holds
1282 Return all the holds placed by this patron
1288 my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1289 return Koha::Holds->_new_from_dbic($holds_rs);
1294 my $old_holds = $patron->old_holds
1296 Return all the historical holds for this patron
1302 my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1303 return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1306 =head3 return_claims
1308 my $return_claims = $patron->return_claims
1314 my $return_claims = $self->_result->return_claims_borrowernumbers;
1315 return Koha::Checkouts::ReturnClaims->_new_from_dbic( $return_claims );
1318 =head3 notice_email_address
1320 my $email = $patron->notice_email_address;
1322 Return the email address of patron used for notices.
1323 Returns the empty string if no email address.
1327 sub notice_email_address{
1330 my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1331 # if syspref is set to 'first valid' (value == OFF), look up email address
1332 if ( $which_address eq 'OFF' ) {
1333 return $self->first_valid_email_address;
1336 return $self->$which_address || '';
1339 =head3 first_valid_email_address
1341 my $first_valid_email_address = $patron->first_valid_email_address
1343 Return the first valid email address for a patron.
1344 For now, the order is defined as email, emailpro, B_email.
1345 Returns the empty string if the borrower has no email addresses.
1349 sub first_valid_email_address {
1352 return $self->email() || $self->emailpro() || $self->B_email() || q{};
1355 =head3 get_club_enrollments
1359 sub get_club_enrollments {
1362 return Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1365 =head3 get_enrollable_clubs
1369 sub get_enrollable_clubs {
1370 my ( $self, $is_enrollable_from_opac ) = @_;
1373 $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1374 if $is_enrollable_from_opac;
1375 $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1377 $params->{borrower} = $self;
1379 return Koha::Clubs->get_enrollable($params);
1382 =head3 account_locked
1384 my $is_locked = $patron->account_locked
1386 Return true if the patron has reached the maximum number of login attempts
1387 (see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
1388 as an administrative lockout (independent of FailedLoginAttempts; see also
1389 Koha::Patron->lock).
1390 Otherwise return false.
1391 If the pref is not set (empty string, null or 0), the feature is considered as
1396 sub account_locked {
1398 my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1399 return 1 if $FailedLoginAttempts
1400 and $self->login_attempts
1401 and $self->login_attempts >= $FailedLoginAttempts;
1402 return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
1406 =head3 can_see_patron_infos
1408 my $can_see = $patron->can_see_patron_infos( $patron );
1410 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1414 sub can_see_patron_infos {
1415 my ( $self, $patron ) = @_;
1416 return unless $patron;
1417 return $self->can_see_patrons_from( $patron->branchcode );
1420 =head3 can_see_patrons_from
1422 my $can_see = $patron->can_see_patrons_from( $branchcode );
1424 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1428 sub can_see_patrons_from {
1429 my ( $self, $branchcode ) = @_;
1431 if ( $self->branchcode eq $branchcode ) {
1433 } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1435 } elsif ( my $library_groups = $self->library->library_groups ) {
1436 while ( my $library_group = $library_groups->next ) {
1437 if ( $library_group->parent->has_child( $branchcode ) ) {
1448 my $can_log_into = $patron->can_log_into( $library );
1450 Given a I<Koha::Library> object, it returns a boolean representing
1451 the fact the patron can log into a the library.
1456 my ( $self, $library ) = @_;
1460 if ( C4::Context->preference('IndependentBranches') ) {
1462 if $self->is_superlibrarian
1463 or $self->branchcode eq $library->id;
1473 =head3 libraries_where_can_see_patrons
1475 my $libraries = $patron-libraries_where_can_see_patrons;
1477 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1478 The branchcodes are arbitrarily returned sorted.
1479 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1481 An empty array means no restriction, the patron can see patron's infos from any libraries.
1485 sub libraries_where_can_see_patrons {
1487 my $userenv = C4::Context->userenv;
1489 return () unless $userenv; # For tests, but userenv should be defined in tests...
1491 my @restricted_branchcodes;
1492 if (C4::Context::only_my_library) {
1493 push @restricted_branchcodes, $self->branchcode;
1497 $self->has_permission(
1498 { borrowers => 'view_borrower_infos_from_any_libraries' }
1502 my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1503 if ( $library_groups->count )
1505 while ( my $library_group = $library_groups->next ) {
1506 my $parent = $library_group->parent;
1507 if ( $parent->has_child( $self->branchcode ) ) {
1508 push @restricted_branchcodes, $parent->children->get_column('branchcode');
1513 @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1517 @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1518 @restricted_branchcodes = uniq(@restricted_branchcodes);
1519 @restricted_branchcodes = sort(@restricted_branchcodes);
1520 return @restricted_branchcodes;
1523 =head3 has_permission
1525 my $permission = $patron->has_permission($required);
1527 See C4::Auth::haspermission for details of syntax for $required
1531 sub has_permission {
1532 my ( $self, $flagsrequired ) = @_;
1533 return unless $self->userid;
1534 # TODO code from haspermission needs to be moved here!
1535 return C4::Auth::haspermission( $self->userid, $flagsrequired );
1538 =head3 is_superlibrarian
1540 my $is_superlibrarian = $patron->is_superlibrarian;
1542 Return true if the patron is a superlibrarian.
1546 sub is_superlibrarian {
1548 return $self->has_permission( { superlibrarian => 1 } ) ? 1 : 0;
1553 my $is_adult = $patron->is_adult
1555 Return true if the patron has a category with a type Adult (A) or Organization (I)
1561 return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1566 my $is_child = $patron->is_child
1568 Return true if the patron has a category with a type Child (C)
1574 return $self->category->category_type eq 'C' ? 1 : 0;
1577 =head3 has_valid_userid
1579 my $patron = Koha::Patrons->find(42);
1580 $patron->userid( $new_userid );
1581 my $has_a_valid_userid = $patron->has_valid_userid
1583 my $patron = Koha::Patron->new( $params );
1584 my $has_a_valid_userid = $patron->has_valid_userid
1586 Return true if the current userid of this patron is valid/unique, otherwise false.
1588 Note that this should be done in $self->store instead and raise an exception if needed.
1592 sub has_valid_userid {
1595 return 0 unless $self->userid;
1597 return 0 if ( $self->userid eq C4::Context->config('user') ); # DB user
1599 my $already_exists = Koha::Patrons->search(
1601 userid => $self->userid,
1604 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1609 return $already_exists ? 0 : 1;
1612 =head3 generate_userid
1614 my $patron = Koha::Patron->new( $params );
1615 $patron->generate_userid
1617 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1619 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).
1623 sub generate_userid {
1626 my $firstname = $self->firstname // q{};
1627 my $surname = $self->surname // q{};
1628 #The script will "do" the following code and increment the $offset until the generated userid is unique
1630 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1631 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1632 my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1633 $userid = NFKD( $userid );
1634 $userid =~ s/\p{NonspacingMark}//g;
1635 $userid .= $offset unless $offset == 0;
1636 $self->userid( $userid );
1638 } while (! $self->has_valid_userid );
1643 =head3 add_extended_attribute
1647 sub add_extended_attribute {
1648 my ($self, $attribute) = @_;
1650 return Koha::Patron::Attribute->new(
1653 ( borrowernumber => $self->borrowernumber ),
1659 =head3 extended_attributes
1661 Return object of Koha::Patron::Attributes type with all attributes set for this patron
1667 sub extended_attributes {
1668 my ( $self, $attributes ) = @_;
1669 if ($attributes) { # setter
1670 my $schema = $self->_result->result_source->schema;
1673 # Remove the existing one
1674 $self->extended_attributes->filter_by_branch_limitations->delete;
1676 # Insert the new ones
1678 for my $attribute (@$attributes) {
1679 $self->add_extended_attribute($attribute);
1680 $new_types->{$attribute->{code}} = 1;
1683 # Check globally mandatory types
1684 my @required_attribute_types =
1685 Koha::Patron::Attribute::Types->search(
1688 'borrower_attribute_types_branches.b_branchcode' =>
1691 { join => 'borrower_attribute_types_branches' }
1692 )->get_column('code');
1693 for my $type ( @required_attribute_types ) {
1694 Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute->throw(
1696 ) if !$new_types->{$type};
1702 my $rs = $self->_result->borrower_attributes;
1703 # We call search to use the filters in Koha::Patron::Attributes->search
1704 return Koha::Patron::Attributes->_new_from_dbic($rs)->search;
1709 my $messages = $patron->messages;
1711 Return the message attached to the patron.
1717 my $messages_rs = $self->_result->messages_borrowernumbers->search;
1718 return Koha::Patron::Messages->_new_from_dbic($messages_rs);
1723 Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
1725 Lock and optionally expire a patron account.
1726 Remove holds and article requests if remove flag set.
1727 In order to distinguish from locking by entering a wrong password, let's
1728 call this an administrative lockout.
1733 my ( $self, $params ) = @_;
1734 $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
1735 if( $params->{expire} ) {
1736 $self->dateexpiry( dt_from_string->subtract(days => 1) );
1739 if( $params->{remove} ) {
1740 $self->holds->delete;
1741 $self->article_requests->delete;
1748 Koha::Patrons->find($id)->anonymize;
1750 Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
1751 are randomized, other personal data is cleared too.
1752 Patrons with issues are skipped.
1758 if( $self->_result->issues->count ) {
1759 warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
1762 # Mandatory fields come from the corresponding pref, but email fields
1763 # are removed since scrambled email addresses only generate errors
1764 my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
1765 split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
1766 $mandatory->{userid} = 1; # needed since sub store does not clear field
1767 my @columns = $self->_result->result_source->columns;
1768 @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized/ } @columns;
1769 push @columns, 'dateofbirth'; # add this date back in
1770 foreach my $col (@columns) {
1771 $self->_anonymize_column($col, $mandatory->{lc $col} );
1773 $self->anonymized(1)->store;
1776 sub _anonymize_column {
1777 my ( $self, $col, $mandatory ) = @_;
1778 my $col_info = $self->_result->result_source->column_info($col);
1779 my $type = $col_info->{data_type};
1780 my $nullable = $col_info->{is_nullable};
1782 if( $type =~ /char|text/ ) {
1784 ? Koha::Token->new->generate({ pattern => '\w{10}' })
1788 } elsif( $type =~ /integer|int$|float|dec|double/ ) {
1789 $val = $nullable ? undef : 0;
1790 } elsif( $type =~ /date|time/ ) {
1791 $val = $nullable ? undef : dt_from_string;
1796 =head3 add_guarantor
1798 my $relationship = $patron->add_guarantor(
1800 borrowernumber => $borrowernumber,
1801 relationships => $relationship,
1805 Adds a new guarantor to a patron.
1810 my ( $self, $params ) = @_;
1812 my $guarantor_id = $params->{guarantor_id};
1813 my $relationship = $params->{relationship};
1815 return Koha::Patron::Relationship->new(
1817 guarantee_id => $self->id,
1818 guarantor_id => $guarantor_id,
1819 relationship => $relationship
1824 =head3 get_extended_attribute
1826 my $attribute_value = $patron->get_extended_attribute( $code );
1828 Return the attribute for the code passed in parameter.
1830 It not exist it returns undef
1832 Note that this will not work for repeatable attribute types.
1834 Maybe you certainly not want to use this method, it is actually only used for SHOW_BARCODE
1835 (which should be a real patron's attribute (not extended)
1839 sub get_extended_attribute {
1840 my ( $self, $code, $value ) = @_;
1841 my $rs = $self->_result->borrower_attributes;
1843 my $attribute = $rs->search({ code => $code, ( $value ? ( attribute => $value ) : () ) });
1844 return unless $attribute->count;
1845 return $attribute->next;
1850 my $json = $patron->to_api;
1852 Overloaded method that returns a JSON representation of the Koha::Patron object,
1853 suitable for API output.
1858 my ( $self, $params ) = @_;
1860 my $json_patron = $self->SUPER::to_api( $params );
1862 $json_patron->{restricted} = ( $self->is_debarred )
1864 : Mojo::JSON->false;
1866 return $json_patron;
1869 =head3 to_api_mapping
1871 This method returns the mapping for representing a Koha::Patron object
1876 sub to_api_mapping {
1878 borrowernotes => 'staff_notes',
1879 borrowernumber => 'patron_id',
1880 branchcode => 'library_id',
1881 categorycode => 'category_id',
1882 checkprevcheckout => 'check_previous_checkout',
1883 contactfirstname => undef, # Unused
1884 contactname => undef, # Unused
1885 contactnote => 'altaddress_notes',
1886 contacttitle => undef, # Unused
1887 dateenrolled => 'date_enrolled',
1888 dateexpiry => 'expiry_date',
1889 dateofbirth => 'date_of_birth',
1890 debarred => undef, # replaced by 'restricted'
1891 debarredcomment => undef, # calculated, API consumers will use /restrictions instead
1892 emailpro => 'secondary_email',
1893 flags => undef, # permissions manipulation handled in /permissions
1894 gonenoaddress => 'incorrect_address',
1895 guarantorid => 'guarantor_id',
1896 lastseen => 'last_seen',
1897 lost => 'patron_card_lost',
1898 opacnote => 'opac_notes',
1899 othernames => 'other_name',
1900 password => undef, # password manipulation handled in /password
1901 phonepro => 'secondary_phone',
1902 relationship => 'relationship_type',
1904 smsalertnumber => 'sms_number',
1905 sort1 => 'statistics_1',
1906 sort2 => 'statistics_2',
1907 autorenew_checkouts => 'autorenew_checkouts',
1908 streetnumber => 'street_number',
1909 streettype => 'street_type',
1910 zipcode => 'postal_code',
1911 B_address => 'altaddress_address',
1912 B_address2 => 'altaddress_address2',
1913 B_city => 'altaddress_city',
1914 B_country => 'altaddress_country',
1915 B_email => 'altaddress_email',
1916 B_phone => 'altaddress_phone',
1917 B_state => 'altaddress_state',
1918 B_streetnumber => 'altaddress_street_number',
1919 B_streettype => 'altaddress_street_type',
1920 B_zipcode => 'altaddress_postal_code',
1921 altcontactaddress1 => 'altcontact_address',
1922 altcontactaddress2 => 'altcontact_address2',
1923 altcontactaddress3 => 'altcontact_city',
1924 altcontactcountry => 'altcontact_country',
1925 altcontactfirstname => 'altcontact_firstname',
1926 altcontactphone => 'altcontact_phone',
1927 altcontactsurname => 'altcontact_surname',
1928 altcontactstate => 'altcontact_state',
1929 altcontactzipcode => 'altcontact_postal_code',
1930 primary_contact_method => undef,
1936 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_name => 'DUE'});
1937 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports });
1938 Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports, test_mode => 1 });
1940 Queue messages to a patron. Can pass a message that is part of the message_attributes
1941 table or supply the transport to use.
1943 If passed a message name we retrieve the patrons preferences for transports
1944 Otherwise we use the supplied transport. In the case of email or sms we fall back to print if
1945 we have no address/number for sending
1947 $letter_params is a hashref of the values to be passed to GetPreparedLetter
1949 test_mode will only report which notices would be sent, but nothing will be queued
1954 my ( $self, $params ) = @_;
1955 my $letter_params = $params->{letter_params};
1956 my $test_mode = $params->{test_mode};
1958 return unless $letter_params;
1959 return unless exists $params->{message_name} xor $params->{message_transports}; # We only want one of these
1961 my $library = Koha::Libraries->find( $letter_params->{branchcode} );
1962 my $from_email_address = $library->from_email_address;
1964 my @message_transports;
1966 $letter_code = $letter_params->{letter_code};
1967 if( $params->{message_name} ){
1968 my $messaging_prefs = C4::Members::Messaging::GetMessagingPreferences( {
1969 borrowernumber => $letter_params->{borrowernumber},
1970 message_name => $params->{message_name}
1972 @message_transports = ( keys %{ $messaging_prefs->{transports} } );
1973 $letter_code = $messaging_prefs->{transports}->{$message_transports[0]} unless $letter_code;
1975 @message_transports = @{$params->{message_transports}};
1977 return unless defined $letter_code;
1978 $letter_params->{letter_code} = $letter_code;
1981 foreach my $mtt (@message_transports){
1982 next if ($mtt eq 'itiva' and C4::Context->preference('TalkingTechItivaPhoneNotification') );
1983 # Notice is handled by TalkingTech_itiva_outbound.pl
1984 if ( ( $mtt eq 'email' and not $self->notice_email_address )
1985 or ( $mtt eq 'sms' and not $self->smsalertnumber )
1986 or ( $mtt eq 'phone' and not $self->phone ) )
1988 push @{ $return{fallback} }, $mtt;
1991 next if $mtt eq 'print' && $print_sent;
1992 $letter_params->{message_transport_type} = $mtt;
1993 my $letter = C4::Letters::GetPreparedLetter( %$letter_params );
1994 C4::Letters::EnqueueLetter({
1996 borrowernumber => $self->borrowernumber,
1997 from_address => $from_email_address,
1998 message_transport_type => $mtt
1999 }) unless $test_mode;
2000 push @{$return{sent}}, $mtt;
2001 $print_sent = 1 if $mtt eq 'print';
2006 =head3 safe_to_delete
2008 my $result = $patron->safe_to_delete;
2009 if ( $result eq 'has_guarantees' ) { ... }
2010 elsif ( $result ) { ... }
2011 else { # cannot delete }
2013 This method tells if the Koha:Patron object can be deleted. Possible return values
2019 =item 'has_checkouts'
2023 =item 'has_guarantees'
2025 =item 'is_anonymous_patron'
2031 sub safe_to_delete {
2034 my $anonymous_patron = C4::Context->preference('AnonymousPatron');
2038 if ( $anonymous_patron && $self->id eq $anonymous_patron ) {
2039 $error = 'is_anonymous_patron';
2041 elsif ( $self->checkouts->count ) {
2042 $error = 'has_checkouts';
2044 elsif ( $self->account->outstanding_debits->total_outstanding > 0 ) {
2045 $error = 'has_debt';
2047 elsif ( $self->guarantee_relationships->count ) {
2048 $error = 'has_guarantees';
2052 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
2055 return Koha::Result::Boolean->new(1);
2060 my $recalls = $patron->recalls;
2062 Return the patron's recalls.
2069 return Koha::Recalls->search({ borrowernumber => $self->borrowernumber });
2072 =head2 Internal methods
2084 Kyle M Hall <kyle@bywatersolutions.com>
2085 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
2086 Martin Renvoize <martin.renvoize@ptfs-europe.com>