package Koha::Account; # Copyright 2016 ByWater Solutions # # This file is part of Koha. # # Koha is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # Koha is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Koha; if not, see . use Modern::Perl; use Carp; use Data::Dumper qw( Dumper ); use Try::Tiny qw( catch try ); use C4::Letters; use C4::Log qw( logaction ); use C4::Stats qw( UpdateStats ); use C4::Overdues qw(GetFine); use C4::Context; use Koha::Patrons; use Koha::Account::Credits; use Koha::Account::Debits; use Koha::Account::Lines; use Koha::Account::Offsets; use Koha::Account::DebitTypes; use Koha::Exceptions; use Koha::Exceptions::Account; use Koha::Plugins; =head1 NAME Koha::Accounts - Module for managing payments and fees for patrons =cut sub new { my ( $class, $params ) = @_; Carp::croak("No patron id passed in!") unless $params->{patron_id}; return bless( $params, $class ); } =head2 pay This method allows payments to be made against fees/fines Koha::Account->new( { patron_id => $borrowernumber } )->pay( { amount => $amount, note => $note, description => $description, library_id => $branchcode, 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) } ); =cut sub pay { my ( $self, $params ) = @_; 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; unless ( $type eq 'WRITEOFF' ) { Koha::Exceptions::Account::PaymentTypeRequired->throw() if ( C4::Context->preference("RequirePaymentType") && !defined($payment_type) ); my $av = Koha::AuthorisedValues->search_with_library_limits( { category => 'PAYMENT_TYPE', authorised_value => $payment_type } ); if ( !$av->count && C4::Context->preference("RequirePaymentType") ) { Koha::Exceptions::Account::InvalidPaymentType->throw( error => 'Invalid payment type' ); } } my $manager_id = $userenv ? $userenv->{number} : undef; my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface; my $payment = $self->payin_amount( { 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, }, ) ) { C4::Letters::EnqueueLetter( { letter => $letter, borrowernumber => $self->{patron_id}, message_transport_type => 'email', } ) or warn "can't enqueue letter $letter"; } } my $renew_outcomes = []; for my $message ( @{$payment->object_messages} ) { push @{$renew_outcomes}, $message->payload; } return { payment_id => $payment->id, renew_result => $renew_outcomes }; } =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, interface => $interface, issue_id => $checkout->id, item_id => $item_id, library_id => $library_id, note => $note, payment_type => $payment_type, type => $credit_type, user_id => $user_id, } ); $credit_type can be any of: - 'CREDIT' - 'PAYMENT' - 'FORGIVEN' - 'LOST_FOUND' - 'OVERPAYMENT' - 'PAYMENT' - 'WRITEOFF' - 'PROCESSING_FOUND' =cut sub add_credit { my ( $self, $params ) = @_; # 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" ); } } # 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' ); } 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}; my $issue_id = $params->{issue_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( { 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, issue_id => $issue_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}, interface => $interface, } ) 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' ); } } $_->rethrow; }; return $line; } =head3 payin_amount my $credit = $account->payin_amount( { 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 } ); This method allows an amount to be paid into a patrons account and immediately applied against debts. You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines. $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} } ); } # Offset against remaining balance if AutoReconcile if ( C4::Context->preference("AccountAutoReconcile") && $credit->amountoutstanding != 0 ) { $credit = $credit->apply( { debits => [ $self->outstanding_debits->as_list ] } ); } } ); return $credit; } =head3 add_debit This method allows adding debits to a patron's account my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit( { 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 } ); $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 =cut 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" ); } } # 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 $line; } =head3 payout_amount my $debit = $account->payout_amount( { payout_type => $payout_type, register_id => $register_id, staff_id => $staff_id, interface => 'intranet', amount => $amount, credits => $credit_lines } ); This method allows an amount to be paid out from a patrons account against outstanding credits. $payout_type can be any of the defined payment_types: =cut sub payout_amount { my ( $self, $params ) = @_; # 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" ); } } # 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' ); } # 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 { # 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} } ); # 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 $payout; } =head3 balance my $balance = $self->balance Return the balance (sum of amountoutstanding columns) =cut sub balance { my ($self) = @_; 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) = @_; return $self->lines->search( { amount => { '>' => 0 }, amountoutstanding => { '>' => 0 } } ); } =head3 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) = @_; 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 can be set as exempt from non-issue by editing the debit type in the Debit Types area of System Preferences. =cut sub non_issues_charges { my ($self) = @_; my @blocking_debit_types = Koha::Account::DebitTypes->search({ restricts_checkouts => 1 }, { columns => 'code' })->get_column('code'); return $self->lines->search( { debit_type_code => { -in => \@blocking_debit_types } }, )->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( { borrowernumber => $self->{patron_id}, } ); } =head3 credits my $credits = $self->credits; Return all credits for the user =cut sub credits { my ($self) = @_; return Koha::Account::Credits->search( { borrowernumber => $self->{patron_id} } ); } =head3 debits my $debits = $self->debits; Return all debits for the user =cut sub debits { my ($self) = @_; return Koha::Account::Debits->search( { borrowernumber => $self->{patron_id}, } ); } =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->as_list ] } ); # applying credit, no special offset $outstanding_debits = $self->outstanding_debits; } return $self; } 1; =head1 AUTHORS =encoding utf8 Kyle M Hall Tomás Cohen Arazi Martin Renvoize =cut