#
# This file is part of Koha.
#
-# Koha is free software; you can redistribute it and/or modify it under the
-# terms of the GNU General Public License as published by the Free Software
-# Foundation; either version 3 of the License, or (at your option) any later
-# version.
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
#
-# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
#
-# You should have received a copy of the GNU General Public License along
-# with Koha; if not, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
use Modern::Perl;
use Carp;
-use List::MoreUtils qw( uniq );
+use List::MoreUtils qw( any uniq );
use JSON qw( to_json );
-use Text::Unaccent qw( unac_string );
+use Unicode::Normalize;
-use C4::Accounts;
use C4::Context;
use C4::Log;
+use Koha::Account;
+use Koha::ArticleRequests;
use Koha::AuthUtils;
use Koha::Checkouts;
+use Koha::Club::Enrollments;
use Koha::Database;
use Koha::DateUtils;
use Koha::Exceptions::Password;
use Koha::Holds;
use Koha::Old::Checkouts;
+use Koha::Patron::Attributes;
use Koha::Patron::Categories;
use Koha::Patron::HouseboundProfile;
use Koha::Patron::HouseboundRole;
use Koha::Patron::Images;
+use Koha::Patron::Modifications;
+use Koha::Patron::Relationships;
use Koha::Patrons;
-use Koha::Virtualshelves;
-use Koha::Club::Enrollments;
-use Koha::Account;
+use Koha::Plugins;
use Koha::Subscription::Routinglists;
+use Koha::Token;
+use Koha::Virtualshelves;
use base qw(Koha::Object);
+use constant ADMINISTRATIVE_LOCKOUT => -1;
+
our $RESULTSET_PATRON_ID_MAPPING = {
Accountline => 'borrowernumber',
Aqbasketuser => 'borrowernumber',
=head2 Class Methods
-=cut
-
=head3 new
=cut
$self->trim_whitespaces;
- # We don't want invalid dates in the db (mysql has a bad habit of inserting 0000-00-00)
- $self->dateofbirth(undef) unless $self->dateofbirth;
- $self->debarred(undef) unless $self->debarred;
- $self->date_renewed(undef) unless $self->date_renewed;
- $self->lastseen(undef) unless $self->lastseen;
- $self->updated_on(undef) unless $self->updated_on;
-
- # Set default values if not set
- $self->sms_provider_id(undef) unless $self->sms_provider_id;
- $self->guarantorid(undef) unless $self->guarantorid;
+ # Set surname to uppercase if uppercasesurname is true
+ $self->surname( uc($self->surname) )
+ if C4::Context->preference("uppercasesurnames");
- # If flags == 0 or flags == '' => no permission
- $self->flags(undef) unless $self->flags;
-
- # tinyint or int
- $self->gonenoaddress(0) unless $self->gonenoaddress;
- $self->login_attempts(0) unless $self->login_attempts;
- $self->privacy_guarantor_checkouts(0) unless $self->privacy_guarantor_checkouts;
- $self->lost(0) unless $self->lost;
+ $self->relationship(undef) # We do not want to store an empty string in this field
+ if defined $self->relationship
+ and $self->relationship eq "";
unless ( $self->in_storage ) { #AddMember
: undef;
$self->privacy($default_privacy);
- unless ( defined $self->privacy_guarantor_checkouts ) {
- $self->privacy_guarantor_checkouts(0);
+ # Call any check_password plugins if password is passed
+ if ( C4::Context->config("enable_plugins") && $self->password ) {
+ my @plugins = Koha::Plugins->new()->GetPlugins({
+ method => 'check_password',
+ });
+ foreach my $plugin ( @plugins ) {
+ # This plugin hook will also be used by a plugin for the Norwegian national
+ # patron database. This is why we need to pass both the password and the
+ # borrowernumber to the plugin.
+ my $ret = $plugin->check_password(
+ {
+ password => $self->password,
+ borrowernumber => $self->borrowernumber
+ }
+ );
+ if ( $ret->{'error'} == 1 ) {
+ Koha::Exceptions::Password::Plugin->throw();
+ }
+ }
}
# Make a copy of the plain text password for later use
$self = $self->SUPER::store;
- $self->add_enrolment_fee_if_needed;
+ $self->add_enrolment_fee_if_needed(0);
logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
if C4::Context->preference("BorrowersLog");
}
else { #ModMember
- # Come from ModMember, but should not be possible (?)
- $self->dateenrolled(undef) unless $self->dateenrolled;
- $self->dateexpiry(undef) unless $self->dateexpiry;
-
-
my $self_from_storage = $self->get_from_storage;
# FIXME We should not deal with that here, callers have to do this job
# Moved from ModMember to prevent regressions
$self->userid($stored_userid);
}
- # Password must be updated using $self->update_password
+ # Password must be updated using $self->set_password
$self->password($self_from_storage->password);
- if ( C4::Context->preference('FeeOnChangePatronCategory')
- and $self->category->categorycode ne
+ if ( $self->category->categorycode ne
$self_from_storage->category->categorycode )
{
- $self->add_enrolment_fee_if_needed;
- }
+ # Add enrolement fee on category change if required
+ $self->add_enrolment_fee_if_needed(1)
+ if C4::Context->preference('FeeOnChangePatronCategory');
+
+ # 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' );
- my $borrowers_log = C4::Context->preference("BorrowersLog");
- my $previous_cardnumber = $self_from_storage->cardnumber;
- if ($borrowers_log
- && ( !defined $previous_cardnumber
- || $previous_cardnumber ne $self->cardnumber )
- )
- {
- logaction(
- "MEMBERS",
- "MODIFY",
- $self->borrowernumber,
- to_json(
- {
- cardnumber_replaced => {
- previous_cardnumber => $previous_cardnumber,
- new_cardnumber => $self->cardnumber,
- }
- },
- { utf8 => 1, pretty => 1 }
- )
- );
}
- logaction( "MEMBERS", "MODIFY", $self->borrowernumber,
- "UPDATE (executed w/ arg: " . $self->borrowernumber . ")" )
- if $borrowers_log;
+ # Actionlogs
+ if ( C4::Context->preference("BorrowersLog") ) {
+ my $info;
+ my $from_storage = $self_from_storage->unblessed;
+ my $from_object = $self->unblessed;
+ my @skip_fields = (qw/lastseen updated_on/);
+ for my $key ( keys %{$from_storage} ) {
+ next if any { /$key/ } @skip_fields;
+ if (
+ (
+ !defined( $from_storage->{$key} )
+ && defined( $from_object->{$key} )
+ )
+ || ( defined( $from_storage->{$key} )
+ && !defined( $from_object->{$key} ) )
+ || (
+ defined( $from_storage->{$key} )
+ && defined( $from_object->{$key} )
+ && ( $from_storage->{$key} ne
+ $from_object->{$key} )
+ )
+ )
+ {
+ $info->{$key} = {
+ before => $from_storage->{$key},
+ after => $from_object->{$key}
+ };
+ }
+ }
+ if ( defined($info) ) {
+ logaction(
+ "MEMBERS",
+ "MODIFY",
+ $self->borrowernumber,
+ to_json(
+ $info,
+ { utf8 => 1, pretty => 1, canonical => 1 }
+ )
+ );
+ }
+ }
+
+ # Final store
$self = $self->SUPER::store;
}
}
sub delete {
my ($self) = @_;
- my $deleted;
$self->_result->result_source->schema->txn_do(
sub {
- # Delete Patron's holds
- $self->holds->delete;
+ # Cancel Patron's holds
+ my $holds = $self->holds;
+ while( my $hold = $holds->next ){
+ $hold->cancel;
+ }
# Delete all lists and all shares of this borrower
# Consistent with the approach Koha uses on deleting individual lists
# FIXME Could be $patron->get_lists
$_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
- $deleted = $self->SUPER::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 } );
+
+ $self->SUPER::delete;
logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
}
);
- return $deleted;
+ return $self;
}
return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
}
-=head3 guarantor
-
-Returns a Koha::Patron object for this patron's guarantor
+=head3 image
=cut
-sub guarantor {
+sub image {
my ( $self ) = @_;
- return unless $self->guarantorid();
-
- return Koha::Patrons->find( $self->guarantorid() );
+ return Koha::Patron::Images->find( $self->borrowernumber );
}
-sub image {
- my ( $self ) = @_;
+=head3 library
- return scalar Koha::Patron::Images->find( $self->borrowernumber );
-}
+Returns a Koha::Library object representing the patron's home library.
+
+=cut
sub library {
my ( $self ) = @_;
return Koha::Library->_new_from_dbic($self->_result->branchcode);
}
-=head3 guarantees
+=head3 sms_provider
-Returns the guarantees (list of Koha::Patron) of this patron
+Returns a Koha::SMS::Provider object representing the patron's SMS provider.
=cut
-sub guarantees {
+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
+
+Returns the set of relationships for the patrons that are guarantors for this patron.
+
+This is returned instead of a Koha::Patron object because the guarantor
+may not exist as a patron in Koha. If this is true, the guarantors name
+exists in the Koha::Patron::Relationship object and will have no guarantor_id.
+
+=cut
+
+sub guarantor_relationships {
+ my ($self) = @_;
+
+ return Koha::Patron::Relationships->search( { guarantee_id => $self->id } );
+}
+
+=head3 guarantee_relationships
+
+Returns Koha::Patron::Relationships object for this patron's guarantors
+
+Returns the set of relationships for the patrons that are guarantees for this patron.
+
+The method returns Koha::Patron::Relationship objects for the sake
+of consistency with the guantors method.
+A guarantee by definition must exist as a patron in Koha.
+
+=cut
+
+sub guarantee_relationships {
+ my ($self) = @_;
- return Koha::Patrons->search( { guarantorid => $self->borrowernumber }, { order_by => { -asc => ['surname','firstname'] } } );
+ return Koha::Patron::Relationships->search(
+ { guarantor_id => $self->id },
+ {
+ prefetch => 'guarantee',
+ order_by => { -asc => [ 'guarantee.surname', 'guarantee.firstname' ] },
+ }
+ );
}
=head3 housebound_profile
=cut
sub siblings {
- my ( $self ) = @_;
+ my ($self) = @_;
- my $guarantor = $self->guarantor;
+ my @guarantors = $self->guarantor_relationships()->guarantors();
- return unless $guarantor;
+ return unless @guarantors;
- return Koha::Patrons->search(
- {
- guarantorid => {
- '!=' => undef,
- '=' => $guarantor->id,
- },
- borrowernumber => {
- '!=' => $self->borrowernumber,
- }
- }
- );
+ my @siblings =
+ map { $_->guarantee_relationships()->guarantees() } @guarantors;
+
+ return unless @siblings;
+
+ my %seen;
+ @siblings =
+ grep { !$seen{ $_->id }++ && ( $_->id != $self->id ) } @siblings;
+
+ return wantarray ? @siblings : Koha::Patrons->search( { borrowernumber => { -in => [ map { $_->id } @siblings ] } } );
}
=head3 merge_with
sub do_check_for_previous_checkout {
my ( $self, $item ) = @_;
- # Find all items for bib and extract item numbers.
- my @items = Koha::Items->search({biblionumber => $item->{biblionumber}});
my @item_nos;
- foreach my $item (@items) {
- push @item_nos, $item->itemnumber;
+ my $biblio = Koha::Biblios->find( $item->{biblionumber} );
+ if ( $biblio->is_serial ) {
+ push @item_nos, $item->{itemnumber};
+ } else {
+ # Get all itemnumbers for given bibliographic record.
+ @item_nos = $biblio->items->get_column( 'itemnumber' );
}
# Create (old)issues search criteria
return 0 unless $delay;
return 0 unless $self->dateexpiry;
return 0 if $self->dateexpiry =~ '^9999';
- return 1 if dt_from_string( $self->dateexpiry )->subtract( days => $delay ) < dt_from_string->truncate( to => 'day' );
+ return 1 if dt_from_string( $self->dateexpiry, undef, 'floating' )->subtract( days => $delay ) < dt_from_string(undef, undef, 'floating')->truncate( to => 'day' );
return 0;
}
-=head3 update_password
-
-my $updated = $patron->update_password( $userid, $password );
-
-Update the userid and the password of a patron.
-If the userid already exists, returns and let DBIx::Class warns
-This will add an entry to action_logs if BorrowersLog is set.
-
-=cut
-
-sub update_password {
- my ( $self, $userid, $password ) = @_;
- eval { $self->userid($userid)->store; };
- return if $@; # Make sure the userid is not already in used by another patron
-
- return 0 if $password eq '****' or $password eq '';
-
- my $digest = Koha::AuthUtils::hash_password($password);
- $self->update(
- {
- password => $digest,
- login_attempts => 0,
- }
- );
-
- logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
- return $digest;
-}
-
=head3 set_password
- $patron->set_password( $plain_text_password );
+ $patron->set_password({ password => $plain_text_password [, skip_validation => 1 ] });
Set the patron's password.
=head4 Exceptions
The passed string is validated against the current password enforcement policy.
+Validation can be skipped by passing the I<skip_validation> parameter.
+
Exceptions are thrown if the password is not good enough.
=over 4
=item Koha::Exceptions::Password::TooWeak
+=item Koha::Exceptions::Password::Plugin (if a "check password" plugin is enabled)
+
=back
=cut
sub set_password {
- my ( $self, $password ) = @_;
+ my ( $self, $args ) = @_;
- my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password );
+ my $password = $args->{password};
- if ( !$is_valid ) {
- if ( $error eq 'too_short' ) {
- my $min_length = C4::Context->preference('minPasswordLength');
- $min_length = 3 if not $min_length or $min_length < 3;
+ unless ( $args->{skip_validation} ) {
+ my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password );
- my $password_length = length($password);
- Koha::Exceptions::Password::TooShort->throw(
- { length => $password_length, min_length => $min_length } );
- }
- elsif ( $error eq 'has_whitespaces' ) {
- Koha::Exceptions::Password::WhitespaceCharacters->throw();
+ if ( !$is_valid ) {
+ if ( $error eq 'too_short' ) {
+ my $min_length = C4::Context->preference('minPasswordLength');
+ $min_length = 3 if not $min_length or $min_length < 3;
+
+ my $password_length = length($password);
+ Koha::Exceptions::Password::TooShort->throw(
+ length => $password_length, min_length => $min_length );
+ }
+ elsif ( $error eq 'has_whitespaces' ) {
+ Koha::Exceptions::Password::WhitespaceCharacters->throw();
+ }
+ elsif ( $error eq 'too_weak' ) {
+ Koha::Exceptions::Password::TooWeak->throw();
+ }
}
- elsif ( $error eq 'too_weak' ) {
- Koha::Exceptions::Password::TooWeak->throw();
+ }
+
+ if ( C4::Context->config("enable_plugins") ) {
+ # Call any check_password plugins
+ my @plugins = Koha::Plugins->new()->GetPlugins({
+ method => 'check_password',
+ });
+ foreach my $plugin ( @plugins ) {
+ # This plugin hook will also be used by a plugin for the Norwegian national
+ # patron database. This is why we need to pass both the password and the
+ # borrowernumber to the plugin.
+ my $ret = $plugin->check_password(
+ {
+ password => $password,
+ borrowernumber => $self->borrowernumber
+ }
+ );
+ # This plugin hook will also be used by a plugin for the Norwegian national
+ # patron database. This is why we need to call the actual plugins and then
+ # check skip_validation afterwards.
+ if ( $ret->{'error'} == 1 && !$args->{skip_validation} ) {
+ Koha::Exceptions::Password::Plugin->throw();
+ }
}
}
my $digest = Koha::AuthUtils::hash_password($password);
- $self->update(
- { password => $digest,
- login_attempts => 0,
- }
- );
+
+ # We do not want to call $self->store and retrieve password from DB
+ $self->password($digest);
+ $self->login_attempts(0);
+ $self->SUPER::store;
logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" )
if C4::Context->preference("BorrowersLog");
$self->date_renewed( dt_from_string() );
$self->store();
- $self->add_enrolment_fee_if_needed;
+ $self->add_enrolment_fee_if_needed(1);
logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
return dt_from_string( $expiry_date )->truncate( to => 'day' );
=head3 add_enrolment_fee_if_needed
-my $enrolment_fee = $patron->add_enrolment_fee_if_needed;
+my $enrolment_fee = $patron->add_enrolment_fee_if_needed($renewal);
Add enrolment fee for a patron if needed.
+$renewal - boolean denoting whether this is an account renewal or not
+
=cut
sub add_enrolment_fee_if_needed {
- my ($self) = @_;
+ my ($self, $renewal) = @_;
my $enrolment_fee = $self->category->enrolmentfee;
if ( $enrolment_fee && $enrolment_fee > 0 ) {
- # insert fee in patron debts
- C4::Accounts::manualinvoice( $self->borrowernumber, '', '', 'A', $enrolment_fee );
+ my $type = $renewal ? 'ACCOUNT_RENEW' : 'ACCOUNT';
+ $self->account->add_debit(
+ {
+ amount => $enrolment_fee,
+ user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
+ interface => C4::Context->interface,
+ library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
+ type => $type
+ }
+ );
}
return $enrolment_fee || 0;
}
It should not be used directly, prefer to access fields you need instead of
retrieving all these fields in one go.
-
=cut
sub pending_checkouts {
return $age;
}
+=head3 is_valid_age
+
+my $is_valid = $patron->is_valid_age
+
+Return 1 if patron's age is between allowed limits, returns 0 if it's not.
+
+=cut
+
+sub is_valid_age {
+ my ($self) = @_;
+ my $age = $self->get_age;
+
+ my $patroncategory = $self->category;
+ my ($low,$high) = ($patroncategory->dateofbirthrequired, $patroncategory->upperagelimit);
+
+ return (defined($age) && (($high && ($age > $high)) or ($age < $low))) ? 0 : 1;
+}
+
=head3 account
my $account = $patron->account
return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
}
+=head3 return_claims
+
+my $return_claims = $patron->return_claims
+
+=cut
+
+sub return_claims {
+ my ($self) = @_;
+ my $return_claims = $self->_result->return_claims_borrowernumbers;
+ return Koha::Checkouts::ReturnClaims->_new_from_dbic( $return_claims );
+}
+
=head3 notice_email_address
my $email = $patron->notice_email_address;
my $is_locked = $patron->account_locked
-Return true if the patron has reach the maximum number of login attempts (see pref FailedLoginAttempts).
+Return true if the patron has reached the maximum number of login attempts
+(see pref FailedLoginAttempts). If login_attempts is < 0, this is interpreted
+as an administrative lockout (independent of FailedLoginAttempts; see also
+Koha::Patron->lock).
Otherwise return false.
-If the pref is not set (empty string, null or 0), the feature is considered as disabled.
+If the pref is not set (empty string, null or 0), the feature is considered as
+disabled.
=cut
sub account_locked {
my ($self) = @_;
my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
- return ( $FailedLoginAttempts
+ return 1 if $FailedLoginAttempts
and $self->login_attempts
- and $self->login_attempts >= $FailedLoginAttempts )? 1 : 0;
+ and $self->login_attempts >= $FailedLoginAttempts;
+ return 1 if ($self->login_attempts || 0) < 0; # administrative lockout
+ return 0;
}
=head3 can_see_patron_infos
sub can_see_patron_infos {
my ( $self, $patron ) = @_;
+ return unless $patron;
return $self->can_see_patrons_from( $patron->library->branchcode );
}
return @restricted_branchcodes;
}
+=head3 has_permission
+
+my $permission = $patron->has_permission($required);
+
+See C4::Auth::haspermission for details of syntax for $required
+
+=cut
+
sub has_permission {
my ( $self, $flagsrequired ) = @_;
return unless $self->userid;
Return true if the patron has a category with a type Child (C)
=cut
+
sub is_child {
my( $self ) = @_;
return $self->category->category_type eq 'C' ? 1 : 0;
$firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
$surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
- $userid = unac_string('utf-8',$userid);
+ $userid = NFKD( $userid );
+ $userid =~ s/\p{NonspacingMark}//g;
$userid .= $offset unless $offset == 0;
$self->userid( $userid );
$offset++;
} while (! $self->has_valid_userid );
return $self;
+}
+
+=head3 add_extended_attribute
+
+=cut
+
+sub add_extended_attribute {
+ my ($self, $attribute) = @_;
+ $attribute->{borrowernumber} = $self->borrowernumber;
+ return Koha::Patron::Attribute->new($attribute)->store;
+}
+
+=head3 extended_attributes
+
+Return object of Koha::Patron::Attributes type with all attributes set for this patron
+
+Or setter FIXME
+
+=cut
+
+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
+ 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 $@;
+ }
+ }
+ );
+ }
+
+ 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 lock
+
+ Koha::Patrons->find($id)->lock({ expire => 1, remove => 1 });
+
+ Lock and optionally expire a patron account.
+ Remove holds and article requests if remove flag set.
+ In order to distinguish from locking by entering a wrong password, let's
+ call this an administrative lockout.
+
+=cut
+
+sub lock {
+ my ( $self, $params ) = @_;
+ $self->login_attempts( ADMINISTRATIVE_LOCKOUT );
+ if( $params->{expire} ) {
+ $self->dateexpiry( dt_from_string->subtract(days => 1) );
+ }
+ $self->store;
+ if( $params->{remove} ) {
+ $self->holds->delete;
+ $self->article_requests->delete;
+ }
+ return $self;
+}
+
+=head3 anonymize
+
+ Koha::Patrons->find($id)->anonymize;
+
+ Anonymize or clear borrower fields. Fields in BorrowerMandatoryField
+ are randomized, other personal data is cleared too.
+ Patrons with issues are skipped.
+=cut
+
+sub anonymize {
+ my ( $self ) = @_;
+ if( $self->_result->issues->count ) {
+ warn "Exiting anonymize: patron ".$self->borrowernumber." still has issues";
+ return;
+ }
+ # Mandatory fields come from the corresponding pref, but email fields
+ # are removed since scrambled email addresses only generate errors
+ my $mandatory = { map { (lc $_, 1); } grep { !/email/ }
+ 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;
+ push @columns, 'dateofbirth'; # add this date back in
+ foreach my $col (@columns) {
+ $self->_anonymize_column($col, $mandatory->{lc $col} );
+ }
+ $self->anonymized(1)->store;
+}
+
+sub _anonymize_column {
+ my ( $self, $col, $mandatory ) = @_;
+ my $col_info = $self->_result->result_source->column_info($col);
+ my $type = $col_info->{data_type};
+ my $nullable = $col_info->{is_nullable};
+ my $val;
+ if( $type =~ /char|text/ ) {
+ $val = $mandatory
+ ? Koha::Token->new->generate({ pattern => '\w{10}' })
+ : $nullable
+ ? undef
+ : q{};
+ } elsif( $type =~ /integer|int$|float|dec|double/ ) {
+ $val = $nullable ? undef : 0;
+ } elsif( $type =~ /date|time/ ) {
+ $val = $nullable ? undef : dt_from_string;
+ }
+ $self->$col($val);
+}
+
+=head3 add_guarantor
+
+ my @relationships = $patron->add_guarantor(
+ {
+ borrowernumber => $borrowernumber,
+ relationships => $relationship,
+ }
+ );
+
+ Adds a new guarantor to a patron.
+
+=cut
+
+sub add_guarantor {
+ my ( $self, $params ) = @_;
+
+ my $guarantor_id = $params->{guarantor_id};
+ my $relationship = $params->{relationship};
+
+ return Koha::Patron::Relationship->new(
+ {
+ guarantee_id => $self->id,
+ guarantor_id => $guarantor_id,
+ relationship => $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;
+
+Overloaded method that returns a JSON representation of the Koha::Patron object,
+suitable for API output.
+
+=cut
+
+sub to_api {
+ my ( $self, $params ) = @_;
+
+ my $json_patron = $self->SUPER::to_api( $params );
+
+ $json_patron->{restricted} = ( $self->is_debarred )
+ ? Mojo::JSON->true
+ : Mojo::JSON->false;
+
+ return $json_patron;
+}
+
+=head3 to_api_mapping
+
+This method returns the mapping for representing a Koha::Patron object
+on the API.
+
+=cut
+
+sub to_api_mapping {
+ return {
+ borrowernotes => 'staff_notes',
+ borrowernumber => 'patron_id',
+ branchcode => 'library_id',
+ categorycode => 'category_id',
+ checkprevcheckout => 'check_previous_checkout',
+ contactfirstname => undef, # Unused
+ contactname => undef, # Unused
+ contactnote => 'altaddress_notes',
+ contacttitle => undef, # Unused
+ dateenrolled => 'date_enrolled',
+ dateexpiry => 'expiry_date',
+ dateofbirth => 'date_of_birth',
+ debarred => undef, # replaced by 'restricted'
+ debarredcomment => undef, # calculated, API consumers will use /restrictions instead
+ 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',
+ othernames => 'other_name',
+ password => undef, # password manipulation handled in /password
+ phonepro => 'secondary_phone',
+ relationship => 'relationship_type',
+ sex => 'gender',
+ smsalertnumber => 'sms_number',
+ sort1 => 'statistics_1',
+ sort2 => 'statistics_2',
+ autorenew_checkouts => 'autorenew_checkouts',
+ streetnumber => 'street_number',
+ streettype => 'street_type',
+ zipcode => 'postal_code',
+ B_address => 'altaddress_address',
+ B_address2 => 'altaddress_address2',
+ B_city => 'altaddress_city',
+ B_country => 'altaddress_country',
+ B_email => 'altaddress_email',
+ B_phone => 'altaddress_phone',
+ B_state => 'altaddress_state',
+ B_streetnumber => 'altaddress_street_number',
+ B_streettype => 'altaddress_street_type',
+ B_zipcode => 'altaddress_postal_code',
+ altcontactaddress1 => 'altcontact_address',
+ altcontactaddress2 => 'altcontact_address2',
+ altcontactaddress3 => 'altcontact_city',
+ altcontactcountry => 'altcontact_country',
+ altcontactfirstname => 'altcontact_firstname',
+ altcontactphone => 'altcontact_phone',
+ altcontactsurname => 'altcontact_surname',
+ altcontactstate => 'altcontact_state',
+ altcontactzipcode => 'altcontact_postal_code'
+ };
}
=head2 Internal methods
return 'Borrower';
}
-=head1 AUTHOR
+=head1 AUTHORS
Kyle M Hall <kyle@bywatersolutions.com>
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
+Martin Renvoize <martin.renvoize@ptfs-europe.com>
=cut