Bug 29290: Add GET /biblios/:biblio_id/checkouts
[koha-ffzg.git] / Koha / Patron.pm
index db04efc..6160c44 100644 (file)
@@ -5,46 +5,52 @@ package Koha::Patron;
 #
 # 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( any uniq );
 use JSON qw( to_json );
-use Text::Unaccent qw( unac_string );
+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;
+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);
 
@@ -56,7 +62,6 @@ our $RESULTSET_PATRON_ID_MAPPING = {
     Aqbudget             => 'budget_owner_id',
     Aqbudgetborrower     => 'borrowernumber',
     ArticleRequest       => 'borrowernumber',
-    BorrowerAttribute    => 'borrowernumber',
     BorrowerDebarment    => 'borrowernumber',
     BorrowerFile         => 'borrowernumber',
     BorrowerModification => 'borrowernumber',
@@ -106,7 +111,11 @@ Autogenerate next cardnumber from highest value found in database
 
 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)',
@@ -193,9 +202,17 @@ sub store {
 
             $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("uppercasesurname");
+                if C4::Context->preference("uppercasesurnames");
+
+            $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
 
@@ -222,6 +239,26 @@ sub store {
                   :                                                   undef;
                 $self->privacy($default_privacy);
 
+                # 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->plain_text_password( $self->password );
@@ -235,7 +272,7 @@ sub store {
 
                 $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");
@@ -253,11 +290,18 @@ sub store {
                 # 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' );
+
                 }
 
                 # Actionlogs
@@ -325,7 +369,9 @@ other lists are kept.
 sub delete {
     my ($self) = @_;
 
-    my $deleted;
+    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
@@ -350,12 +396,16 @@ sub delete {
             # 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;
 }
 
 
@@ -372,41 +422,127 @@ sub category {
     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::Patron::Relationships->search(
+        { guarantor_id => $self->id },
+        {
+            prefetch => 'guarantee',
+            order_by => { -asc => [ 'guarantee.surname', 'guarantee.firstname' ] },
+        }
+    );
+}
+
+=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;
+    } else {
+        # I am a guarantor, I need to get all the guarantors of all my guarantees
+        @guarantors = map { $_->guarantor_relationships->guarantors } $self->guarantee_relationships->guarantees;
+    }
+
+    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();
+        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;
+        }
 
-    return Koha::Patrons->search( { guarantorid => $self->borrowernumber }, { order_by => { -asc => ['surname','firstname'] } } );
+        $non_issues_charges += $guarantees_non_issues_charges;
+        $seen->{ $guarantor->id } = 1;
+    }
+
+    return $non_issues_charges;
 }
 
 =head3 housebound_profile
@@ -444,23 +580,22 @@ Returns the siblings of this patron.
 =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
@@ -477,6 +612,9 @@ sub siblings {
 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
@@ -488,6 +626,9 @@ sub merge_with {
 
     $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;
@@ -495,10 +636,23 @@ sub merge_with {
             # 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();
@@ -556,11 +710,13 @@ $PATRON, 0 otherwise.
 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
@@ -569,6 +725,13 @@ sub do_check_for_previous_checkout {
         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
@@ -629,7 +792,7 @@ sub is_going_to_expire {
     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;
 }
 
@@ -654,6 +817,8 @@ Exceptions are thrown if the password is not good enough.
 
 =item Koha::Exceptions::Password::TooWeak
 
+=item Koha::Exceptions::Password::Plugin (if a "check password" plugin is enabled)
+
 =back
 
 =cut
@@ -664,11 +829,11 @@ sub set_password {
     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);
@@ -684,12 +849,36 @@ sub set_password {
         }
     }
 
-    my $digest = Koha::AuthUtils::hash_password($password);
-    $self->update(
-        {   password       => $digest,
-            login_attempts => 0,
+    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);
+
+    # 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");
@@ -723,7 +912,7 @@ sub renew_account {
     $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' );
@@ -778,92 +967,78 @@ sub move_to_deleted {
     return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
 }
 
-=head3 article_requests
-
-my @requests = $borrower->article_requests();
-my $requests = $borrower->article_requests();
-
-Returns either a list of ArticleRequests objects,
-or an ArtitleRequests object, depending on the
-calling context.
+=head3 can_request_article
 
-=cut
-
-sub article_requests {
-    my ( $self ) = @_;
-
-    $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
-
-    return $self->{_article_requests};
-}
+    if ( $patron->can_request_article( $library->id ) ) { ... }
 
-=head3 article_requests_current
+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.
 
-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 $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 ) {
+        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       => 'account'
+                type       => $type
             }
         );
     }
@@ -999,7 +1174,7 @@ sub 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
@@ -1041,6 +1216,18 @@ sub old_holds {
     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;
@@ -1177,6 +1364,33 @@ sub 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;
@@ -1227,6 +1441,14 @@ sub libraries_where_can_see_patrons {
     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;
@@ -1234,6 +1456,19 @@ sub has_permission {
     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
@@ -1316,30 +1551,78 @@ sub generate_userid {
       $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 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 {
-    my ( $self ) = @_;
-    return Koha::Patron::Attributes->search({
-        borrowernumber => $self->borrowernumber,
-        branchcode     => $self->branchcode,
-    });
+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,
+                            '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 lock
@@ -1417,6 +1700,216 @@ sub _anonymize_column {
     $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',
+        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