Bug 21336: Introduce administrative lockout
[koha-equinox.git] / Koha / Patron.pm
index aadb714..769324a 100644 (file)
@@ -21,11 +21,10 @@ package Koha::Patron;
 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 C4::Accounts;
 use C4::Context;
 use C4::Log;
 use Koha::AuthUtils;
@@ -35,6 +34,7 @@ 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;
@@ -85,8 +85,6 @@ Koha::Patron - Koha Patron Object class
 
 =head2 Class Methods
 
-=cut
-
 =head3 new
 
 =cut
@@ -192,29 +190,6 @@ sub store {
 
             $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;
-
-            if ( defined $self->updated_on and not $self->updated_on ) {
-                $self->updated_on(undef);
-            }
-
-            # Set default values if not set
-            $self->sms_provider_id(undef) unless $self->sms_provider_id;
-            $self->guarantorid(undef)     unless $self->guarantorid;
-
-            # 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;
-
             unless ( $self->in_storage ) {    #AddMember
 
                 # Generate a valid userid/login if needed
@@ -240,9 +215,6 @@ sub store {
                   :                                                   undef;
                 $self->privacy($default_privacy);
 
-                unless ( defined $self->privacy_guarantor_checkouts ) {
-                    $self->privacy_guarantor_checkouts(0);
-                }
 
                 # Make a copy of the plain text password for later use
                 $self->plain_text_password( $self->password );
@@ -263,11 +235,6 @@ sub store {
             }
             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
@@ -286,33 +253,50 @@ sub store {
                     $self->add_enrolment_fee_if_needed;
                 }
 
-                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 }
-                        )
-                    );
-                }
+                # 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/);
+                    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}
+                            };
+                        }
+                    }
 
-                logaction( "MEMBERS", "MODIFY", $self->borrowernumber,
-                    "UPDATE (executed w/ arg: " . $self->borrowernumber . ")" )
-                  if $borrowers_log;
+                    if ( defined($info) ) {
+                        logaction(
+                            "MEMBERS",
+                            "MODIFY",
+                            $self->borrowernumber,
+                            to_json(
+                                $info,
+                                { utf8 => 1, pretty => 1, canonical => 1 }
+                            )
+                        );
+                    }
+                }
 
+                # Final store
                 $self = $self->SUPER::store;
             }
         }
@@ -863,8 +847,15 @@ sub add_enrolment_fee_if_needed {
     my ($self) = @_;
     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 );
+        $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'
+            }
+        );
     }
     return $enrolment_fee || 0;
 }
@@ -893,7 +884,6 @@ It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
 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 {
@@ -1099,18 +1089,24 @@ sub get_enrollable_clubs {
 
 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
@@ -1123,6 +1119,7 @@ Return true if the patron (usually the logged in user) can see the patron's info
 
 sub can_see_patron_infos {
     my ( $self, $patron ) = @_;
+    return unless $patron;
     return $self->can_see_patrons_from( $patron->library->branchcode );
 }
 
@@ -1229,6 +1226,7 @@ my $is_child = $patron->is_child
 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;
@@ -1300,6 +1298,22 @@ sub generate_userid {
 
 }
 
+=head3 attributes
+
+my $attributes = $patron->attributes
+
+Return object of Koha::Patron::Attributes type with all attributes set for this patron
+
+=cut
+
+sub attributes {
+    my ( $self ) = @_;
+    return Koha::Patron::Attributes->search({
+        borrowernumber => $self->borrowernumber,
+        branchcode     => $self->branchcode,
+    });
+}
+
 =head2 Internal methods
 
 =head3 _type
@@ -1310,10 +1324,11 @@ sub _type {
     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