Bug 22435: Fix _set_found_trigger
[koha-ffzg.git] / Koha / Account / Line.pm
index 5272b34..a05ab65 100644 (file)
@@ -17,16 +17,16 @@ package Koha::Account::Line;
 
 use Modern::Perl;
 
-use Carp;
-use Data::Dumper;
+use Data::Dumper qw( Dumper );
 
-use C4::Log qw(logaction);
-use C4::Overdues qw(GetFine);
+use C4::Log qw( logaction );
+use C4::Overdues qw( UpdateFine );
 
 use Koha::Account::CreditType;
 use Koha::Account::DebitType;
 use Koha::Account::Offsets;
 use Koha::Database;
+use Koha::DateUtils qw( dt_from_string );
 use Koha::Exceptions::Account;
 use Koha::Items;
 
@@ -131,8 +131,8 @@ Return the credit_offsets linked to this account line if some exist
 =cut
 
 sub credit_offsets {
-    my ( $self ) = @_;
-    my $rs = $self->_result->account_offsets_credits;
+    my ( $self, $cond, $attr ) = @_;
+    my $rs = $self->_result->search_related( 'account_offsets_credits', $cond, $attr);
     return unless $rs;
     return Koha::Account::Offsets->_new_from_dbic($rs);
 }
@@ -144,8 +144,8 @@ Return the debit_offsets linked to this account line if some exist
 =cut
 
 sub debit_offsets {
-    my ( $self ) = @_;
-    my $rs = $self->_result->account_offsets_debits;
+    my ( $self, $cond, $attr ) = @_;
+    my $rs = $self->_result->search_related( 'account_offsets_debits', $cond, $attr);
     return unless $rs;
     return Koha::Account::Offsets->_new_from_dbic($rs);
 }
@@ -166,14 +166,15 @@ sub credits {
     my ( $self, $cond, $attr ) = @_;
 
     unless ( $self->is_debit ) {
-        Koha::Exceptions::Account::IsNotCredit->throw(
+        Koha::Exceptions::Account::IsNotDebit->throw(
             error => 'Account line ' . $self->id . ' is not a debit'
         );
     }
 
+    my $cond_m = { map { "credit.".$_ => $cond->{$_} } keys %{$cond}};
     my $rs =
       $self->_result->search_related('account_offsets_debits')
-      ->search_related( 'credit', $cond, $attr );
+      ->search_related( 'credit', $cond_m, $attr );
     return unless $rs;
     return Koha::Account::Lines->_new_from_dbic($rs);
 }
@@ -198,16 +199,20 @@ sub debits {
         );
     }
 
+    my $cond_m = { map { "debit.".$_ => $cond->{$_} } keys %{$cond}};
     my $rs =
       $self->_result->search_related('account_offsets_credits')
-      ->search_related( 'debit', $cond, $attr );
+      ->search_related( 'debit', $cond_m, $attr );
     return unless $rs;
     return Koha::Account::Lines->_new_from_dbic($rs);
 }
 
 =head3 void
 
-  $payment_accountline->void();
+  $payment_accountline->void({
+      interface => $interface,
+      [ staff_id => $staff_id, branch => $branchcode ]
+  });
 
 Used to 'void' (or reverse) a payment/credit. It will roll back any offsets
 created by the application of this credit upon any debits and mark the credit
