View | Details | Raw Unified | Return to bug 24603
Collapse All | Expand All

(-)a/Koha/Account/Line.pm (-27 / +72 lines)
Lines 277-322 sub void { Link Here
277
277
278
Cancel a charge. It will mark the debit as 'cancelled' by updating its
278
Cancel a charge. It will mark the debit as 'cancelled' by updating its
279
status to 'CANCELLED'.
279
status to 'CANCELLED'.
280
280
Charges that have been fully or partially paid cannot be cancelled.
281
Charges that have been fully or partially paid cannot be cancelled.
281
282
282
Return self in case of success, undef otherwise
283
Returns the cancellation accountline.
283
284
284
=cut
285
=cut
285
286
286
sub cancel {
287
sub cancel {
287
    my ($self) = @_;
288
    my ( $self, $params ) = @_;
288
289
289
    # Make sure it is a charge we are cancelling
290
    # Make sure it is a charge we are reducing
290
    return unless $self->is_debit;
291
    unless ( $self->is_debit ) {
292
        Koha::Exceptions::Account::IsNotDebit->throw(
293
            error => 'Account line ' . $self->id . 'is not a debit' );
294
    }
295
    if ( $self->debit_type_code eq 'PAYOUT' ) {
296
        Koha::Exceptions::Account::IsNotDebit->throw(
297
            error => 'Account line ' . $self->id . 'is a payout' );
298
    }
291
299
292
    # Make sure it is not already cancelled
300
    # Make sure it is not already cancelled
293
    return if $self->status && $self->status eq 'CANCELLED';
301
    if ( $self->status && $self->status eq 'CANCELLED' ) {
302
        Koha::Exceptions::Account->throw(
303
            error => 'Account line ' . $self->id . 'is already cancelled' );
304
    }
294
305
295
    # Make sure it has not be paid yet
306
    # Make sure it has not be paid yet
296
    return if $self->amount != $self->amountoutstanding;
307
    if ( $self->amount != $self->amountoutstanding ) {
297
308
        Koha::Exceptions::Account->throw(
298
    if ( C4::Context->preference("FinesLog") ) {
309
            error => 'Account line ' . $self->id . 'is already offset' );
299
        logaction('FINES', 'CANCEL', $self->borrowernumber, Dumper({
300
            action => 'cancel_charge',
301
            borrowernumber => $self->borrowernumber,
302
            amount => $self->amount,
303
            amountoutstanding => $self->amountoutstanding,
304
            description => $self->description,
305
            debit_type_code => $self->debit_type_code,
306
            note => $self->note,
307
            itemnumber => $self->itemnumber,
308
            manager_id => $self->manager_id,
309
        }));
310
    }
310
    }
311
311
312
    $self->set({
312
    # Check for mandatory parameters
313
        status => 'CANCELLED',
313
    my @mandatory = ( 'staff_id', 'branch' );
314
        amountoutstanding => 0,
314
    for my $param (@mandatory) {
315
        amount => 0,
315
        unless ( defined( $params->{$param} ) ) {
316
    });
316
            Koha::Exceptions::MissingParameter->throw(
317
    $self->store();
317
                error => "The $param parameter is mandatory" );
318
        }
319
    }
318
320
319
    return $self;
321
    my $cancellation;
322
    $self->_result->result_source->schema->txn_do(
323
        sub {
324
325
            # A 'cancellation' is a 'credit'
326
            $cancellation = Koha::Account::Line->new(
327
                {
328
                    date              => \'NOW()',
329
                    amount            => 0 - $self->amount,
330
                    credit_type_code  => 'CANCELLATION',
331
                    status            => 'ADDED',
332
                    amountoutstanding => 0 - $self->amount,
333
                    manager_id        => $params->{staff_id},
334
                    borrowernumber    => $self->borrowernumber,
335
                    interface         => 'intranet',
336
                    branchcode        => $params->{branch},
337
                }
338
            )->store();
339
340
            my $cancellation_offset = Koha::Account::Offset->new(
341
                {
342
                    credit_id => $cancellation->accountlines_id,
343
                    type      => 'CANCELLATION',
344
                    amount    => $self->amount
345
                }
346
            )->store();
347
348
            # Link cancellation to charge
349
            $cancellation->apply(
350
                {
351
                    debits      => [$self],
352
                    offset_type => 'CANCELLATION'
353
                }
354
            );
355
            $cancellation->status('APPLIED')->store();
356
357
            # Update status of original debit
358
            $self->status('CANCELLED')->store;
359
        }
360
    );
361
362
    $cancellation->discard_changes;
363
    return $cancellation;
320
}
364
}
321
365
322
=head3 reduce
366
=head3 reduce
Lines 439-445 sub reduce { Link Here
439
            }
483
            }
