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 C4::Context;
-use C4::Log;
+use C4::Log qw( logaction );
use Koha::Account;
use Koha::ArticleRequests;
+use C4::Letters;
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::Exceptions::Password;
use Koha::Holds;
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;
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");
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
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;
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 ) {
+ $self->add_extended_attribute($attribute);
+ }
+
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 Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
}
-=head3 article_requests
+=head3 can_request_article
-my @requests = $borrower->article_requests();
-my $requests = $borrower->article_requests();
+ if ( $patron->can_request_article( $library->id ) ) { ... }
-Returns either a list of ArticleRequests objects,
-or an ArtitleRequests object, depending on the
-calling context.
+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.
-=cut
-
-sub article_requests {
- my ( $self ) = @_;
-
- $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
-
- return $self->{_article_requests};
-}
-
-=head3 article_requests_current
-
-my @requests = $patron->article_requests_current
-
-Returns the article requests associated with this patron that are incomplete
+A I<library_id> can be passed as parameter, falling back to userenv if absent.
=cut
-sub article_requests_current {
- my ( $self ) = @_;
+sub can_request_article {
+ my ($self, $library_id) = @_;
- $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
+ $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 => 'open_article_requests_limit'
}
);
- return $self->{_article_requests_current};
+ 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_finished
+=head3 article_requests
-my @requests = $biblio->article_requests_finished
+ my $article_requests = $patron->article_requests;
-Returns the article requests associated with this patron that are completed
+Returns the patron article requests.
=cut
-sub article_requests_finished {
- my ( $self, $borrower ) = @_;
-
- $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
- {
- borrowernumber => $self->id(),
- -or => [
- { status => Koha::ArticleRequest::Status::Completed },
- { status => Koha::ArticleRequest::Status::Canceled }
- ]
- }
- );
+sub article_requests {
+ my ($self) = @_;
- return $self->{_article_requests_finished};
+ return Koha::ArticleRequests->_new_from_dbic( scalar $self->_result->article_requests );
}
=head3 add_enrolment_fee_if_needed
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 $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;
sub add_extended_attribute {
my ($self, $attribute) = @_;
- $attribute->{borrowernumber} = $self->borrowernumber;
- return Koha::Patron::Attribute->new($attribute)->store;
+
+ return Koha::Patron::Attribute->new(
+ {
+ %$attribute,
+ ( borrowernumber => $self->borrowernumber ),
+ }
+ )->store;
+
}
=head3 extended_attributes
$self->extended_attributes->filter_by_branch_limitations->delete;
# Insert the new ones
+ my $new_types = {};
for my $attribute (@$attributes) {
- eval {
- $self->_result->create_related('borrower_attributes', $attribute);
- };
- # FIXME We should:
- # 1 - Raise an exception
- # 2 - Execute in a transaction and don't save
- # or Insert anyway but display a message on the UI
- warn $@ if $@;
+ $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,
+ '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};
}
}
);
altcontactphone => 'altcontact_phone',
altcontactsurname => 'altcontact_surname',
altcontactstate => 'altcontact_state',
- altcontactzipcode => 'altcontact_postal_code'
+ altcontactzipcode => 'altcontact_postal_code',
+ primary_contact_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;
+}
+
=head2 Internal methods
=head3 _type