@@ -216,17 +221,81 @@ as 'void' by updating it's status to "VOID".
 =cut
 
 sub void {
-    my ($self) = @_;
+    my ($self, $params) = @_;
 
-    # Make sure it is a payment we are voiding
-    return unless $self->amount < 0;
+    # Make sure it is a credit we are voiding
+    unless ( $self->is_credit ) {
+        Koha::Exceptions::Account::IsNotCredit->throw(
+            error => 'Account line ' . $self->id . 'is not a credit' );
+    }
 
+    # Make sure it is not already voided
+    if ( $self->status && $self->status eq 'VOID' ) {
+        Koha::Exceptions::Account->throw(
+            error => 'Account line ' . $self->id . 'is already void' );
+    }
+
+    # Check for mandatory parameters
+    my @mandatory = ( 'interface' );
+    for my $param (@mandatory) {
+        unless ( defined( $params->{$param} ) ) {
+            Koha::Exceptions::MissingParameter->throw(
+                error => "The $param parameter is mandatory" );
+        }
+    }
+
+    # More mandatory parameters
+    if ( $params->{interface} eq 'intranet' ) {
+        my @optional = ( 'staff_id', 'branch' );
+        for my $param (@optional) {
+            unless ( defined( $params->{$param} ) ) {
+                Koha::Exceptions::MissingParameter->throw( error =>
+"The $param parameter is mandatory when interface is set to 'intranet'"
+                );
+            }
+        }
+    }
+
+    # Find any applied offsets for the credit so we may reverse them
     my @account_offsets =
       Koha::Account::Offsets->search(
         { credit_id => $self->id, amount => { '<' => 0 }  } );
 
+    my $void;
     $self->_result->result_source->schema->txn_do(
         sub {
+
+            # A 'void' is a 'debit'
+            $void = Koha::Account::Line->new(
+                {
+                    borrowernumber    => $self->borrowernumber,
+                    date              => \'NOW()',
+                    debit_type_code   => 'VOID',
+                    amount            => $self->amount * -1,
+                    amountoutstanding => $self->amount * -1,
+                    manager_id        => $params->{staff_id},
+                    interface         => $params->{interface},
+                    branchcode        => $params->{branch},
+                }
+            )->store();
+
+            # Record the creation offset
+            Koha::Account::Offset->new(
+                {
+                    debit_id => $void->id,
+                    type     => 'CREATE',
+                    amount   => $self->amount * -1
+                }
+            )->store();
+
+            # Link void to payment
+            $self->set({
+                amountoutstanding => $self->amount,
+                status => 'VOID'
+            })->store();
+            $self->apply( { debits => [$void] } );
+
+            # Reverse any applied payments
             foreach my $account_offset (@account_offsets) {
                 my $fee_paid =
                   Koha::Account::Lines->find( $account_offset->debit_id );
@@ -243,7 +312,7 @@ sub void {
                         credit_id => $self->id,
                         debit_id  => $fee_paid->id,
                         amount    => $amount_paid,
-                        type      => 'Void Payment',
+                        type      => 'VOID',
                     }
                 )->store();
             }
@@ -270,18 +339,98 @@ sub void {
                     )
                 );
             }
+        }
+    );
 
-            $self->set(
+    $void->discard_changes;
+    return $void;
+}
+
+=head3 cancel
+
+  $debit_accountline->cancel();
+
+Cancel a charge. It will mark the debit as 'cancelled' by updating its
+status to 'CANCELLED'.
+
+Charges that have been fully or partially paid cannot be cancelled.
+
+Returns the cancellation accountline.
+
+=cut
+
+sub cancel {
+    my ( $self, $params ) = @_;
+
+    # Make sure it is a charge we are reducing
+    unless ( $self->is_debit ) {
+        Koha::Exceptions::Account::IsNotDebit->throw(
+            error => 'Account line ' . $self->id . 'is not a debit' );
+    }
+    if ( $self->debit_type_code eq 'PAYOUT' ) {
+        Koha::Exceptions::Account::IsNotDebit->throw(
+            error => 'Account line ' . $self->id . 'is a payout' );
+    }
+
+    # Make sure it is not already cancelled
+    if ( $self->status && $self->status eq 'CANCELLED' ) {
+        Koha::Exceptions::Account->throw(
+            error => 'Account line ' . $self->id . 'is already cancelled' );
+    }
+
+    # Make sure it has not be paid yet
+    if ( $self->amount != $self->amountoutstanding ) {
+        Koha::Exceptions::Account->throw(
+            error => 'Account line ' . $self->id . 'is already offset' );
+    }
+
+    # Check for mandatory parameters
+    my @mandatory = ( 'staff_id', 'branch' );
+    for my $param (@mandatory) {
+        unless ( defined( $params->{$param} ) ) {
+            Koha::Exceptions::MissingParameter->throw(
+                error => "The $param parameter is mandatory" );
+        }
+    }
+
+    my $cancellation;
+    $self->_result->result_source->schema->txn_do(
+        sub {
+
+            # A 'cancellation' is a 'credit'
+            $cancellation = Koha::Account::Line->new(
+                {
+                    date              => \'NOW()',
+                    amount            => 0 - $self->amount,
+                    credit_type_code  => 'CANCELLATION',
+                    status            => 'ADDED',
+                    amountoutstanding => 0 - $self->amount,
+                    manager_id        => $params->{staff_id},
+                    borrowernumber    => $self->borrowernumber,
+                    interface         => 'intranet',
+                    branchcode        => $params->{branch},
+                }
+            )->store();
+
+            my $cancellation_offset = Koha::Account::Offset->new(
                 {
-                    status            => 'VOID',
-                    amountoutstanding => 0,
-                    amount            => 0,
+                    credit_id => $cancellation->accountlines_id,
+                    type      => 'CREATE',
+                    amount    => 0 - $self->amount
                 }
-            );
-            $self->store();
+            )->store();
+
+            # Link cancellation to charge
+            $cancellation->apply( { debits => [$self] } );
+            $cancellation->status('APPLIED')->store();
+
+            # Update status of original debit
+            $self->status('CANCELLED')->store;
         }
     );
 
