Bug 21909: Make Koha::Account::outstanding_* preserve context
[srvgit] / Koha / Account.pm
index 7041cb2..304d0e2 100644 (file)
@@ -21,12 +21,16 @@ use Modern::Perl;
 
 use Carp;
 use Data::Dumper;
+use List::MoreUtils qw( uniq );
 
+use C4::Circulation qw( ReturnLostItem );
+use C4::Letters;
 use C4::Log qw( logaction );
 use C4::Stats qw( UpdateStats );
 
-use Koha::Account::Line;
+use Koha::Patrons;
 use Koha::Account::Lines;
+use Koha::Account::Offsets;
 use Koha::DateUtils qw( dt_from_string );
 
 =head1 NAME
@@ -49,11 +53,14 @@ This method allows payments to be made against fees/fines
 
 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
     {
-        amount     => $amount,
-        sip        => $sipmode,
-        note       => $note,
-        accountlines_id => $accountlines_id,
-        library_id => $branchcode,
+        amount      => $amount,
+        sip         => $sipmode,
+        note        => $note,
+        description => $description,
+        library_id  => $branchcode,
+        lines        => $lines, # Arrayref of Koha::Account::Line objects to pay
+        account_type => $type,  # accounttype code
+        offset_type => $offset_type,    # offset type code
     }
 );
 
