Bug 32336: (QA follow-up) Use $metadata->schema
[srvgit] / Koha / Account.pm
index 1912e68..e37e646 100644 (file)
@@ -20,15 +20,22 @@ package Koha::Account;
 use Modern::Perl;
 
 use Carp;
-use Data::Dumper;
-use List::MoreUtils qw( uniq );
+use Data::Dumper qw( Dumper );
+use Try::Tiny qw( catch try );
 
+use C4::Circulation qw( ReturnLostItem CanBookBeRenewed AddRenewal );
+use C4::Letters;
 use C4::Log qw( logaction );
 use C4::Stats qw( UpdateStats );
+use C4::Overdues qw(GetFine);
 
+use Koha::Patrons;
 use Koha::Account::Lines;
 use Koha::Account::Offsets;
-use Koha::DateUtils qw( dt_from_string );
+use Koha::Account::DebitTypes;
+use Koha::Exceptions;
+use Koha::Exceptions::Account;
+use Koha::Plugins;
 
 =head1 NAME
 
@@ -51,13 +58,12 @@ This method allows payments to be made against fees/fines
 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
     {
         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
+        lines       => $lines, # Arrayref of Koha::Account::Line objects to pay
+        credit_type => $type,  # credit_type_code code
+        item_id     => $itemnumber,     # pass the itemnumber if this is a credit pertianing to a specific item (i.e LOST_FOUND)
     }
 );
 
@@ -66,316 +72,573 @@ Koha::Account->new( { patron_id => $borrowernumber } )->pay(
 sub pay {
     my ( $self, $params ) = @_;
 
-    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 $amount        = $params->{amount};
+    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 $cash_register = $params->{cash_register};
+    my $item_id       = $params->{item_id};
 
     my $userenv = C4::Context->userenv;
 
-    # We should remove accountno, it is no longer needed
-    my $last = Koha::Account::Lines->search(
+    my $manager_id = $userenv ? $userenv->{number} : undef;
+    my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
+    my $payment = $self->payin_amount(
         {
-            borrowernumber => $self->{patron_id}
-        },
+            interface     => $interface,
+            type          => $type,
+            amount        => $amount,
+            payment_type  => $payment_type,
+            cash_register => $cash_register,
+            user_id       => $manager_id,
+            library_id    => $library_id,
+            item_id       => $item_id,
+            description   => $description,
+            note          => $note,
+            debits        => $lines
+        }
+    );
+
+    # NOTE: Pay historically always applied as much credit as it could to all
+    # existing outstanding debits, whether passed specific debits or otherwise.
+    if ( $payment->amountoutstanding ) {
+        $payment =
+          $payment->apply(
+            { debits => [ $self->outstanding_debits->as_list ] } );
+    }
+
+    my $patron = Koha::Patrons->find( $self->{patron_id} );
+    my @account_offsets = $payment->credit_offsets({ type => 'APPLY' })->as_list;
+    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        => $library_id,
+                },
+                substitute => {
+                    credit => $payment,
+                    offsets => \@account_offsets,
+                },
+              )
+          )
         {
-            order_by => 'accountno'
+            C4::Letters::EnqueueLetter(
+                {
+                    letter                 => $letter,
+                    borrowernumber         => $self->{patron_id},
+                    message_transport_type => 'email',
+                }
+            ) or warn "can't enqueue letter $letter";
         }
-    )->next();
-    my $accountno = $last ? $last->accountno + 1 : 1;
+    }
+
+    my $renew_outcomes = [];
+    for my $message ( @{$payment->object_messages} ) {
+        push @{$renew_outcomes}, $message->payload;
+    }
 
-    my $manager_id = $userenv ? $userenv->{number} : 0;
+    return { payment_id => $payment->id, renew_result => $renew_outcomes };
+}
 
-    my @fines_paid; # List of account lines paid on with this payment
+=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,
+        interface    => $interface,
+        library_id   => $library_id,
+        payment_type => $payment_type,
+        type         => $credit_type,
+        item_id      => $item_id
+    }
+);
 