+    $cancellation->discard_changes;
+    return $cancellation;
 }
 
 =head3 reduce
@@ -385,8 +534,8 @@ sub reduce {
             my $reduction_offset = Koha::Account::Offset->new(
                 {
                     credit_id => $reduction->accountlines_id,
-                    type      => uc( $params->{reduction_type} ),
-                    amount    => $params->{amount}
+                    type      => 'CREATE',
+                    amount    => 0 - $params->{amount}
                 }
             )->store();
 
@@ -394,22 +543,18 @@ sub reduce {
             my $debit_outstanding = $self->amountoutstanding;
             if ( $debit_outstanding >= $params->{amount} ) {
 
-                $reduction->apply(
-                    {
-                        debits      => [$self],
-                        offset_type => uc( $params->{reduction_type} )
-                    }
-                );
+                $reduction->apply( { debits => [$self] } );
                 $reduction->status('APPLIED')->store();
             }
             else {
 
-        # Zero amount offset used to link original 'debit' to reduction 'credit'
+                # Zero amount offset used to link original 'debit' to
+                # reduction 'credit'
                 my $link_reduction_offset = Koha::Account::Offset->new(
                     {
                         credit_id => $reduction->accountlines_id,
                         debit_id  => $self->accountlines_id,
-                        type      => uc( $params->{reduction_type} ),
+                        type      => 'APPLY',
                         amount    => 0
                     }
                 )->store();
@@ -427,7 +572,7 @@ sub reduce {
 =head3 apply
 
     my $debits = $account->outstanding_debits;
-    my $outstanding_amount = $credit->apply( { debits => $debits, [ offset_type => $offset_type ] } );
+    my $credit = $credit->apply( { debits => $debits } );
 
 Applies the credit to a given debits array reference.
 
@@ -437,9 +582,6 @@ Applies the credit to a given debits array reference.
 
 =item debits - Koha::Account::Lines object set of debits
 
-=item offset_type (optional) - a string indicating the offset type (valid values are those from
-the 'account_offset_types' table)
-
 =back
 
 =cut
@@ -448,7 +590,6 @@ sub apply {
     my ( $self, $params ) = @_;
 
     my $debits      = $params->{debits};
-    my $offset_type = $params->{offset_type} // 'Credit Applied';
 
     unless ( $self->is_credit ) {
         Koha::Exceptions::Account::IsNotCredit->throw(
@@ -489,7 +630,7 @@ sub apply {
                 {   credit_id => $self->id,
                     debit_id  => $debit->id,
                     amount    => $amount_to_cancel * -1,
-                    type      => $offset_type,
+                    type      => 'APPLY'
                 }
             )->store();
 
@@ -500,9 +641,17 @@ sub apply {
 
             # Attempt to renew the item associated with this debit if
             # appropriate
-            if ($debit->renewable) {
-                $debit->renew_item($params->{interface});
+            if ( $self->credit_type_code ne 'FORGIVEN' && $debit->is_renewable ) {
+                my $outcome = $debit->renew_item( { interface => $params->{interface} } );
+                $self->add_message(
+                    {
+                        type    => 'info',
+                        message => 'renewal',
+                        payload => $outcome
+                    }
+                ) if $outcome;
             }
+            $debit->discard_changes; # Refresh values from DB to clear floating point remainders
 
             # Same logic exists in Koha::Account::pay
             if (
@@ -521,10 +670,12 @@ sub apply {
                 C4::Circulation::ReturnLostItem( $self->borrowernumber,
                     $debit->itemnumber );
             }
+
+            last if $available_credit == 0;
         }
     });
 
-    return $available_credit;
+    return $self;
 }
 
 =head3 payout
@@ -607,12 +758,12 @@ sub payout {
             my $payout_offset = Koha::Account::Offset->new(
                 {
                     debit_id => $payout->accountlines_id,
-                    type     => 'PAYOUT',
+                    type     => 'CREATE',
                     amount   => $amount
                 }
             )->store();
 
-            $self->apply( { debits => [$payout], offset_type => 'PAYOUT' } );
+            $self->apply( { debits => [$payout] } );
             $self->status('PAID')->store;
         }
     );
@@ -694,8 +845,7 @@ sub adjust {
                 my $credit = $account->add_credit(
                     {
                         amount      => $new_outstanding * -1,
-                        description => 'Overpayment refund',
-                        type        => 'CREDIT',
+                        type        => 'OVERPAYMENT',
                         interface   => $interface,
                         ( $update_type eq 'overdue_update' ? ( item_id => $self->itemnumber ) : ()),
                     }
@@ -754,7 +904,7 @@ sub adjust {
 sub is_credit {
     my ($self) = @_;
 
-    return ( $self->amount < 0 );
+    return defined $self->credit_type_code;
 }
 
 =head3 is_debit
@@ -779,6 +929,7 @@ on the API.
 sub to_api_mapping {
     return {
         accountlines_id   => 'account_line_id',
+        credit_number     => undef,
         credit_type_code  => 'credit_type',
         debit_type_code   => 'debit_type',
         amountoutstanding => 'amount_outstanding',
@@ -788,17 +939,18 @@ sub to_api_mapping {
         itemnumber        => 'item_id',
         manager_id        => 'user_id',
         note              => 'internal_note',
+        register_id       => 'cash_register_id',
     };
 
 }
 
-=head3 renewable
+=head3 is_renewable
 
-    my $bool = $line->renewable;
+    my $bool = $line->is_renewable;
 
 =cut
 
-sub renewable {
+sub is_renewable {
     my ($self) = @_;
 
     return (
@@ -806,7 +958,9 @@ sub renewable {
         $self->debit_type_code &&
         $self->debit_type_code eq 'OVERDUE' &&
         $self->status &&
-        $self->status eq 'UNRETURNED'
+        $self->status eq 'UNRETURNED' &&
+        $self->item &&
+        $self->patron
     ) ? 1 : 0;
 }
 
@@ -815,7 +969,8 @@ sub renewable {
     my $renew_result = $line->renew_item;
 
 Conditionally attempt to renew an item and return the outcome. This is
-as a consequence of the fine on an item being fully paid off
+as a consequence of the fine on an item being fully paid off.
+Caller must call is_renewable before.
 
 =cut
 
@@ -824,19 +979,14 @@ sub renew_item {
 
     my $outcome = {};
 
-    # We want to reject the call to renew if any of these apply:
+    # We want to reject the call to renew if:
     # - The RenewAccruingItemWhenPaid syspref is off
-    # - The line item doesn't have an item attached to it
-    # - The line item doesn't have a patron attached to it
-    #
+    # OR
     # - The RenewAccruingItemInOpac syspref is off
-    # AND
     # - There is an interface param passed and it's value is 'opac'
 
     if (
         !C4::Context->preference('RenewAccruingItemWhenPaid') ||
-        !$self->item ||
-        !$self->patron ||
         (
             !C4::Context->preference('RenewAccruingItemInOpac') &&
             $params->{interface} &&
@@ -886,7 +1036,13 @@ sub store {
     my ($self) = @_;
 
     my $AutoCreditNumber = C4::Context->preference('AutoCreditNumber');
-    if ($AutoCreditNumber && !$self->in_storage && $self->is_credit && !$self->credit_number) {
+    my $credit_number_enabled = $self->is_credit && $self->credit_type->credit_number_enabled;
+
+    if ($AutoCreditNumber && $credit_number_enabled && !$self->in_storage) {
+        if (defined $self->credit_number) {
+            Koha::Exceptions::Account->throw('AutoCreditNumber is enabled but credit_number is already defined');
+        }
+
         my $rs = Koha::Database->new->schema->resultset($self->_type);
 
         if ($AutoCreditNumber eq 'incremental') {
@@ -899,7 +1055,7 @@ sub store {
             $max //= 0;
             $self->credit_number($max + 1);
         } elsif ($AutoCreditNumber eq 'annual') {
-            my $now = DateTime->now;
+            my $now = dt_from_string;
             my $prefix = sprintf('%d-', $now->year);
             my $max = $rs->search({
                 -and => [
@@ -914,7 +1070,7 @@ sub store {
             my $userenv = C4::Context->userenv;
             if ($userenv) {
                 my $branch = $userenv->{branch};
-                my $now = DateTime->now;
+                my $now = dt_from_string;
                 my $prefix = sprintf('%s%d%02d', $branch, $now->year, $now->month);
                 my $pattern = $prefix;
                 $pattern =~ s/([\?%_])/\\$1/g;