@@ -62,23 +69,25 @@ Koha::Account->new( { patron_id => $borrowernumber } )->pay(
 sub pay {
     my ( $self, $params ) = @_;
 
-    my $amount          = $params->{amount};
-    my $sip             = $params->{sip};
-    my $note            = $params->{note} || q{};
-    my $accountlines_id = $params->{accountlines_id};
-    my $library_id      = $params->{library_id};
+    my $amount       = $params->{amount};
+    my $sip          = $params->{sip};
+    my $description  = $params->{description};
+    my $note         = $params->{note} || q{};
+    my $library_id   = $params->{library_id};
+    my $lines        = $params->{lines};
+    my $type         = $params->{type} || 'payment';
+    my $payment_type = $params->{payment_type} || undef;
+    my $account_type = $params->{account_type};
+    my $offset_type  = $params->{offset_type} || $type eq 'writeoff' ? 'Writeoff' : 'Payment';
 
     my $userenv = C4::Context->userenv;
 
+    my $patron = Koha::Patrons->find( $self->{patron_id} );
+
     # We should remove accountno, it is no longer needed
-    my $last = Koha::Account::Lines->search(
-        {
-            borrowernumber => $self->{patron_id}
-        },
-        {
-            order_by => 'accountno'
-        }
-    )->next();
+    my $last = $self->lines->search(
+        {},
+        { order_by => 'accountno' } )->next();
     my $accountno = $last ? $last->accountno + 1 : 1;
 
     my $manager_id = $userenv ? $userenv->{number} : 0;
@@ -88,23 +97,34 @@ sub pay {
     my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
     $balance_remaining ||= 0;
 
-    # We were passed a specific line to pay
-    if ( $accountlines_id ) {
-        my $fine = Koha::Account::Lines->find( $accountlines_id );
+    my @account_offsets;
 
-        # If accountline id is passed but no amount, we pay that line in full
-        $amount = $fine->amountoutstanding unless defined($amount);
+    # We were passed a specific line to pay
+    foreach my $fine ( @$lines ) {
+        my $amount_to_pay =
+            $fine->amountoutstanding > $balance_remaining
+          ? $balance_remaining
+          : $fine->amountoutstanding;
 
         my $old_amountoutstanding = $fine->amountoutstanding;
-        my $new_amountoutstanding = $old_amountoutstanding - $amount;
-        $fine->amountoutstanding( $new_amountoutstanding )->store();
-        $balance_remaining = $balance_remaining - $amount;
+        my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
+        $fine->amountoutstanding($new_amountoutstanding)->store();
+        $balance_remaining = $balance_remaining - $amount_to_pay;
 
-        if ( $fine->accounttype eq 'Rep' || $fine->accounttype eq 'L' )
+        if ( $fine->itemnumber && $fine->accounttype && ( $fine->accounttype eq 'Rep' || $fine->accounttype eq 'L' ) )
         {
             C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
         }
 
+        my $account_offset = Koha::Account::Offset->new(
+            {
+                debit_id => $fine->id,
+                type     => $offset_type,
+                amount   => $amount_to_pay * -1,
+            }
+        );
+        push( @account_offsets, $account_offset );
+
         if ( C4::Context->preference("FinesLog") ) {
             logaction(
                 "FINES", 'MODIFY',
@@ -131,9 +151,8 @@ sub pay {
     # than the what was owed on the given line. In that case pay down other
     # lines with remaining balance.
     my @outstanding_fines;
-    @outstanding_fines = Koha::Account::Lines->search(
+    @outstanding_fines = $self->lines->search(
         {
-            borrowernumber    => $self->{patron_id},
             amountoutstanding => { '>' => 0 },
         }
     ) if $balance_remaining > 0;
@@ -148,13 +167,22 @@ sub pay {
         $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
         $fine->store();
 
+        my $account_offset = Koha::Account::Offset->new(
+            {
+                debit_id => $fine->id,
+                type     => $offset_type,
+                amount   => $amount_to_pay * -1,
+            }
+        );
+        push( @account_offsets, $account_offset );
+
         if ( C4::Context->preference("FinesLog") ) {
             logaction(
                 "FINES", 'MODIFY',
                 $self->{patron_id},
                 Dumper(
                     {
-                        action                => 'fee_payment',
+                        action                => "fee_$type",
                         borrowernumber        => $fine->borrowernumber,
                         old_amountoutstanding => $old_amountoutstanding,
                         new_amountoutstanding => $fine->amountoutstanding,
@@ -173,7 +201,12 @@ sub pay {
         last unless $balance_remaining > 0;
     }
 
-    my $account_type = defined($sip) ? "Pay$sip" : 'Pay';
+    $account_type ||=
+        $type eq 'writeoff' ? 'W'
+      : defined($sip)       ? "Pay$sip"
+      :                       'Pay';
+
+    $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
 
     my $payment = Koha::Account::Line->new(
         {
@@ -181,20 +214,26 @@ sub pay {
             accountno         => $accountno,
             date              => dt_from_string(),
             amount            => 0 - $amount,
-            description       => q{},
+            description       => $description,
             accounttype       => $account_type,
+            payment_type      => $payment_type,
             amountoutstanding => 0 - $balance_remaining,
             manager_id        => $manager_id,
             note              => $note,
         }
     )->store();
 
+    foreach my $o ( @account_offsets ) {
+        $o->credit_id( $payment->id() );
+        $o->store();
+    }
+
     $library_id ||= $userenv ? $userenv->{'branch'} : undef;
 
     UpdateStats(
         {
             branch         => $library_id,
-            type           => 'payment',
+            type           => $type,
             amount         => $amount,
             borrowernumber => $self->{patron_id},
             accountno      => $accountno,
@@ -207,12 +246,12 @@ sub pay {
             $self->{patron_id},
             Dumper(
                 {
-                    action            => 'create_payment',
+                    action            => "create_$type",
                     borrowernumber    => $self->{patron_id},
                     accountno         => $accountno,
                     amount            => 0 - $amount,
                     amountoutstanding => 0 - $balance_remaining,
-                    accounttype       => 'Pay',
+                    accounttype       => $account_type,
                     accountlines_paid => \@fines_paid,
                     manager_id        => $manager_id,
                 }
@@ -220,9 +259,153 @@ sub pay {
         );
     }
 
+    if ( C4::Context->preference('UseEmailReceipts') ) {
+        if (
+            my $letter = C4::Letters::GetPreparedLetter(
+                module                 => 'circulation',
+                letter_code            => uc("ACCOUNT_$type"),
+                message_transport_type => 'email',
+                lang    => $patron->lang,
+                tables => {
+                    borrowers       => $self->{patron_id},
+                    branches        => $self->{library_id},
+                },
+                substitute => {
+                    credit => $payment,
+                    offsets => \@account_offsets,
+                },
+              )
+          )
+        {
+            C4::Letters::EnqueueLetter(
+                {
+                    letter                 => $letter,
+                    borrowernumber         => $self->{patron_id},
+                    message_transport_type => 'email',
+                }
+            ) or warn "can't enqueue letter $letter";
+        }
+    }
+
     return $payment->id;
 }
 
+=head3 add_credit
+
+This method allows adding credits to a patron's account
+
+my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
+    {
+        amount       => $amount,
+        description  => $description,
+        note         => $note,
+        user_id      => $user_id,
+        library_id   => $library_id,
+        sip          => $sip,
+        payment_type => $payment_type,
+        type         => $credit_type,
+        item_id      => $item_id
+    }
+);
+
+$credit_type can be any of:
+  - 'credit'
+  - 'payment'
+  - 'forgiven'
+  - 'lost_item_return'
+  - 'writeoff'
+
+=cut
+
+sub add_credit {
+
+    my ( $self, $params ) = @_;
+
+    # amount is passed as a positive value, but we store credit as negative values
+    my $amount       = $params->{amount} * -1;
+    my $description  = $params->{description} // q{};
+    my $note         = $params->{note} // q{};
+    my $user_id      = $params->{user_id};
+    my $library_id   = $params->{library_id};
+    my $sip          = $params->{sip};
+    my $payment_type = $params->{payment_type};
+    my $type         = $params->{type} || 'payment';
+    my $item_id      = $params->{item_id};
+
+    my $schema = Koha::Database->new->schema;
+
+    my $account_type = $Koha::Account::account_type->{$type};
+    $account_type .= $sip
+        if defined $sip &&
+           $type eq 'payment';
+
+    my $line;
+
+    $schema->txn_do(
+        sub {
+            # We should remove accountno, it is no longer needed
+            my $last = $self->lines->search(
+                {},
+                { order_by => 'accountno' } )->next();
+            my $accountno = $last ? $last->accountno + 1 : 1;
+
+            # Insert the account line
+            $line = Koha::Account::Line->new(
+                {   borrowernumber    => $self->{patron_id},
+                    date              => \'NOW()',
+                    amount            => $amount,
+                    description       => $description,
+                    accounttype       => $account_type,
+                    amountoutstanding => $amount,
+                    payment_type      => $payment_type,
+                    note              => $note,
+                    manager_id        => $user_id,
+                    itemnumber        => $item_id
+                }
+            )->store();
+
+            # Record the account offset
+            my $account_offset = Koha::Account::Offset->new(
+                {   credit_id => $line->id,
+                    type      => $Koha::Account::offset_type->{$type},
+                    amount    => $amount
+                }
+            )->store();
+
+            UpdateStats(
+                {   branch         => $library_id,
+                    type           => $type,
+                    amount         => $amount,
+                    borrowernumber => $self->{patron_id},
+                    accountno      => $accountno,
+                }
+            ) if grep { $type eq $_ } ('payment', 'writeoff') ;
+
+            if ( C4::Context->preference("FinesLog") ) {
+                logaction(
+                    "FINES", 'CREATE',
+                    $self->{patron_id},
+                    Dumper(
+                        {   action            => "create_$type",
+                            borrowernumber    => $self->{patron_id},
+                            accountno         => $accountno,
+                            amount            => $amount,
+                            description       => $description,
+                            amountoutstanding => $amount,
+                            accounttype       => $account_type,
+                            note              => $note,
+                            itemnumber        => $item_id,
+                            manager_id        => $user_id,
+                        }
+                    )
+                );
+            }
+        }
+    );
+
+    return $line;
+}
+
 =head3 balance
 
 my $balance = $self->balance
@@ -233,22 +416,160 @@ Return the balance (sum of amountoutstanding columns)
 
 sub balance {
     my ($self) = @_;
-    my $fines = Koha::Account::Lines->search(
+    return $self->lines->total_outstanding;
+}
+
+=head3 outstanding_debits
+
+my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
+
+=cut
+
+sub outstanding_debits {
+    my ($self) = @_;
+
+    return $self->lines->search(
         {
-            borrowernumber => $self->{patron_id},
+            amount            => { '>' => 0 },
+            amountoutstanding => { '>' => 0 }
+        }
+    );
+}
+
+=head3 outstanding_credits
+
+my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
+
+=cut
+
+sub outstanding_credits {
+    my ($self) = @_;
+
+    return $self->lines->search(
+        {
+            amount            => { '<' => 0 },
+            amountoutstanding => { '<' => 0 }
+        }
+    );
+}
+
+=head3 non_issues_charges
+
+my $non_issues_charges = $self->non_issues_charges
+
+Calculates amount immediately owing by the patron - non-issue charges.
+
+Charges exempt from non-issue are:
+* Res (holds) if HoldsInNoissuesCharge syspref is set to false
+* Rent (rental) if RentalsInNoissuesCharge syspref is set to false
+* Manual invoices if ManInvInNoissuesCharge syspref is set to false
+
+=cut
+
+sub non_issues_charges {
+    my ($self) = @_;
+
+    # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
+    my $ACCOUNT_TYPE_LENGTH = 5;    # this is plain ridiculous...
+
+    my @not_fines;
+    push @not_fines, 'Res'
+      unless C4::Context->preference('HoldsInNoissuesCharge');
+    push @not_fines, 'Rent'
+      unless C4::Context->preference('RentalsInNoissuesCharge');
+    unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
+        my $dbh = C4::Context->dbh;
+        push @not_fines,
+          @{
+            $dbh->selectcol_arrayref(q|
+                SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
+            |)
+          };
+    }
+    @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
+
+    return $self->lines->search(
+        {
+            accounttype    => { -not_in => \@not_fines }
         },
+    )->total_outstanding;
+}
+
+=head3 lines
+
+my $lines = $self->lines;
+
+Return all credits and debits for the user, outstanding or otherwise
+
+=cut
+
+sub lines {
+    my ($self) = @_;
+
+    return Koha::Account::Lines->search(
         {
-            select => [ { sum => 'amountoutstanding' } ],
-            as => ['total_amountoutstanding'],
+            borrowernumber => $self->{patron_id},
         }
     );
-    return $fines->count
-      ? $fines->next->get_column('total_amountoutstanding')
-      : 0;
+}
+
+=head3 reconcile_balance
+
+$account->reconcile_balance();
+
+Find outstanding credits and use them to pay outstanding debits.
+Currently, this implicitly uses the 'First In First Out' rule for
+applying credits against debits.
+
+=cut
+
+sub reconcile_balance {
+    my ($self) = @_;
+
+    my $outstanding_debits  = $self->outstanding_debits;
+    my $outstanding_credits = $self->outstanding_credits;
+
+    while (     $outstanding_debits->total_outstanding > 0
+            and my $credit = $outstanding_credits->next )
+    {
+        # there's both outstanding debits and credits
+        $credit->apply( { debits => $outstanding_debits } );    # applying credit, no special offset
+
+        $outstanding_debits = $self->outstanding_debits;
+
+    }
+
+    return $self;
 }
 
 1;
 
+=head2 Name mappings
+
+=head3 $offset_type
+
+=cut
+
+our $offset_type = {
+    'credit'           => 'Manual Credit',
+    'forgiven'         => 'Writeoff',
+    'lost_item_return' => 'Lost Item',
+    'payment'          => 'Payment',
+    'writeoff'         => 'Writeoff'
+};
+
+=head3 $account_type
+
+=cut
+
+our $account_type = {
+    'credit'           => 'C',
+    'forgiven'         => 'FOR',
+    'lost_item_return' => 'CR',
+    'payment'          => 'Pay',
+    'writeoff'         => 'W'
+};
+
 =head1 AUTHOR
 
 Kyle M Hall <kyle.m.hall@gmail.com>