-    my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
-    $balance_remaining ||= 0;
+$credit_type can be any of:
+  - 'CREDIT'
+  - 'PAYMENT'
+  - 'FORGIVEN'
+  - 'LOST_FOUND'
+  - 'OVERPAYMENT'
+  - 'PAYMENT'
+  - 'WRITEOFF'
+  - 'PROCESSING_FOUND'
 
-    my @account_offsets;
+=cut
 
-    # We were passed a specific line to pay
-    foreach my $fine ( @$lines ) {
-        my $amount_to_pay =
-            $fine->amountoutstanding > $balance_remaining
-          ? $balance_remaining
-          : $fine->amountoutstanding;
+sub add_credit {
 
-        my $old_amountoutstanding = $fine->amountoutstanding;
-        my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
-        $fine->amountoutstanding($new_amountoutstanding)->store();
-        $balance_remaining = $balance_remaining - $amount_to_pay;
+    my ( $self, $params ) = @_;
 
-        if ( $fine->itemnumber && $fine->accounttype && ( $fine->accounttype eq 'Rep' || $fine->accounttype eq 'L' ) )
-        {
-            C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
+    # check for mandatory params
+    my @mandatory = ( 'interface', 'amount' );
+    for my $param (@mandatory) {
+        unless ( defined( $params->{$param} ) ) {
+            Koha::Exceptions::MissingParameter->throw(
+                error => "The $param parameter is mandatory" );
         }
+    }
 
-        my $account_offset = Koha::Account::Offset->new(
-            {
-                debit_id => $fine->id,
-                type     => $offset_type,
-                amount   => $amount_to_pay * -1,
-            }
-        );
-        push( @account_offsets, $account_offset );
+    # amount should always be passed as a positive value
+    my $amount = $params->{amount} * -1;
+    unless ( $amount < 0 ) {
+        Koha::Exceptions::Account::AmountNotPositive->throw(
+            error => 'Debit amount passed is not positive' );
+    }
 
-        if ( C4::Context->preference("FinesLog") ) {
-            logaction(
-                "FINES", 'MODIFY',
-                $self->{patron_id},
-                Dumper(
+    my $description   = $params->{description} // q{};
+    my $note          = $params->{note} // q{};
+    my $user_id       = $params->{user_id};
+    my $interface     = $params->{interface};
+    my $library_id    = $params->{library_id};
+    my $cash_register = $params->{cash_register};
+    my $payment_type  = $params->{payment_type};
+    my $credit_type   = $params->{type} || 'PAYMENT';
+    my $item_id       = $params->{item_id};
+
+    Koha::Exceptions::Account::RegisterRequired->throw()
+      if ( C4::Context->preference("UseCashRegisters")
+        && defined($payment_type)
+        && ( $payment_type eq 'CASH' || $payment_type eq 'SIP00' )
+        && !defined($cash_register) );
+
+    my $line;
+    my $schema = Koha::Database->new->schema;
+    try {
+        $schema->txn_do(
+            sub {
+
+                # Insert the account line
+                $line = Koha::Account::Line->new(
                     {
-                        action                => 'fee_payment',
-                        borrowernumber        => $fine->borrowernumber,
-                        old_amountoutstanding => $old_amountoutstanding,
-                        new_amountoutstanding => 0,
-                        amount_paid           => $old_amountoutstanding,
-                        accountlines_id       => $fine->id,
-                        accountno             => $fine->accountno,
-                        manager_id            => $manager_id,
-                        note                  => $note,
+                        borrowernumber    => $self->{patron_id},
+                        date              => \'NOW()',
+                        amount            => $amount,
+                        description       => $description,
+                        credit_type_code  => $credit_type,
+                        amountoutstanding => $amount,
+                        payment_type      => $payment_type,
+                        note              => $note,
+                        manager_id        => $user_id,
+                        interface         => $interface,
+                        branchcode        => $library_id,
+                        register_id       => $cash_register,
+                        itemnumber        => $item_id,
                     }
-                )
-            );
-            push( @fines_paid, $fine->id );
-        }
+                )->store();
+
+                # Record the account offset
+                my $account_offset = Koha::Account::Offset->new(
+                    {
+                        credit_id => $line->id,
+                        type      => 'CREATE',
+                        amount    => $amount * -1
+                    }
+                )->store();
+
+                C4::Stats::UpdateStats(
+                    {
+                        branch         => $library_id,
+                        type           => lc($credit_type),
+                        amount         => $amount,
+                        borrowernumber => $self->{patron_id},
+                    }
+                ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
+
+                Koha::Plugins->call(
+                    'after_account_action',
+                    {
+                        action  => "add_credit",
+                        payload => {
+                            type => lc($credit_type),
+                            line => $line->get_from_storage, #TODO Seems unneeded
+                        }
+                    }
+                );
+
+                if ( C4::Context->preference("FinesLog") ) {
+                    logaction(
+                        "FINES", 'CREATE',
+                        $self->{patron_id},
+                        Dumper(
+                            {
+                                action            => "create_$credit_type",
+                                borrowernumber    => $self->{patron_id},
+                                amount            => $amount,
+                                description       => $description,
+                                amountoutstanding => $amount,
+                                credit_type_code  => $credit_type,
+                                note              => $note,
+                                itemnumber        => $item_id,
+                                manager_id        => $user_id,
+                                branchcode        => $library_id,
+                            }
+                        ),
+                        $interface
+                    );
+                }
+            }
+        );
     }
+    catch {
+        if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
+            if ( $_->broken_fk eq 'credit_type_code' ) {
+                Koha::Exceptions::Account::UnrecognisedType->throw(
+                    error => 'Type of credit not recognised' );
+            }
+            else {
+                $_->rethrow;
+            }
+        }
+    };
 
-    # Were not passed a specific line to pay, or the payment was for more
-    # 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(
+    return $line;
+}
+
+=head3 payin_amount
+
+    my $credit = $account->payin_amount(
         {
-            borrowernumber    => $self->{patron_id},
-            amountoutstanding => { '>' => 0 },
+            amount          => $amount,
+            type            => $credit_type,
+            payment_type    => $payment_type,
+            cash_register   => $register_id,
+            interface       => $interface,
+            library_id      => $branchcode,
+            user_id         => $staff_id,
+            debits          => $debit_lines,
+            description     => $description,
+            note            => $note
         }
-    ) if $balance_remaining > 0;
+    );
 
-    foreach my $fine (@outstanding_fines) {
-        my $amount_to_pay =
-            $fine->amountoutstanding > $balance_remaining
-          ? $balance_remaining
-          : $fine->amountoutstanding;
+This method allows an amount to be paid into a patrons account and immediately applied against debts.
 
-        my $old_amountoutstanding = $fine->amountoutstanding;
-        $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
-        $fine->store();
+You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
 
-        my $account_offset = Koha::Account::Offset->new(
-            {
-                debit_id => $fine->id,
-                type     => $offset_type,
-                amount   => $amount_to_pay * -1,
+$credit_type can be any of:
+  - 'PAYMENT'
+  - 'WRITEOFF'
+  - 'FORGIVEN'
+
+=cut
+
+sub payin_amount {
+    my ( $self, $params ) = @_;
+
+    # check for mandatory params
+    my @mandatory = ( 'interface', 'amount', 'type' );
+    for my $param (@mandatory) {
+        unless ( defined( $params->{$param} ) ) {
+            Koha::Exceptions::MissingParameter->throw(
+                error => "The $param parameter is mandatory" );
+        }
+    }
+
+    # Check for mandatory register
+    Koha::Exceptions::Account::RegisterRequired->throw()
+      if ( C4::Context->preference("UseCashRegisters")
+        && defined( $params->{payment_type} )
+        && ( $params->{payment_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
+        && !defined($params->{cash_register}) );
+
+    # amount should always be passed as a positive value
+    my $amount = $params->{amount};
+    unless ( $amount > 0 ) {
+        Koha::Exceptions::Account::AmountNotPositive->throw(
+            error => 'Payin amount passed is not positive' );
+    }
+
+    my $credit;
+    my $schema = Koha::Database->new->schema;
+    $schema->txn_do(
+        sub {
+
+            # Add payin credit
+            $credit = $self->add_credit($params);
+
+            # Offset debts passed first
+            if ( exists( $params->{debits} ) ) {
+                $credit = $credit->apply(
+                    {
+                        debits => $params->{debits}
+                    }
+                );
             }
-        );
-        push( @account_offsets, $account_offset );
 
-        if ( C4::Context->preference("FinesLog") ) {
-            logaction(
-                "FINES", 'MODIFY',
-                $self->{patron_id},
-                Dumper(
+            # Offset against remaining balance if AutoReconcile
+            if ( C4::Context->preference("AccountAutoReconcile")
+                && $credit->amountoutstanding != 0 )
+            {
+                $credit = $credit->apply(
                     {
-                        action                => "fee_$type",
-                        borrowernumber        => $fine->borrowernumber,
-                        old_amountoutstanding => $old_amountoutstanding,
-                        new_amountoutstanding => $fine->amountoutstanding,
-                        amount_paid           => $amount_to_pay,
-                        accountlines_id       => $fine->id,
-                        accountno             => $fine->accountno,
-                        manager_id            => $manager_id,
-                        note                  => $note,
+                        debits => [ $self->outstanding_debits->as_list ]
                     }
-                )
-            );
-            push( @fines_paid, $fine->id );
+                );
+            }
         }
+    );
 
-        $balance_remaining = $balance_remaining - $amount_to_pay;
-        last unless $balance_remaining > 0;
-    }
+    return $credit;
+}
 
-    $account_type ||=
-        $type eq 'writeoff' ? 'W'
-      : defined($sip)       ? "Pay$sip"
-      :                       'Pay';
+=head3 add_debit
 
-    $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
+This method allows adding debits to a patron's account
 
-    my $payment = Koha::Account::Line->new(
+    my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
         {
-            borrowernumber    => $self->{patron_id},
-            accountno         => $accountno,
-            date              => dt_from_string(),
-            amount            => 0 - $amount,
-            description       => $description,
-            accounttype       => $account_type,
-            payment_type      => $payment_type,
-            amountoutstanding => 0 - $balance_remaining,
-            manager_id        => $manager_id,
-            note              => $note,
+            amount           => $amount,
+            description      => $description,
+            note             => $note,
+            user_id          => $user_id,
+            interface        => $interface,
+            library_id       => $library_id,
+            type             => $debit_type,
+            transaction_type => $transaction_type,
+            cash_register    => $register_id,
+            item_id          => $item_id,
+            issue_id         => $issue_id
         }
-    )->store();
+    );
 
-    foreach my $o ( @account_offsets ) {
-        $o->credit_id( $payment->id() );
-        $o->store();
-    }
+$debit_type can be any of:
+  - ACCOUNT
+  - ACCOUNT_RENEW
+  - RESERVE_EXPIRED
+  - LOST
+  - sundry
+  - NEW_CARD
+  - OVERDUE
+  - PROCESSING
+  - RENT
+  - RENT_DAILY
+  - RENT_RENEW
+  - RENT_DAILY_RENEW
+  - RESERVE
+  - PAYOUT
 
-    $library_id ||= $userenv ? $userenv->{'branch'} : undef;
+=cut
 
-    UpdateStats(
-        {
-            branch         => $library_id,
-            type           => $type,
-            amount         => $amount,
-            borrowernumber => $self->{patron_id},
-            accountno      => $accountno,
+sub add_debit {
+
+    my ( $self, $params ) = @_;
+
+    # check for mandatory params
+    my @mandatory = ( 'interface', 'type', 'amount' );
+    for my $param (@mandatory) {
+        unless ( defined( $params->{$param} ) ) {
+            Koha::Exceptions::MissingParameter->throw(
+                error => "The $param parameter is mandatory" );
         }
-    );
+    }
 
-    if ( C4::Context->preference("FinesLog") ) {
-        logaction(
-            "FINES", 'CREATE',
-            $self->{patron_id},
-            Dumper(
-                {
-                    action            => "create_$type",
-                    borrowernumber    => $self->{patron_id},
-                    accountno         => $accountno,
-                    amount            => 0 - $amount,
-                    amountoutstanding => 0 - $balance_remaining,
-                    accounttype       => $account_type,
-                    accountlines_paid => \@fines_paid,
-                    manager_id        => $manager_id,
+    # check for cash register if using cash
+    Koha::Exceptions::Account::RegisterRequired->throw()
+      if ( C4::Context->preference("UseCashRegisters")
+        && defined( $params->{transaction_type} )
+        && ( $params->{transaction_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
+        && !defined( $params->{cash_register} ) );
+
+    # amount should always be a positive value
+    my $amount = $params->{amount};
+    unless ( $amount > 0 ) {
+        Koha::Exceptions::Account::AmountNotPositive->throw(
+            error => 'Debit amount passed is not positive' );
+    }
+
+    my $description      = $params->{description} // q{};
+    my $note             = $params->{note} // q{};
+    my $user_id          = $params->{user_id};
+    my $interface        = $params->{interface};
+    my $library_id       = $params->{library_id};
+    my $cash_register    = $params->{cash_register};
+    my $debit_type       = $params->{type};
+    my $transaction_type = $params->{transaction_type};
+    my $item_id          = $params->{item_id};
+    my $issue_id         = $params->{issue_id};
+
+    my $line;
+    my $schema = Koha::Database->new->schema;
+    try {
+        $schema->txn_do(
+            sub {
+
+                # Insert the account line
+                $line = Koha::Account::Line->new(
+                    {
+                        borrowernumber    => $self->{patron_id},
+                        date              => \'NOW()',
+                        amount            => $amount,
+                        description       => $description,
+                        debit_type_code   => $debit_type,
+                        amountoutstanding => $amount,
+                        payment_type      => $transaction_type,
+                        note              => $note,
+                        manager_id        => $user_id,
+                        interface         => $interface,
+                        itemnumber        => $item_id,
+                        issue_id          => $issue_id,
+                        branchcode        => $library_id,
+                        register_id       => $cash_register,
+                        (
+                            $debit_type eq 'OVERDUE'
+                            ? ( status => 'UNRETURNED' )
+                            : ()
+                        ),
+                    }
+                )->store();
+
+                # Record the account offset
+                my $account_offset = Koha::Account::Offset->new(
+                    {
+                        debit_id => $line->id,
+                        type     => 'CREATE',
+                        amount   => $amount
+                    }
+                )->store();
+
+                if ( C4::Context->preference("FinesLog") ) {
+                    logaction(
+                        "FINES", 'CREATE',
+                        $self->{patron_id},
+                        Dumper(
+                            {
+                                action            => "create_$debit_type",
+                                borrowernumber    => $self->{patron_id},
+                                amount            => $amount,
+                                description       => $description,
+                                amountoutstanding => $amount,
+                                debit_type_code   => $debit_type,
+                                note              => $note,
+                                itemnumber        => $item_id,
+                                manager_id        => $user_id,
+                            }
+                        ),
+                        $interface
+                    );
                 }
-            )
+            }
         );
     }
+    catch {
+        if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
+            if ( $_->broken_fk eq 'debit_type_code' ) {
+                Koha::Exceptions::Account::UnrecognisedType->throw(
+                    error => 'Type of debit not recognised' );
+            }
+            else {
+                $_->rethrow;
+            }
+        }
+    };
 
-    return $payment->id;
+    return $line;
 }
 
-=head3 add_credit
+=head3 payout_amount
 
-This method allows adding credits to a patron's account
+    my $debit = $account->payout_amount(
+        {
+            payout_type => $payout_type,
+            register_id => $register_id,
+            staff_id    => $staff_id,
+            interface   => 'intranet',
+            amount      => $amount,
+            credits     => $credit_lines
+        }
+    );
 
-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
-    }
-);
+This method allows an amount to be paid out from a patrons account against outstanding credits.
 
-$credit_type can be any of:
-  - 'credit'
-  - 'payment'
-  - 'forgiven'
-  - 'lost_item_return'
-  - 'writeoff'
+$payout_type can be any of the defined payment_types:
 
 =cut
 
-sub add_credit {
-
+sub payout_amount {
     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;
+    # Check for mandatory parameters
+    my @mandatory =
+      ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
+    for my $param (@mandatory) {
+        unless ( defined( $params->{$param} ) ) {
+            Koha::Exceptions::MissingParameter->throw(
+                error => "The $param parameter is mandatory" );
+        }
+    }
 
-    my $account_type = $Koha::Account::account_type->{$type};
-    $account_type .= $sip
-        if defined $sip &&
-           $type eq 'payment';
+    # Check for mandatory register
+    Koha::Exceptions::Account::RegisterRequired->throw()
+      if ( C4::Context->preference("UseCashRegisters")
+        && ( $params->{payout_type} eq 'CASH' || $params->{payout_type} eq 'SIP00' )
+        && !defined($params->{cash_register}) );
+
+    # Amount should always be passed as a positive value
+    my $amount = $params->{amount};
+    unless ( $amount > 0 ) {
+        Koha::Exceptions::Account::AmountNotPositive->throw(
+            error => 'Payout amount passed is not positive' );
+    }
 
-    my $line;
+    # Amount should always be less than or equal to outstanding credit
+    my $outstanding = 0;
+    my $outstanding_credits =
+      exists( $params->{credits} )
+      ? $params->{credits}
+      : $self->outstanding_credits->as_list;
+    for my $credit ( @{$outstanding_credits} ) {
+        $outstanding += $credit->amountoutstanding;
+    }
+    $outstanding = $outstanding * -1;
+    Koha::Exceptions::ParameterTooHigh->throw( error =>
+"Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
+    ) unless ( $outstanding >= $amount );
 
+    my $payout;
+    my $schema = Koha::Database->new->schema;
     $schema->txn_do(
         sub {
-            # We should remove accountno, it is no longer needed
-            my $last = Koha::Account::Lines->search( { borrowernumber => $self->{patron_id} },
-                { 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,
+            # A 'payout' is a 'debit'
+            $payout = $self->add_debit(
+                {
+                    amount            => $params->{amount},
+                    type              => 'PAYOUT',
+                    transaction_type  => $params->{payout_type},
+                    amountoutstanding => $params->{amount},
+                    user_id           => $params->{staff_id},
+                    interface         => $params->{interface},
+                    branchcode        => $params->{branch},
+                    cash_register     => $params->{cash_register}
                 }
-            ) 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,
-                        }
-                    )
-                );
+            );
+
+            # Offset against credits
+            for my $credit ( @{$outstanding_credits} ) {
+                $credit->apply( { debits => [$payout] } );
+                $payout->discard_changes;
+                last if $payout->amountoutstanding == 0;
             }
+
+            # Set payout as paid
+            $payout->status('PAID')->store;
         }
     );
 
-    return $line;
+    return $payout;
 }
 
 =head3 balance
@@ -388,70 +651,49 @@ Return the balance (sum of amountoutstanding columns)
 
 sub balance {
     my ($self) = @_;
-    my $fines = Koha::Account::Lines->search(
-        {
-            borrowernumber => $self->{patron_id},
-        },
-        {
-            select => [ { sum => 'amountoutstanding' } ],
-            as => ['total_amountoutstanding'],
-        }
-    );
-
-    return ( $fines->count )
-      ? $fines->next->get_column('total_amountoutstanding') + 0
-      : 0;
+    return $self->lines->total_outstanding;
 }
 
 =head3 outstanding_debits
 
 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
 
+It returns the debit lines with outstanding amounts for the patron.
+
+It returns a Koha::Account::Lines iterator.
+
 =cut
 
 sub outstanding_debits {
     my ($self) = @_;
 
-    my $lines = Koha::Account::Lines->search(
+    return $self->lines->search(
         {
-            borrowernumber    => $self->{patron_id},
+            amount            => { '>' => 0 },
             amountoutstanding => { '>' => 0 }
         }
     );
-
-    return $lines;
 }
 
 =head3 outstanding_credits
 
-my ( $total, $lines ) = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
+my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
+
+It returns the credit lines with outstanding amounts for the patron.
+
+It returns a Koha::Account::Lines iterator.
 
 =cut
 
 sub outstanding_credits {
     my ($self) = @_;
 
-    my $outstanding_credits = Koha::Account::Lines->search(
-        {   borrowernumber    => $self->{patron_id},
-            amountoutstanding => { '<' => 0 }
-        },
-        {   select => [ { sum => 'amountoutstanding' } ],
-            as     => ['outstanding_credits_total'],
-        }
-    );
-    my $total
-        = ( $outstanding_credits->count )
-        ? $outstanding_credits->next->get_column('outstanding_credits_total') + 0
-        : 0;
-
-    my $lines = Koha::Account::Lines->search(
+    return $self->lines->search(
         {
-            borrowernumber    => $self->{patron_id},
+            amount            => { '<' => 0 },
             amountoutstanding => { '<' => 0 }
         }
     );
-
-    return ( $total, $lines );
 }
 
 =head3 non_issues_charges
@@ -470,70 +712,80 @@ Charges exempt from non-issue are:
 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...
-
+    #NOTE: With bug 23049 these preferences could be moved to being attached
+    #to individual debit types to give more flexability and specificity.
     my @not_fines;
-    push @not_fines, 'Res'
+    push @not_fines, 'RESERVE'
       unless C4::Context->preference('HoldsInNoissuesCharge');
-    push @not_fines, 'Rent'
+    push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
       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'
-            |)
-          };
+        my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
+        push @not_fines, @man_inv;
     }
-    @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
 
-    my $non_issues_charges = Koha::Account::Lines->search(
+    return $self->lines->search(
         {
-            borrowernumber => $self->{patron_id},
-            accounttype    => { -not_in => \@not_fines }
+            debit_type_code => { -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     => ['non_issues_charges'],
+            borrowernumber => $self->{patron_id},
         }
     );
-    return $non_issues_charges->count
-      ? $non_issues_charges->next->get_column('non_issues_charges') + 0
-      : 0;
 }
 
-1;
+=head3 reconcile_balance
 
-=head2 Name mappings
+$account->reconcile_balance();
 
-=head3 $offset_type
+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
 
-our $offset_type = {
-    'credit'           => 'Manual Credit',
-    'forgiven'         => 'Writeoff',
-    'lost_item_return' => 'Lost Item Return',
-    'payment'          => 'Payment',
-    'writeoff'         => 'Writeoff'
-};
+sub reconcile_balance {
+    my ($self) = @_;
+
+    my $outstanding_debits  = $self->outstanding_debits;
+    my $outstanding_credits = $self->outstanding_credits;
 
-=head3 $account_type
+    while (     $outstanding_debits->total_outstanding > 0
+            and my $credit = $outstanding_credits->next )
+    {
+        # there's both outstanding debits and credits
+        $credit->apply( { debits => [ $outstanding_debits->as_list ] } );    # applying credit, no special offset
 
-=cut
+        $outstanding_debits = $self->outstanding_debits;
+
+    }
+
+    return $self;
+}
+
+1;
 
-our $account_type = {
-    'credit'           => 'C',
-    'forgiven'         => 'FOR',
-    'lost_item_return' => 'CR',
-    'payment'          => 'Pay',
-    'writeoff'         => 'W'
-};
+=head1 AUTHORS
 
-=head1 AUTHOR
+=encoding utf8
 
 Kyle M Hall <kyle.m.hall@gmail.com>
+Tomás Cohen Arazi <tomascohen@gmail.com>
+Martin Renvoize <martin.renvoize@ptfs-europe.com>
 
 =cut