440
            else {
484
            else {
441
485
442
        # Zero amount offset used to link original 'debit' to reduction 'credit'
486
                # Zero amount offset used to link original 'debit' to 
487
                # reduction 'credit'
443
                my $link_reduction_offset = Koha::Account::Offset->new(
488
                my $link_reduction_offset = Koha::Account::Offset->new(
444
                    {
489
                    {
445
                        credit_id => $reduction->accountlines_id,
490
                        credit_id => $reduction->accountlines_id,
(-)a/installer/data/mysql/account_credit_types.sql (-1 / +2 lines)
Lines 6-11 INSERT INTO account_credit_types ( code, description, can_be_added_manually, is_ Link Here
6
('DISCOUNT', 'A discount applied to a patrons fine', 0, 1),
6
('DISCOUNT', 'A discount applied to a patrons fine', 0, 1),
7
('REFUND', 'A refund applied to a patrons fine', 0, 1),
7
('REFUND', 'A refund applied to a patrons fine', 0, 1),
8
('LOST_FOUND', 'Lost item fee refund', 0, 1),
8
('LOST_FOUND', 'Lost item fee refund', 0, 1),
9
('PURCHASE', 'Purchase', 0, 1);
9
('PURCHASE', 'Purchase', 0, 1),
10
('CANCELLATION', 'Cancellation', 0, 1);
10
11
11
INSERT INTO authorised_values (category,authorised_value,lib) VALUES ('PAYMENT_TYPE','CASH','Cash');
12
INSERT INTO authorised_values (category,authorised_value,lib) VALUES ('PAYMENT_TYPE','CASH','Cash');
(-)a/installer/data/mysql/account_offset_types.sql (-1 / +2 lines)
Lines 21-24 INSERT INTO account_offset_types ( type ) VALUES Link Here
21
('Credit Applied'),
21
('Credit Applied'),
22
('PAYOUT'),
22
('PAYOUT'),
23
('DISCOUNT'),
23
('DISCOUNT'),
24
('REFUND');
24
('REFUND'),
25
('CANCELLATION');
(-)a/installer/data/mysql/atomicupdate/bug_24603.perl (+20 lines)
Line 0 Link Here
1
$DBversion = 'XXX';    # will be replaced by the RM
2
if ( CheckVersion($DBversion) ) {
3
4
    $dbh->do(
5
        qq{
6
            INSERT IGNORE INTO account_credit_types (code, description, can_be_added_manually, is_system)
7
            VALUES
8
              ('CANCELLATION', 'A cancellation applied to a patron charge', 0, 1)
9
        }
10
    );
11
12
    $dbh->do(
13
        qq{
14
        INSERT IGNORE INTO account_offset_types ( type ) VALUES ('CANCELLATION');
15
    }
16
    );
17
18
    # Always end with this (adjust the bug info)
19
    NewVersion( $DBversion, 24603, "Add CANCELLATION credit_type_code" );
20
}
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/members/boraccount.tt (-2 / +2 lines)
Lines 85-96 Link Here
85
        [% END %]
85
        [% END %]
86
        <a href="accountline-details.pl?accountlines_id=[% account.accountlines_id | uri %]" class="btn btn-default btn-xs"><i class="fa fa-list"></i> Details</a>
86
        <a href="accountline-details.pl?accountlines_id=[% account.accountlines_id | uri %]" class="btn btn-default btn-xs"><i class="fa fa-list"></i> Details</a>
87
        [% IF account.is_debit && account.amountoutstanding > 0 %]
87
        [% IF account.is_debit && account.amountoutstanding > 0 %]
88
            <a class="btn btn-default btn-xs" href="/cgi-bin/koha/members/paycollect.pl?borrowernumber=[% account.borrowernumber | html %]&pay_individual=1&debit_type_code=[% account.debit_type_code | html %]&amount=[% account.amount | html %]&amountoutstanding=[% account.amountoutstanding | html %]&description=[% account.description | html %]&itemnumber=[% account.itemnumber | html %]&accountlines_id=[% account.accountlines_id | html %]">Pay</a>
88
            <a class="btn btn-default btn-xs" href="/cgi-bin/koha/members/paycollect.pl?borrowernumber=[% account.borrowernumber | html %]&pay_individual=1&debit_type_code=[% account.debit_type_code | html %]&amount=[% account.amount | html %]&amountoutstanding=[% account.amountoutstanding | html %]&description=[% account.description | html %]&itemnumber=[% account.itemnumber | html %]&accountlines_id=[% account.accountlines_id | html %]"><i class="fa fa-money"></i> Pay</a>
89
        [% END %]
89
        [% END %]
90
        [% IF account.is_credit && account.status != 'VOID' %]
90
        [% IF account.is_credit && account.status != 'VOID' %]
91
          <a href="boraccount.pl?action=void&amp;accountlines_id=[% account.accountlines_id | uri %]&amp;borrowernumber=[% account.borrowernumber | uri %]" class="btn btn-default btn-xs void"><i class="fa fa-ban"></i> Void payment</a>
91
          <a href="boraccount.pl?action=void&amp;accountlines_id=[% account.accountlines_id | uri %]&amp;borrowernumber=[% account.borrowernumber | uri %]" class="btn btn-default btn-xs void"><i class="fa fa-ban"></i> Void payment</a>
92
        [% END %]
92
        [% END %]
93
        [% IF account.is_debit && account.amount == account.amountoutstanding && account.status != 'CANCELLED' %]
93
        [% IF account.is_debit && account.amount == account.amountoutstanding && account.status != 'CANCELLED' && !(account.debit_type_code == 'PAYOUT') %]
94
          <form method="post" action="/cgi-bin/koha/members/cancel-charge.pl">
94
          <form method="post" action="/cgi-bin/koha/members/cancel-charge.pl">
95
            <input type="hidden" name="csrf_token" value="[% csrf_token | html %]">
95
            <input type="hidden" name="csrf_token" value="[% csrf_token | html %]">
96
            <input type="hidden" name="borrowernumber" value="[% patron.borrowernumber | html %]">
96
            <input type="hidden" name="borrowernumber" value="[% patron.borrowernumber | html %]">
(-)a/members/cancel-charge.pl (-2 / +7 lines)
Lines 42-48 unless ($csrf_token_is_valid) { Link Here
42
my $borrowernumber = $cgi->param('borrowernumber');
42
my $borrowernumber = $cgi->param('borrowernumber');
43
my $accountlines_id = $cgi->param('accountlines_id');
43
my $accountlines_id = $cgi->param('accountlines_id');
44
44
45
my $line = Koha::Account::Lines->find($accountlines_id);
45
my $charge = Koha::Account::Lines->find($accountlines_id);
46
$line->cancel();
46
$charge->cancel(
47
    {
48
        branch   => C4::Context->userenv->{'branch'},
49
        staff_id => $user
50
    }
51
);
47
52
48
print $cgi->redirect('/cgi-bin/koha/members/boraccount.pl?borrowernumber=' . $borrowernumber);
53
print $cgi->redirect('/cgi-bin/koha/members/boraccount.pl?borrowernumber=' . $borrowernumber);
(-)a/t/db_dependent/Koha/Account/Line.t (-35 / +97 lines)
Lines 862-868 subtest "payout() tests" => sub { Link Here
862
862
863
subtest "reduce() tests" => sub {
863
subtest "reduce() tests" => sub {
864
864
865
    plan tests => 27;
865
    plan tests => 29;
866
866
867
    $schema->storage->txn_begin;
867
    $schema->storage->txn_begin;
868
868
Lines 922-928 subtest "reduce() tests" => sub { Link Here
922
922
923
    my $reduce_params = {
923
    my $reduce_params = {
924
        interface      => 'commandline',
924
        interface      => 'commandline',
925
        reduction_type => 'REFUND',
925
        reduction_type => 'DISCOUNT',
926
        amount         => 5,
926
        amount         => 5,
927
        staff_id       => $staff->borrowernumber,
927
        staff_id       => $staff->borrowernumber,
928
        branch         => $branchcode
928
        branch         => $branchcode
Lines 970-976 subtest "reduce() tests" => sub { Link Here
970
      '->reduce() cannot reduce more than original amount';
970
      '->reduce() cannot reduce more than original amount';
971
971
972
    # Partial Reduction
972
    # Partial Reduction
973
    # (Refund 5 on debt of 20)
973
    # (Discount 5 on debt of 20)
974
    my $reduction = $debit1->reduce($reduce_params);
974
    my $reduction = $debit1->reduce($reduce_params);
975
975
976
    is( ref($reduction), 'Koha::Account::Line',
976
    is( ref($reduction), 'Koha::Account::Line',
Lines 980-985 subtest "reduce() tests" => sub { Link Here
980
        0, "Reduce amountoutstanding is 0" );
980
        0, "Reduce amountoutstanding is 0" );
981
    is( $debit1->amountoutstanding() * 1,
981
    is( $debit1->amountoutstanding() * 1,
982
        15, "Debit amountoutstanding reduced by 5 to 15" );
982
        15, "Debit amountoutstanding reduced by 5 to 15" );
983
    is( $debit1->status(), 'DISCOUNTED', "Debit status updated to DISCOUNTED");
983
    is( $account->balance() * 1, -5,        "Account balance is -5" );
984
    is( $account->balance() * 1, -5,        "Account balance is -5" );
984
    is( $reduction->status(),    'APPLIED', "Reduction status is 'APPLIED'" );
985
    is( $reduction->status(),    'APPLIED', "Reduction status is 'APPLIED'" );
985
986
Lines 989-1005 subtest "reduce() tests" => sub { Link Here
989
    my $THE_offset = $offsets->next;
990
    my $THE_offset = $offsets->next;
990
    is( $THE_offset->amount * 1,
991
    is( $THE_offset->amount * 1,
991
        -5, 'Correct amount was applied against debit' );
992
        -5, 'Correct amount was applied against debit' );
992
    is( $THE_offset->type, 'REFUND', "Offset type set to 'REFUND'" );
993
    is( $THE_offset->type, 'DISCOUNT', "Offset type set to 'DISCOUNT'" );
993
994
994
    # Zero offset created when zero outstanding
995
    # Zero offset created when zero outstanding
995
    # (Refund another 5 on paid debt of 20)
996
    # (Refund another 5 on paid debt of 20)
996
    $credit1->apply( { debits => [$debit1] } );
997
    $credit1->apply( { debits => [$debit1] } );
997
    is( $debit1->amountoutstanding + 0,
998
    is( $debit1->amountoutstanding + 0,
998
        0, 'Debit1 amountoutstanding reduced to 0' );
999
        0, 'Debit1 amountoutstanding reduced to 0' );
1000
    $reduce_params->{reduction_type} = 'REFUND';
999
    $reduction = $debit1->reduce($reduce_params);
1001
    $reduction = $debit1->reduce($reduce_params);
1000
    is( $reduction->amount() * 1, -5, "Reduce amount is -5" );
1002
    is( $reduction->amount() * 1, -5, "Reduce amount is -5" );
1001
    is( $reduction->amountoutstanding() * 1,
1003
    is( $reduction->amountoutstanding() * 1,
1002
        -5, "Reduce amountoutstanding is -5" );
1004
        -5, "Reduce amountoutstanding is -5" );
1005
    is( $debit1->status(), 'REFUNDED', "Debit status updated to REFUNDED");
1003
1006
1004
    $offsets = Koha::Account::Offsets->search(
1007
    $offsets = Koha::Account::Offsets->search(
1005
        { credit_id => $reduction->id, debit_id => $debit1->id } );
1008
        { credit_id => $reduction->id, debit_id => $debit1->id } );
Lines 1038-1065 subtest "reduce() tests" => sub { Link Here
1038
};
1041
};
1039
1042
1040
subtest "cancel() tests" => sub {
1043
subtest "cancel() tests" => sub {
1041
    plan tests => 9;
1044
    plan tests => 16;
1042
1045
1043
    $schema->storage->txn_begin;
1046
    $schema->storage->txn_begin;
1044
1047
1045
    # Create a borrower
1048
    # Create a borrower
1046
    my $categorycode = $builder->build({ source => 'Category' })->{ categorycode };
1049
    my $categorycode =
1047
    my $branchcode   = $builder->build({ source => 'Branch' })->{ branchcode };
1050
      $builder->build( { source => 'Category' } )->{categorycode};
1051
    my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode};
1048
1052
1049
    my $borrower = Koha::Patron->new( {
1053
    my $borrower = Koha::Patron->new(
1050
        cardnumber => 'dariahall',
1054
        {
1051
        surname => 'Hall',
1055
            cardnumber => 'dariahall',
1052
        firstname => 'Daria',
1056
            surname    => 'Hall',
1053
    } );
1057
            firstname  => 'Daria',
1054
    $borrower->categorycode( $categorycode );
1058
        }
1055
    $borrower->branchcode( $branchcode );
1059
    );
1060
    $borrower->categorycode($categorycode);
1061
    $borrower->branchcode($branchcode);
1056
    $borrower->store;
1062
    $borrower->store;
1057
1063
1058
    t::lib::Mocks::mock_userenv({ branchcode => $branchcode, borrowernumber => $borrower->borrowernumber });
1064
    my $staff = Koha::Patron->new(
1065
        {
1066
            cardnumber => 'bobby',
1067
            surname    => 'Bloggs',
1068
            firstname  => 'Bobby',
1069
        }
1070
    );
1071
    $staff->categorycode($categorycode);
1072
    $staff->branchcode($branchcode);
1073
    $staff->store;
1074
1075
    t::lib::Mocks::mock_userenv(
1076
        {
1077
            branchcode     => $branchcode,
1078
            borrowernumber => $borrower->borrowernumber
1079
        }
1080
    );
1059
1081
1060
    my $account = Koha::Account->new({ patron_id => $borrower->id });
1082
    my $account = Koha::Account->new( { patron_id => $borrower->id } );
1061
1083
1062
    my $line1 = Koha::Account::Line->new(
1084
    my $debit1 = Koha::Account::Line->new(
1063
        {
1085
        {
1064
            borrowernumber    => $borrower->borrowernumber,
1086
            borrowernumber    => $borrower->borrowernumber,
1065
            amount            => 10,
1087
            amount            => 10,
Lines 1068-1074 subtest "cancel() tests" => sub { Link Here
1068
            debit_type_code   => 'OVERDUE',
1090
            debit_type_code   => 'OVERDUE',
1069
        }
1091
        }
1070
    )->store();
1092
    )->store();
1071
    my $line2 = Koha::Account::Line->new(
1093
    my $debit2 = Koha::Account::Line->new(
1072
        {
1094
        {
1073
            borrowernumber    => $borrower->borrowernumber,
1095
            borrowernumber    => $borrower->borrowernumber,
1074
            amount            => 20,
1096
            amount            => 20,
Lines 1078-1104 subtest "cancel() tests" => sub { Link Here
1078
        }
1100
        }
1079
    )->store();
1101
    )->store();
1080
1102
1081
    my $id = $account->pay({
1103
    my $ret = $account->pay(
1082
        lines  => [$line2],
1104
        {
1083
        amount => 5,
1105
            lines  => [$debit2],
1084
    });
1106
            amount => 5,
1107
        }
1108
    );
1109
    my $credit = Koha::Account::Lines->find({ accountlines_id => $ret->{payment_id} });
1085
1110
1086
    is( $account->balance(), 25, "Account balance is 25" );
1111
    is( $account->balance(), 25, "Account balance is 25" );
1087
    is( $line1->amountoutstanding+0, 10, 'First fee has amount outstanding of 10' );
1112
    is( $debit1->amountoutstanding + 0,
1088
    is( $line2->amountoutstanding+0, 15, 'Second fee has amount outstanding of 15' );
1113
        10, 'First fee has amount outstanding of 10' );
1114
    is( $debit2->amountoutstanding + 0,
1115
        15, 'Second fee has amount outstanding of 15' );
1116
    throws_ok {
1117
        $credit->cancel(
1118
            { staff_id => $staff->borrowernumber, branch => $branchcode } );
1119
    }
1120
    'Koha::Exceptions::Account::IsNotDebit',
1121
      '->cancel() can only be used with debits';
1089
1122
1090
    my $ret = $line1->cancel();
1123
    throws_ok {
1091
    is( ref($ret), 'Koha::Account::Line', 'Cancel returns the account line' );
1124
        $debit1->reduce( { staff_id => $staff->borrowernumber } );
1092
    is( $account->balance(), 15, "Account balance is 15" );
1125
    }
1093
    is( $line1->amount+0, 0, 'First fee has amount of 0' );
1126
    'Koha::Exceptions::MissingParameter',
1094
    is( $line1->amountoutstanding+0, 0, 'First fee has amount outstanding of 0' );
1127
      "->cancel() requires the `branch` parameter is passed";
1128
    throws_ok {
1129
        $debit1->reduce( { branch => $branchcode } );
1130
    }
1131
    'Koha::Exceptions::MissingParameter',
1132
      "->cancel() requires the `staff_id` parameter is passed";
1095
1133
1096
    $ret = $line2->cancel();
1134
    throws_ok {
1097
    is ($ret, undef, 'cancel returns undef when line cannot be cancelled');
1135
        $debit2->cancel(
1136
            { staff_id => $staff->borrowernumber, branch => $branchcode } );
1137
    }
1138
    'Koha::Exceptions::Account',
1139
      '->cancel() can only be used with debits that have not been offset';
1140
1141
    my $cancellation = $debit1->cancel(
1142
        { staff_id => $staff->borrowernumber, branch => $branchcode } );
1143
    is( ref($cancellation), 'Koha::Account::Line',
1144
        'Cancel returns an account line' );
1145
    is(
1146
        $cancellation->amount() * 1,
1147
        $debit1->amount * -1,
1148
        "Cancellation amount is " . $debit1->amount
1149
    );
1150
    is( $cancellation->amountoutstanding() * 1,
1151
        0, "Cancellation amountoutstanding is 0" );
1152
    is( $debit1->amountoutstanding() * 1,
1153
        0, "Debit amountoutstanding reduced to 0" );
1154
    is( $debit1->status(), 'CANCELLED', "Debit status updated to CANCELLED" );
1155
    is( $account->balance() * 1, 15, "Account balance is 15" );
1098
1156
1099
    my $account_payment = Koha::Account::Lines->find($id);
1157
    my $offsets = Koha::Account::Offsets->search(
1100
    $ret = $account_payment->cancel();
1158
        { credit_id => $cancellation->id, debit_id => $debit1->id } );
1101
    is ($ret, undef, 'payment cannot be cancelled');
1159
    is( $offsets->count, 1, 'Only one offset is generated' );
1160
    my $THE_offset = $offsets->next;
1161
    is( $THE_offset->amount * 1,
1162
        -10, 'Correct amount was applied against debit' );
1163
    is( $THE_offset->type, 'CANCELLATION',
1164
        "Offset type set to 'CANCELLATION'" );
1102
1165
1103
    $schema->storage->txn_rollback;
1166
    $schema->storage->txn_rollback;
1104
};
1167
};
1105
- 

Return to bug 24603