Bug 28424: Fix patron credits route (POST)
[srvgit] / Koha / Patron.pm
index 9948f8c..19a9215 100644 (file)
@@ -29,6 +29,7 @@ use C4::Context;
 use C4::Log;
 use Koha::Account;
 use Koha::ArticleRequests;
+use C4::Letters qw( GetPreparedLetter EnqueueLetter );
 use Koha::AuthUtils;
 use Koha::Checkouts;
 use Koha::Club::Enrollments;
@@ -39,9 +40,11 @@ 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::Plugins;
@@ -59,7 +62,6 @@ our $RESULTSET_PATRON_ID_MAPPING = {
     Aqbudget             => 'budget_owner_id',
     Aqbudgetborrower     => 'borrowernumber',
     ArticleRequest       => 'borrowernumber',
-    BorrowerAttribute    => 'borrowernumber',
     BorrowerDebarment    => 'borrowernumber',
     BorrowerFile         => 'borrowernumber',
     BorrowerModification => 'borrowernumber',
@@ -200,10 +202,6 @@ sub store {
             $self->surname( uc($self->surname) )
                 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
 
                 # Generate a valid userid/login if needed
@@ -359,6 +357,9 @@ other lists are kept.
 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
@@ -383,6 +384,10 @@ sub delete {
             # FIXME Could be $patron->get_lists
             $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
 
+            # 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");
@@ -426,6 +431,19 @@ sub library {
     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
@@ -468,6 +486,53 @@ sub guarantee_relationships {
     );
 }
 
+=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;
+        }
+
+        $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.
@@ -535,6 +600,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
@@ -546,6 +614,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;
@@ -553,10 +624,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();
@@ -629,6 +713,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
@@ -726,11 +817,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);
@@ -1088,7 +1179,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
@@ -1278,6 +1369,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;
@@ -1343,6 +1461,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
@@ -1441,8 +1572,14 @@ sub generate_userid {
 
 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
@@ -1463,15 +1600,26 @@ sub 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};
                 }
             }
         );
@@ -1694,6 +1842,78 @@ sub to_api_mapping {
     };
 }
 
+=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 $admin_email_address = $library->inbound_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   => $admin_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