use Modern::Perl;
-use Carp;
use List::MoreUtils qw( any uniq );
use JSON qw( to_json );
-use Unicode::Normalize;
+use Unicode::Normalize qw( NFKD );
+use Try::Tiny;
use C4::Context;
-use C4::Log;
+use C4::Auth qw( checkpw_hash );
+use C4::Log qw( logaction );
use Koha::Account;
use Koha::ArticleRequests;
+use C4::Letters qw( GetPreparedLetter EnqueueLetter SendQueuedMessages );
use Koha::AuthUtils;
use Koha::Checkouts;
+use Koha::CirculationRules;
use Koha::Club::Enrollments;
use Koha::Database;
-use Koha::DateUtils;
+use Koha::DateUtils qw( dt_from_string );
+use Koha::Encryption;
use Koha::Exceptions::Password;
use Koha::Holds;
+use Koha::CurbsidePickups;
use Koha::Old::Checkouts;
use Koha::Patron::Attributes;
use Koha::Patron::Categories;
+use Koha::Patron::Debarments;
use Koha::Patron::HouseboundProfile;
use Koha::Patron::HouseboundRole;
use Koha::Patron::Images;
+use Koha::Patron::Messages;
+use Koha::Patron::Modifications;
use Koha::Patron::Relationships;
use Koha::Patrons;
use Koha::Plugins;
+use Koha::Recalls;
+use Koha::Result::Boolean;
use Koha::Subscription::Routinglists;
use Koha::Token;
use Koha::Virtualshelves;
Aqbudget => 'budget_owner_id',
Aqbudgetborrower => 'borrowernumber',
ArticleRequest => 'borrowernumber',
- BorrowerAttribute => 'borrowernumber',
BorrowerDebarment => 'borrowernumber',
BorrowerFile => 'borrowernumber',
BorrowerModification => 'borrowernumber',
sub fixup_cardnumber {
my ( $self ) = @_;
- my $max = Koha::Patrons->search({
+
+ my $max = $self->cardnumber;
+ Koha::Plugins->call( 'patron_barcode_transform', \$max );
+
+ $max ||= Koha::Patrons->search({
cardnumber => {-regexp => '^-?[0-9]+$'}
}, {
select => \'CAST(cardnumber AS SIGNED)',
$self->trim_whitespaces;
+ my $new_cardnumber = $self->cardnumber;
+ Koha::Plugins->call( 'patron_barcode_transform', \$new_cardnumber );
+ $self->cardnumber( $new_cardnumber );
+
# Set surname to uppercase if uppercasesurname is true
$self->surname( uc($self->surname) )
if C4::Context->preference("uppercasesurnames");
$self->privacy($default_privacy);
# Call any check_password plugins if password is passed
- if ( C4::Context->preference('UseKohaPlugins')
- && C4::Context->config("enable_plugins")
- && $self->password )
- {
+ if ( C4::Context->config("enable_plugins") && $self->password ) {
my @plugins = Koha::Plugins->new()->GetPlugins({
method => 'check_password',
});
# Make a copy of the plain text password for later use
$self->plain_text_password( $self->password );
+ $self->password_expiration_date( $self->password
+ ? $self->category->get_password_expiry_date || undef
+ : undef );
# Create a disabled account if no password provided
$self->password( $self->password
? Koha::AuthUtils::hash_password( $self->password )
# Clean up guarantors on category change if required
$self->guarantor_relationships->delete
- if ( $self->category->category_type ne 'C'
- && $self->category->category_type ne 'P' );
+ unless ( $self->category->can_be_guarantee );
}
Delete patron's holds, lists and finally the patron.
-Lists owned by the borrower are deleted, but entries from the borrower to
-other lists are kept.
+Lists owned by the borrower are deleted or ownership is transferred depending on the
+ListOwnershipUponPatronDeletion pref, but entries from the borrower to other lists are kept.
=cut
sub delete {
my ($self) = @_;
+ my $anonymous_patron = C4::Context->preference("AnonymousPatron");
+ Koha::Exceptions::Patron::FailedDeleteAnonymousPatron->throw() if $anonymous_patron && $self->id eq $anonymous_patron;
+
$self->_result->result_source->schema->txn_do(
sub {
# Cancel Patron's holds
$hold->cancel;
}
- # Delete all lists and all shares of this borrower
- # Consistent with the approach Koha uses on deleting individual lists
- # Note that entries in virtualshelfcontents added by this borrower to
- # lists of others will be handled by a table constraint: the borrower
- # is set to NULL in those entries.
- # NOTE:
- # We could handle the above deletes via a constraint too.
- # But a new BZ report 11889 has been opened to discuss another approach.
- # Instead of deleting we could also disown lists (based on a pref).
- # In that way we could save shared and public lists.
- # The current table constraints support that idea now.
- # This pref should then govern the results of other routines/methods such as
- # Koha::Virtualshelf->new->delete too.
- # FIXME Could be $patron->get_lists
- $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
+ # Handle lists (virtualshelves)
+ $self->virtualshelves->disown_or_delete;
+
+ # We cannot have a FK on borrower_modifications.borrowernumber, the table is also used
+ # for patron selfreg
+ $_->delete for Koha::Patron::Modifications->search( { borrowernumber => $self->borrowernumber } )->as_list;
$self->SUPER::delete;
return $self;
}
-
=head3 category
my $patron_category = $patron->category
return Koha::Library->_new_from_dbic($self->_result->branchcode);
}
+=head3 sms_provider
+
+Returns a Koha::SMS::Provider object representing the patron's SMS provider.
+
+=cut
+
+sub sms_provider {
+ my ( $self ) = @_;
+ my $sms_provider_rs = $self->_result->sms_provider;
+ return unless $sms_provider_rs;
+ return Koha::SMS::Provider->_new_from_dbic($sms_provider_rs);
+}
+
=head3 guarantor_relationships
Returns Koha::Patron::Relationships object for this patron's guarantors
);
}
+=head3 relationships_debt
+
+Returns the amount owed by the patron's guarantors *and* the other guarantees of those guarantors
+
+=cut
+
+sub relationships_debt {
+ my ($self, $params) = @_;
+
+ my $include_guarantors = $params->{include_guarantors};
+ my $only_this_guarantor = $params->{only_this_guarantor};
+ my $include_this_patron = $params->{include_this_patron};
+
+ my @guarantors;
+ if ( $only_this_guarantor ) {
+ @guarantors = $self->guarantee_relationships->count ? ( $self ) : ();
+ Koha::Exceptions::BadParameter->throw( { parameter => 'only_this_guarantor' } ) unless @guarantors;
+ } elsif ( $self->guarantor_relationships->count ) {
+ # I am a guarantee, just get all my guarantors
+ @guarantors = $self->guarantor_relationships->guarantors->as_list;
+ } else {
+ # I am a guarantor, I need to get all the guarantors of all my guarantees
+ @guarantors = map { $_->guarantor_relationships->guarantors->as_list } $self->guarantee_relationships->guarantees->as_list;
+ }
+
+ my $non_issues_charges = 0;
+ my $seen = $include_this_patron ? {} : { $self->id => 1 }; # For tracking members already added to the total
+ foreach my $guarantor (@guarantors) {
+ $non_issues_charges += $guarantor->account->non_issues_charges if $include_guarantors && !$seen->{ $guarantor->id };
+
+ # We've added what the guarantor owes, not added in that guarantor's guarantees as well
+ my @guarantees = map { $_->guarantee } $guarantor->guarantee_relationships->as_list;
+ my $guarantees_non_issues_charges = 0;
+ foreach my $guarantee (@guarantees) {
+ next if $seen->{ $guarantee->id };
+ $guarantees_non_issues_charges += $guarantee->account->non_issues_charges;
+ # Mark this guarantee as seen so we don't double count a guarantee linked to multiple guarantors
+ $seen->{ $guarantee->id } = 1;
+ }
+
+ $non_issues_charges += $guarantees_non_issues_charges;
+ $seen->{ $guarantor->id } = 1;
+ }
+
+ return $non_issues_charges;
+}
+
=head3 housebound_profile
Returns the HouseboundProfile associated with this patron.
sub siblings {
my ($self) = @_;
- my @guarantors = $self->guarantor_relationships()->guarantors();
+ my @guarantors = $self->guarantor_relationships()->guarantors()->as_list;
return unless @guarantors;
my @siblings =
- map { $_->guarantee_relationships()->guarantees() } @guarantors;
+ map { $_->guarantee_relationships()->guarantees()->as_list } @guarantors;
return unless @siblings;
@siblings =
grep { !$seen{ $_->id }++ && ( $_->id != $self->id ) } @siblings;
- return wantarray ? @siblings : Koha::Patrons->search( { borrowernumber => { -in => [ map { $_->id } @siblings ] } } );
+ return Koha::Patrons->search( { borrowernumber => { -in => [ map { $_->id } @siblings ] } } );
}
=head3 merge_with
sub merge_with {
my ( $self, $patron_ids ) = @_;
+ my $anonymous_patron = C4::Context->preference("AnonymousPatron");
+ return if $anonymous_patron && $self->id eq $anonymous_patron;
+
my @patron_ids = @{ $patron_ids };
# Ensure the keeper isn't in the list of patrons to merge
$self->_result->result_source->schema->txn_do( sub {
foreach my $patron_id (@patron_ids) {
+
+ next if $patron_id eq $anonymous_patron;
+
my $patron = Koha::Patrons->find( $patron_id );
next unless $patron;
# Unbless for safety, the patron will end up being deleted
$results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
+ my $attributes = $patron->extended_attributes;
+ my $new_attributes = [
+ map { { code => $_->code, attribute => $_->attribute } }
+ $attributes->as_list
+ ];
+ $attributes->delete; # We need to delete before trying to merge them to prevent exception on unique and repeatable
+ for my $attribute ( @$new_attributes ) {
+ try {
+ $self->add_extended_attribute($attribute);
+ } catch {
+ # Don't block the merge if there is a non-repeatable attribute that cannot be added to the current patron.
+ unless ( $_->isa('Koha::Exceptions::Patron::Attribute::NonRepeatable') ) {
+ $_->rethrow;
+ }
+ };
+ }
+
while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
my $rs = $schema->resultset($r)->search({ $field => $patron_id });
$results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
$rs->update({ $field => $self->id });
+ if ( $r eq 'BorrowerDebarment' ) {
+ Koha::Patron::Debarments::UpdateBorrowerDebarmentFlags($self->id);
+ }
}
$patron->move_to_deleted();
itemnumber => \@item_nos,
};
+ my $delay = C4::Context->preference('CheckPrevCheckoutDelay') || 0;
+ if ($delay) {
+ my $dtf = Koha::Database->new->schema->storage->datetime_parser;
+ my $newer_than = dt_from_string()->subtract( days => $delay );
+ $criteria->{'returndate'} = { '>' => $dtf->format_datetime($newer_than), };
+ }
+
# Check current issues table
my $issues = Koha::Checkouts->search($criteria);
return 1 if $issues->count; # 0 || N
return 0;
}
+=head3 password_expired
+
+my $password_expired = $patron->password_expired;
+
+Returns 1 if the patron's password is expired or 0;
+
+=cut
+
+sub password_expired {
+ my ($self) = @_;
+ return 0 unless $self->password_expiration_date;
+ return 1 if dt_from_string( $self->password_expiration_date ) <= dt_from_string->truncate( to => 'day' );
+ return 0;
+}
+
=head3 is_going_to_expire
my $is_going_to_expire = $patron->is_going_to_expire;
my $password = $args->{password};
unless ( $args->{skip_validation} ) {
- my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password );
+ my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password, $self->category );
if ( !$is_valid ) {
if ( $error eq 'too_short' ) {
- my $min_length = C4::Context->preference('minPasswordLength');
+ my $min_length = $self->category->effective_min_password_length;
$min_length = 3 if not $min_length or $min_length < 3;
my $password_length = length($password);
}
}
- if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
+ if ( C4::Context->config("enable_plugins") ) {
# Call any check_password plugins
my @plugins = Koha::Plugins->new()->GetPlugins({
method => 'check_password',
}
}
+ if ( C4::Context->preference('NotifyPasswordChange') ) {
+ my $self_from_storage = $self->get_from_storage;
+ if ( !C4::Auth::checkpw_hash( $password, $self_from_storage->password ) ) {
+ my $emailaddr = $self_from_storage->notice_email_address;
+
+ # if we manage to find a valid email address, send notice
+ if ($emailaddr) {
+ my $letter = C4::Letters::GetPreparedLetter(
+ module => 'members',
+ letter_code => 'PASSWORD_CHANGE',
+ branchcode => $self_from_storage->branchcode,
+ ,
+ lang => $self_from_storage->lang || 'default',
+ tables => {
+ 'branches' => $self_from_storage->branchcode,
+ 'borrowers' => $self_from_storage->borrowernumber,
+ },
+ want_librarian => 1,
+ ) or return;
+
+ my $message_id = C4::Letters::EnqueueLetter(
+ {
+ letter => $letter,
+ borrowernumber => $self_from_storage->id,
+ to_address => $emailaddr,
+ message_transport_type => 'email'
+ }
+ );
+ C4::Letters::SendQueuedMessages( { message_id => $message_id } );
+ }
+ }
+ }
+
my $digest = Koha::AuthUtils::hash_password($password);
+ $self->password_expiration_date( $self->category->get_password_expiry_date || undef );
+
# We do not want to call $self->store and retrieve password from DB
$self->password($digest);
$self->login_attempts(0);
return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
}
-=head3 article_requests
+=head3 can_request_article
+
+ if ( $patron->can_request_article( $library->id ) ) { ... }
-my @requests = $borrower->article_requests();
-my $requests = $borrower->article_requests();
+Returns true if the patron can request articles. As limits apply for the patron
+on the same day, those completed the same day are considered as current.
-Returns either a list of ArticleRequests objects,
-or an ArtitleRequests object, depending on the
-calling context.
+A I<library_id> can be passed as parameter, falling back to userenv if absent.
=cut
-sub article_requests {
- my ( $self ) = @_;
+sub can_request_article {
+ my ($self, $library_id) = @_;
- $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
+ $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
- return $self->{_article_requests};
+ my $rule = Koha::CirculationRules->get_effective_rule(
+ {
+ branchcode => $library_id,
+ categorycode => $self->categorycode,
+ rule_name => 'open_article_requests_limit'
+ }
+ );
+
+ my $limit = ($rule) ? $rule->rule_value : undef;
+
+ return 1 unless defined $limit;
+
+ my $count = Koha::ArticleRequests->search(
+ [ { borrowernumber => $self->borrowernumber, status => [ 'REQUESTED', 'PENDING', 'PROCESSING' ] },
+ { borrowernumber => $self->borrowernumber, status => 'COMPLETED', updated_on => { '>=' => \'CAST(NOW() AS DATE)' } },
+ ]
+ )->count;
+ return $count < $limit ? 1 : 0;
}
-=head3 article_requests_current
+=head3 article_request_fee
-my @requests = $patron->article_requests_current
+ my $fee = $patron->article_request_fee(
+ {
+ [ library_id => $library->id, ]
+ }
+ );
-Returns the article requests associated with this patron that are incomplete
+Returns the fee to be charged to the patron when it places an article request.
+
+A I<library_id> can be passed as parameter, falling back to userenv if absent.
=cut
-sub article_requests_current {
- my ( $self ) = @_;
+sub article_request_fee {
+ my ($self, $params) = @_;
- $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
+ my $library_id = $params->{library_id};
+
+ $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
+
+ my $rule = Koha::CirculationRules->get_effective_rule(
{
- borrowernumber => $self->id(),
- -or => [
- { status => Koha::ArticleRequest::Status::Pending },
- { status => Koha::ArticleRequest::Status::Processing }
- ]
+ branchcode => $library_id,
+ categorycode => $self->categorycode,
+ rule_name => 'article_request_fee'
}
);
- return $self->{_article_requests_current};
+ my $fee = ($rule) ? $rule->rule_value + 0 : 0;
+
+ return $fee;
}
-=head3 article_requests_finished
+=head3 add_article_request_fee_if_needed
-my @requests = $biblio->article_requests_finished
+ my $fee = $patron->add_article_request_fee_if_needed(
+ {
+ [ item_id => $item->id,
+ library_id => $library->id, ]
+ }
+ );
+
+If an article request fee needs to be charged, it adds a debit to the patron's
+account.
-Returns the article requests associated with this patron that are completed
+Returns the fee line.
+
+A I<library_id> can be passed as parameter, falling back to userenv if absent.
=cut
-sub article_requests_finished {
- my ( $self, $borrower ) = @_;
+sub add_article_request_fee_if_needed {
+ my ($self, $params) = @_;
+
+ my $library_id = $params->{library_id};
+ my $item_id = $params->{item_id};
- $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
+ $library_id //= C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
+
+ my $amount = $self->article_request_fee(
{
- borrowernumber => $self->id(),
- -or => [
- { status => Koha::ArticleRequest::Status::Completed },
- { status => Koha::ArticleRequest::Status::Canceled }
- ]
+ library_id => $library_id,
}
);
- return $self->{_article_requests_finished};
+ my $debit_line;
+
+ if ( $amount > 0 ) {
+ $debit_line = $self->account->add_debit(
+ {
+ amount => $amount,
+ user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
+ interface => C4::Context->interface,
+ library_id => $library_id,
+ type => 'ARTICLE_REQUEST',
+ item_id => $item_id,
+ }
+ );
+ }
+
+ return $debit_line;
+}
+
+=head3 article_requests
+
+ my $article_requests = $patron->article_requests;
+
+Returns the patron article requests.
+
+=cut
+
+sub article_requests {
+ my ($self) = @_;
+
+ return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
}
=head3 add_enrolment_fee_if_needed
return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
}
-=head3 get_overdues
+=head3 overdues
-my $overdue_items = $patron->get_overdues
+my $overdue_items = $patron->overdues
Return the overdue items
=cut
-sub get_overdues {
+sub overdues {
my ($self) = @_;
my $dtf = Koha::Database->new->schema->storage->datetime_parser;
return $self->checkouts->search(
=head3 get_routing_lists
-my @routinglists = $patron->get_routing_lists
+my $routinglists = $patron->get_routing_lists
Returns the routing lists a patron is subscribed to.
=head3 get_age
-my $age = $patron->get_age
+ my $age = $patron->get_age
Return the age of the patron
sub get_age {
my ($self) = @_;
- my $today_str = dt_from_string->strftime("%Y-%m-%d");
- return unless $self->dateofbirth;
- my $dob_str = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
- my ( $dob_y, $dob_m, $dob_d ) = split /-/, $dob_str;
- my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
+ return unless $self->dateofbirth;
- my $age = $today_y - $dob_y;
- if ( $dob_m . $dob_d > $today_m . $today_d ) {
- $age--;
- }
+ my $date_of_birth = dt_from_string( $self->dateofbirth );
+ my $today = dt_from_string->truncate( to => 'day' );
- return $age;
+ return $today->subtract_datetime( $date_of_birth )->years;
}
=head3 is_valid_age
my $patroncategory = $self->category;
my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
- return (defined($age) && (($high && ($age > $high)) or ($age < $low))) ? 0 : 1;
+ return (defined($age) && (($high && ($age > $high)) or ($low && ($age < $low)))) ? 0 : 1;
}
=head3 account
return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
}
+=head3 curbside_pickups
+
+my $curbside_pickups = $patron->curbside_pickups;
+
+Return all the curbside pickups for this patron
+
+=cut
+
+sub curbside_pickups {
+ my ($self) = @_;
+ my $curbside_pickups_rs = $self->_result->curbside_pickups_borrowernumbers->search;
+ return Koha::CurbsidePickups->_new_from_dbic($curbside_pickups_rs);
+}
+
=head3 return_claims
my $return_claims = $patron->return_claims
=cut
sub get_club_enrollments {
- my ( $self, $return_scalar ) = @_;
-
- my $e = Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
-
- return $e if $return_scalar;
+ my ( $self ) = @_;
- return wantarray ? $e->as_list : $e;
+ return Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
}
=head3 get_enrollable_clubs
=cut
sub get_enrollable_clubs {
- my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
+ my ( $self, $is_enrollable_from_opac ) = @_;
my $params;
$params->{is_enrollable_from_opac} = $is_enrollable_from_opac
$params->{borrower} = $self;
- my $e = Koha::Clubs->get_enrollable($params);
-
- return $e if $return_scalar;
-
- return wantarray ? $e->as_list : $e;
+ return Koha::Clubs->get_enrollable($params);
}
=head3 account_locked
sub can_see_patron_infos {
my ( $self, $patron ) = @_;
return unless $patron;
- return $self->can_see_patrons_from( $patron->library->branchcode );
+ return $self->can_see_patrons_from( $patron->branchcode );
}
=head3 can_see_patrons_from
return $can;
}
+=head3 can_log_into
+
+my $can_log_into = $patron->can_log_into( $library );
+
+Given a I<Koha::Library> object, it returns a boolean representing
+the fact the patron can log into a the library.
+
+=cut
+
+sub can_log_into {
+ my ( $self, $library ) = @_;
+
+ my $can = 0;
+
+ if ( C4::Context->preference('IndependentBranches') ) {
+ $can = 1
+ if $self->is_superlibrarian
+ or $self->branchcode eq $library->id;
+ }
+ else {
+ # no restrictions
+ $can = 1;
+ }
+
+ return $can;
+}
+
=head3 libraries_where_can_see_patrons
my $libraries = $patron-libraries_where_can_see_patrons;
return C4::Auth::haspermission( $self->userid, $flagsrequired );
}
+=head3 is_superlibrarian
+
+ my $is_superlibrarian = $patron->is_superlibrarian;
+
+Return true if the patron is a superlibrarian.
+
+=cut
+
+sub is_superlibrarian {
+ my ($self) = @_;
+ return $self->has_permission( { superlibrarian => 1 } ) ? 1 : 0;
+}
+
=head3 is_adult
my $is_adult = $patron->is_adult
return $self;
}
-=head3 attributes
+=head3 add_extended_attribute
-my $attributes = $patron->attributes
+=cut
+
+sub add_extended_attribute {
+ my ($self, $attribute) = @_;
+
+ return Koha::Patron::Attribute->new(
+ {
+ %$attribute,
+ ( borrowernumber => $self->borrowernumber ),
+ }
+ )->store;
+
+}
+
+=head3 extended_attributes
Return object of Koha::Patron::Attributes type with all attributes set for this patron
+Or setter FIXME
+
=cut
-sub attributes {
+sub extended_attributes {
+ my ( $self, $attributes ) = @_;
+ if ($attributes) { # setter
+ my $schema = $self->_result->result_source->schema;
+ $schema->txn_do(
+ sub {
+ # Remove the existing one
+ $self->extended_attributes->filter_by_branch_limitations->delete;
+
+ # Insert the new ones
+ my $new_types = {};
+ for my $attribute (@$attributes) {
+ $self->add_extended_attribute($attribute);
+ $new_types->{$attribute->{code}} = 1;
+ }
+
+ # Check globally mandatory types
+ my @required_attribute_types =
+ Koha::Patron::Attribute::Types->search(
+ {
+ mandatory => 1,
+ category_code => [ undef, $self->categorycode ],
+ 'borrower_attribute_types_branches.b_branchcode' =>
+ undef,
+ },
+ { join => 'borrower_attribute_types_branches' }
+ )->get_column('code');
+ for my $type ( @required_attribute_types ) {
+ Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute->throw(
+ type => $type,
+ ) if !$new_types->{$type};
+ }
+ }
+ );
+ }
+
+ my $rs = $self->_result->borrower_attributes;
+ # We call search to use the filters in Koha::Patron::Attributes->search
+ return Koha::Patron::Attributes->_new_from_dbic($rs)->search;
+}
+
+=head3 messages
+
+ my $messages = $patron->messages;
+
+Return the message attached to the patron.
+
+=cut
+
+sub messages {
my ( $self ) = @_;
- return Koha::Patron::Attributes->search({
- borrowernumber => $self->borrowernumber,
- branchcode => $self->branchcode,
- });
+ my $messages_rs = $self->_result->messages_borrowernumbers->search;
+ return Koha::Patron::Messages->_new_from_dbic($messages_rs);
}
=head3 lock
split /\s*\|\s*/, C4::Context->preference('BorrowerMandatoryField') };
$mandatory->{userid} = 1; # needed since sub store does not clear field
my @columns = $self->_result->result_source->columns;
- @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized/ } @columns;
+ @columns = grep { !/borrowernumber|branchcode|categorycode|^date|password|flags|updated_on|lastseen|lang|login_attempts|anonymized|auth_method/ } @columns;
push @columns, 'dateofbirth'; # add this date back in
foreach my $col (@columns) {
$self->_anonymize_column($col, $mandatory->{lc $col} );
=head3 add_guarantor
- my @relationships = $patron->add_guarantor(
+ my $relationship = $patron->add_guarantor(
{
borrowernumber => $borrowernumber,
relationships => $relationship,
)->store();
}
+=head3 get_extended_attribute
+
+my $attribute_value = $patron->get_extended_attribute( $code );
+
+Return the attribute for the code passed in parameter.
+
+It not exist it returns undef
+
+Note that this will not work for repeatable attribute types.
+
+Maybe you certainly not want to use this method, it is actually only used for SHOW_BARCODE
+(which should be a real patron's attribute (not extended)
+
+=cut
+
+sub get_extended_attribute {
+ my ( $self, $code, $value ) = @_;
+ my $rs = $self->_result->borrower_attributes;
+ return unless $rs;
+ my $attribute = $rs->search({ code => $code, ( $value ? ( attribute => $value ) : () ) });
+ return unless $attribute->count;
+ return $attribute->next;
+}
+
=head3 to_api
my $json = $patron->to_api;
emailpro => 'secondary_email',
flags => undef, # permissions manipulation handled in /permissions
gonenoaddress => 'incorrect_address',
- guarantorid => 'guarantor_id',
lastseen => 'last_seen',
lost => 'patron_card_lost',
opacnote => 'opac_notes',
smsalertnumber => 'sms_number',
sort1 => 'statistics_1',
sort2 => 'statistics_2',
+ autorenew_checkouts => 'autorenew_checkouts',
streetnumber => 'street_number',
streettype => 'street_type',
zipcode => 'postal_code',
altcontactphone => 'altcontact_phone',
altcontactsurname => 'altcontact_surname',
altcontactstate => 'altcontact_state',
- altcontactzipcode => 'altcontact_postal_code'
+ altcontactzipcode => 'altcontact_postal_code',
+ password_expiration_date => undef,
+ primary_contact_method => undef,
+ secret => undef,
+ auth_method => undef,
};
}
+=head3 queue_notice
+
+ Koha::Patrons->queue_notice({ letter_params => $letter_params, message_name => 'DUE'});
+ Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports });
+ Koha::Patrons->queue_notice({ letter_params => $letter_params, message_transports => \@message_transports, test_mode => 1 });
+
+ Queue messages to a patron. Can pass a message that is part of the message_attributes
+ table or supply the transport to use.
+
+ If passed a message name we retrieve the patrons preferences for transports
+ Otherwise we use the supplied transport. In the case of email or sms we fall back to print if
+ we have no address/number for sending
+
+ $letter_params is a hashref of the values to be passed to GetPreparedLetter
+
+ test_mode will only report which notices would be sent, but nothing will be queued
+
+=cut
+
+sub queue_notice {
+ my ( $self, $params ) = @_;
+ my $letter_params = $params->{letter_params};
+ my $test_mode = $params->{test_mode};
+
+ return unless $letter_params;
+ return unless exists $params->{message_name} xor $params->{message_transports}; # We only want one of these
+
+ my $library = Koha::Libraries->find( $letter_params->{branchcode} );
+ my $from_email_address = $library->from_email_address;
+
+ my @message_transports;
+ my $letter_code;
+ $letter_code = $letter_params->{letter_code};
+ if( $params->{message_name} ){
+ my $messaging_prefs = C4::Members::Messaging::GetMessagingPreferences( {
+ borrowernumber => $letter_params->{borrowernumber},
+ message_name => $params->{message_name}
+ } );
+ @message_transports = ( keys %{ $messaging_prefs->{transports} } );
+ $letter_code = $messaging_prefs->{transports}->{$message_transports[0]} unless $letter_code;
+ } else {
+ @message_transports = @{$params->{message_transports}};
+ }
+ return unless defined $letter_code;
+ $letter_params->{letter_code} = $letter_code;
+ my $print_sent = 0;
+ my %return;
+ foreach my $mtt (@message_transports){
+ next if ($mtt eq 'itiva' and C4::Context->preference('TalkingTechItivaPhoneNotification') );
+ # Notice is handled by TalkingTech_itiva_outbound.pl
+ if ( ( $mtt eq 'email' and not $self->notice_email_address )
+ or ( $mtt eq 'sms' and not $self->smsalertnumber )
+ or ( $mtt eq 'phone' and not $self->phone ) )
+ {
+ push @{ $return{fallback} }, $mtt;
+ $mtt = 'print';
+ }
+ next if $mtt eq 'print' && $print_sent;
+ $letter_params->{message_transport_type} = $mtt;
+ my $letter = C4::Letters::GetPreparedLetter( %$letter_params );
+ C4::Letters::EnqueueLetter({
+ letter => $letter,
+ borrowernumber => $self->borrowernumber,
+ from_address => $from_email_address,
+ message_transport_type => $mtt
+ }) unless $test_mode;
+ push @{$return{sent}}, $mtt;
+ $print_sent = 1 if $mtt eq 'print';
+ }
+ return \%return;
+}
+
+=head3 safe_to_delete
+
+ my $result = $patron->safe_to_delete;
+ if ( $result eq 'has_guarantees' ) { ... }
+ elsif ( $result ) { ... }
+ else { # cannot delete }
+
+This method tells if the Koha:Patron object can be deleted. Possible return values
+
+=over 4
+
+=item 'ok'
+
+=item 'has_checkouts'
+
+=item 'has_debt'
+
+=item 'has_guarantees'
+
+=item 'is_anonymous_patron'
+
+=back
+
+=cut
+
+sub safe_to_delete {
+ my ($self) = @_;
+
+ my $anonymous_patron = C4::Context->preference('AnonymousPatron');
+
+ my $error;
+
+ if ( $anonymous_patron && $self->id eq $anonymous_patron ) {
+ $error = 'is_anonymous_patron';
+ }
+ elsif ( $self->checkouts->count ) {
+ $error = 'has_checkouts';
+ }
+ elsif ( $self->account->outstanding_debits->total_outstanding > 0 ) {
+ $error = 'has_debt';
+ }
+ elsif ( $self->guarantee_relationships->count ) {
+ $error = 'has_guarantees';
+ }
+
+ if ( $error ) {
+ return Koha::Result::Boolean->new(0)->add_message({ message => $error });
+ }
+
+ return Koha::Result::Boolean->new(1);
+}
+
+=head3 recalls
+
+ my $recalls = $patron->recalls;
+
+Return the patron's recalls.
+
+=cut
+
+sub recalls {
+ my ( $self ) = @_;
+
+ return Koha::Recalls->search({ patron_id => $self->borrowernumber });
+}
+
+=head3 account_balance
+
+ my $balance = $patron->account_balance
+
+Return the patron's account balance
+
+=cut
+
+sub account_balance {
+ my ($self) = @_;
+ return $self->account->balance;
+}
+
+=head3 notify_library_of_registration
+
+$patron->notify_library_of_registration( $email_patron_registrations );
+
+Send patron registration email to library if EmailPatronRegistrations system preference is enabled.
+
+=cut
+
+sub notify_library_of_registration {
+ my ( $self, $email_patron_registrations ) = @_;
+
+ if (
+ my $letter = C4::Letters::GetPreparedLetter(
+ module => 'members',
+ letter_code => 'OPAC_REG',
+ branchcode => $self->branchcode,
+ lang => $self->lang || 'default',
+ tables => {
+ 'borrowers' => $self->borrowernumber
+ },
+ )
+ ) {
+ my $to_address;
+ if ( $email_patron_registrations eq "BranchEmailAddress" ) {
+ my $library = Koha::Libraries->find( $self->branchcode );
+ $to_address = $library->inbound_email_address;
+ }
+ elsif ( $email_patron_registrations eq "KohaAdminEmailAddress" ) {
+ $to_address = C4::Context->preference('ReplytoDefault')
+ || C4::Context->preference('KohaAdminEmailAddress');
+ }
+ else {
+ $to_address =
+ C4::Context->preference('EmailAddressForPatronRegistrations')
+ || C4::Context->preference('ReplytoDefault')
+ || C4::Context->preference('KohaAdminEmailAddress');
+ }
+
+ my $message_id = C4::Letters::EnqueueLetter(
+ {
+ letter => $letter,
+ borrowernumber => $self->borrowernumber,
+ to_address => $to_address,
+ message_transport_type => 'email'
+ }
+ ) or warn "can't enqueue letter $letter";
+ if ( $message_id ) {
+ return 1;
+ }
+ }
+}
+
+=head3 has_messaging_preference
+
+my $bool = $patron->has_messaging_preference({
+ message_name => $message_name, # A value from message_attributes.message_name
+ message_transport_type => $message_transport_type, # email, sms, phone, itiva, etc...
+ wants_digest => $wants_digest, # 1 if you are looking for the digest version, don't pass if you just want either
+});
+
+=cut
+
+sub has_messaging_preference {
+ my ( $self, $params ) = @_;
+
+ my $message_name = $params->{message_name};
+ my $message_transport_type = $params->{message_transport_type};
+ my $wants_digest = $params->{wants_digest};
+
+ return $self->_result->search_related_rs(
+ 'borrower_message_preferences',
+ $params,
+ {
+ prefetch =>
+ [ 'borrower_message_transport_preferences', 'message_attribute' ]
+ }
+ )->count;
+}
+
+=head3 can_patron_change_staff_only_lists
+
+$patron->can_patron_change_staff_only_lists;
+
+Return 1 if a patron has 'Superlibrarian' or 'Catalogue' permission.
+Otherwise, return 0.
+
+=cut
+
+sub can_patron_change_staff_only_lists {
+ my ( $self, $params ) = @_;
+ return 1 if C4::Auth::haspermission( $self->userid, { 'catalogue' => 1 });
+ return 0;
+}
+
+=head3 encode_secret
+
+ $patron->encode_secret($secret32);
+
+Secret (TwoFactorAuth expects it in base32 format) is encrypted.
+You still need to call ->store.
+
+=cut
+
+sub encode_secret {
+ my ( $self, $secret ) = @_;
+ if( $secret ) {
+ return $self->secret( Koha::Encryption->new->encrypt_hex($secret) );
+ }
+ return $self->secret($secret);
+}
+
+=head3 decoded_secret
+
+ my $secret32 = $patron->decoded_secret;
+
+Decode the patron secret. We expect to get back a base32 string, but this
+is not checked here. Caller of encode_secret is responsible for that.
+
+=cut
+
+sub decoded_secret {
+ my ( $self ) = @_;
+ if( $self->secret ) {
+ return Koha::Encryption->new->decrypt_hex( $self->secret );
+ }
+ return $self->secret;
+}
+
+=head3 virtualshelves
+
+ my $shelves = $patron->virtualshelves;
+
+=cut
+
+sub virtualshelves {
+ my $self = shift;
+ return Koha::Virtualshelves->_new_from_dbic( scalar $self->_result->virtualshelves );
+}
+
=head2 Internal methods
=head3 _type