From 167643c79c924f72e9d968213b619b6f3deb4461 Mon Sep 17 00:00:00 2001 From: Julian Maurice Date: Thu, 6 Feb 2020 14:13:44 +0100 Subject: [PATCH] Bug 24603: Allow to cancel charges in patron accounting There is already a button to void a payment. It should be possible to cancel a charge too. This patch adds a button in patron's accounting section (Transactions tab) that allow to cancel charges. Charges that have been fully or partially paid cannot be cancelled. It also fixes Koha::Account::Line::is_credit by looking at credit_type_code instead of amount (amount can be 0 for voided payments) It also fixes the tests for Koha::Account::Line::void when database does not contain the borrowernumber 51 (the default in t::lib::Mocks::mock_userenv) Test plan: 1. Go to a patron's accounting section 2. Create a manual invoice 3. In Transactions tab, you should see a 'Cancel charge' button. Click on it. It should now be marked as cancelled 4. Create another manual invoice 5. Pay it (partially or fully) 6. Notice that the 'Cancel charge' button is not available 7. Void the payment 8. 'Cancel charge' button is available again. Click on it and verify that it still works 9. prove t/db_dependent/Koha/Account/Lines.t --- Koha/Account/Line.pm | 50 ++++++++++++- .../prog/en/includes/accounts.inc | 1 + .../prog/en/modules/members/boraccount.tt | 15 +++- members/boraccount.pl | 6 ++ members/cancel-charge.pl | 48 +++++++++++++ t/db_dependent/Koha/Account/Lines.t | 70 ++++++++++++++++++- 6 files changed, 186 insertions(+), 4 deletions(-) create mode 100755 members/cancel-charge.pl diff --git a/Koha/Account/Line.pm b/Koha/Account/Line.pm index 1fac85d10c..63c94282a8 100644 --- a/Koha/Account/Line.pm +++ b/Koha/Account/Line.pm @@ -270,6 +270,54 @@ sub 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. + +Return self in case of success, undef otherwise + +=cut + +sub cancel { + my ($self) = @_; + + # Make sure it is a charge we are cancelling + return unless $self->is_debit; + + # Make sure it is not already cancelled + return if $self->status && $self->status eq 'CANCELLED'; + + # Make sure it has not be paid yet + return if $self->amount != $self->amountoutstanding; + + if ( C4::Context->preference("FinesLog") ) { + logaction('FINES', 'CANCEL', $self->borrowernumber, Dumper({ + action => 'cancel_charge', + borrowernumber => $self->borrowernumber, + amount => $self->amount, + amountoutstanding => $self->amountoutstanding, + description => $self->description, + debit_type_code => $self->debit_type_code, + note => $self->note, + itemnumber => $self->itemnumber, + manager_id => $self->manager_id, + })); + } + + $self->set({ + status => 'CANCELLED', + amountoutstanding => 0, + amount => 0, + }); + $self->store(); + + return $self; +} + =head3 reduce $charge_accountline->reduce({ @@ -725,7 +773,7 @@ sub adjust { sub is_credit { my ($self) = @_; - return ( $self->amount < 0 ); + return defined $self->credit_type_code; } =head3 is_debit diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/accounts.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/accounts.inc index 162a641e57..bb554e63d2 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/accounts.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/accounts.inc @@ -55,6 +55,7 @@ [%- CASE 'FORGIVEN' -%] (Forgiven) [%- CASE 'VOID' -%] (Voided) [%- CASE 'LOST' -%] (Lost) + [%- CASE 'CANCELLED' -%] (Cancelled) [%- CASE -%] [%- END -%] [%- END -%] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/boraccount.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/boraccount.tt index c93eb6fa09..6b618e6b2c 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/boraccount.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/boraccount.tt @@ -81,8 +81,19 @@ Print [% END %] Details - [% IF account.is_credit %] - Void + [% IF account.is_credit && account.status != 'VOID' %] + Void payment + [% END %] + [% IF account.is_debit && account.amount == account.amountoutstanding && account.status != 'CANCELLED' %] +
+ + + + +
[% END %] [% IF CAN_user_updatecharges_payout && account.is_credit && ( account.amountoutstanding < 0 ) %] diff --git a/members/boraccount.pl b/members/boraccount.pl index 8dfe35d92e..de6c772e6b 100755 --- a/members/boraccount.pl +++ b/members/boraccount.pl @@ -32,6 +32,7 @@ use C4::Accounts; use Koha::Cash::Registers; use Koha::Patrons; use Koha::Patron::Categories; +use Koha::Token; my $input=new CGI; @@ -164,6 +165,10 @@ if($total <= 0){ $totalcredit = 1; } +my $csrf_token = Koha::Token->new->generate_csrf({ + session_id => scalar $input->cookie('CGISESSID'), +}); + $template->param( patron => $patron, finesview => 1, @@ -172,6 +177,7 @@ $template->param( accounts => \@accountlines, payment_id => $payment_id, change_given => $change_given, + csrf_token => $csrf_token, ); output_html_with_http_headers $input, $cookie, $template->output; diff --git a/members/cancel-charge.pl b/members/cancel-charge.pl new file mode 100755 index 0000000000..4aa0b00517 --- /dev/null +++ b/members/cancel-charge.pl @@ -0,0 +1,48 @@ +#!/usr/bin/perl + +# 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 CGI; + +use C4::Auth; +use Koha::Token; + +my $cgi = CGI->new; + +my $authnotrequired = 0; +my $flags = { updatecharges => 'remaining_permissions' }; +my $type = 'intranet'; +my ($user, $cookie) = C4::Auth::checkauth($cgi, $authnotrequired, $flags, $type); + +my $csrf_token_is_valid = Koha::Token->new->check_csrf( { + session_id => scalar $cgi->cookie('CGISESSID'), + token => scalar $cgi->param('csrf_token'), +}); +unless ($csrf_token_is_valid) { + print $cgi->header('text/plain', '403 Forbidden'); + print 'Wrong CSRF token'; + exit; +} + +my $borrowernumber = $cgi->param('borrowernumber'); +my $accountlines_id = $cgi->param('accountlines_id'); + +my $line = Koha::Account::Lines->find($accountlines_id); +$line->cancel(); + +print $cgi->redirect('/cgi-bin/koha/members/boraccount.pl?borrowernumber=' . $borrowernumber); diff --git a/t/db_dependent/Koha/Account/Lines.t b/t/db_dependent/Koha/Account/Lines.t index a2a12c16f0..bda11af013 100755 --- a/t/db_dependent/Koha/Account/Lines.t +++ b/t/db_dependent/Koha/Account/Lines.t @@ -19,7 +19,7 @@ use Modern::Perl; -use Test::More tests => 15; +use Test::More tests => 16; use Test::Exception; use C4::Circulation qw/AddIssue AddReturn/; @@ -824,6 +824,8 @@ subtest "void() tests" => sub { $borrower->branchcode( $branchcode ); $borrower->store; + t::lib::Mocks::mock_userenv({ branchcode => $branchcode, borrowernumber => $borrower->borrowernumber }); + my $account = Koha::Account->new({ patron_id => $borrower->id }); my $line1 = Koha::Account::Line->new( @@ -893,6 +895,72 @@ subtest "void() tests" => sub { $schema->storage->txn_rollback; }; +subtest "cancel() tests" => sub { + plan tests => 9; + + $schema->storage->txn_begin; + + # Create a borrower + my $categorycode = $builder->build({ source => 'Category' })->{ categorycode }; + my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode }; + + my $borrower = Koha::Patron->new( { + cardnumber => 'dariahall', + surname => 'Hall', + firstname => 'Daria', + } ); + $borrower->categorycode( $categorycode ); + $borrower->branchcode( $branchcode ); + $borrower->store; + + t::lib::Mocks::mock_userenv({ branchcode => $branchcode, borrowernumber => $borrower->borrowernumber }); + + my $account = Koha::Account->new({ patron_id => $borrower->id }); + + my $line1 = Koha::Account::Line->new( + { + borrowernumber => $borrower->borrowernumber, + amount => 10, + amountoutstanding => 10, + interface => 'commandline', + debit_type_code => 'OVERDUE', + } + )->store(); + my $line2 = Koha::Account::Line->new( + { + borrowernumber => $borrower->borrowernumber, + amount => 20, + amountoutstanding => 20, + interface => 'commandline', + debit_type_code => 'OVERDUE', + } + )->store(); + + my $id = $account->pay({ + lines => [$line2], + amount => 5, + }); + + is( $account->balance(), 25, "Account balance is 25" ); + is( $line1->amountoutstanding+0, 10, 'First fee has amount outstanding of 10' ); + is( $line2->amountoutstanding+0, 15, 'Second fee has amount outstanding of 15' ); + + my $ret = $line1->cancel(); + is( ref($ret), 'Koha::Account::Line', 'Cancel returns the account line' ); + is( $account->balance(), 15, "Account balance is 15" ); + is( $line1->amount+0, 0, 'First fee has amount of 0' ); + is( $line1->amountoutstanding+0, 0, 'First fee has amount outstanding of 0' ); + + $ret = $line2->cancel(); + is ($ret, undef, 'cancel returns undef when line cannot be cancelled'); + + my $account_payment = Koha::Account::Lines->find($id); + $ret = $account_payment->cancel(); + is ($ret, undef, 'payment cannot be cancelled'); + + $schema->storage->txn_rollback; +}; + subtest "payout() tests" => sub { plan tests => 18; -- 2.20.1