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

(-)a/Koha/Account/Line.pm (-27 / +72 lines)
Lines 291-336 sub void { Link Here
291
291
292
Cancel a charge. It will mark the debit as 'cancelled' by updating its
292
Cancel a charge. It will mark the debit as 'cancelled' by updating its
293
status to 'CANCELLED'.
293
status to 'CANCELLED'.
294
294
Charges that have been fully or partially paid cannot be cancelled.
295
Charges that have been fully or partially paid cannot be cancelled.
295
296
296
Return self in case of success, undef otherwise
297
Returns the cancellation accountline.
297
298
298
=cut
299
=cut
299
300
300
sub cancel {
301
sub cancel {
301
    my ($self) = @_;
302
    my ( $self, $params ) = @_;
302
303
303
    # Make sure it is a charge we are cancelling
304
    # Make sure it is a charge we are reducing
304
    return unless $self->is_debit;
305
    unless ( $self->is_debit ) {
306
        Koha::Exceptions::Account::IsNotDebit->throw(
307
            error => 'Account line ' . $self->id . 'is not a debit' );
308
    }
309
    if ( $self->debit_type_code eq 'PAYOUT' ) {
310
        Koha::Exceptions::Account::IsNotDebit->throw(
311
            error => 'Account line ' . $self->id . 'is a payout' );
312
    }
305
313
306
    # Make sure it is not already cancelled
314
    # Make sure it is not already cancelled
307
    return if $self->status && $self->status eq 'CANCELLED';
315
    if ( $self->status && $self->status eq 'CANCELLED' ) {
316
        Koha::Exceptions::Account->throw(
317
            error => 'Account line ' . $self->id . 'is already cancelled' );
318
    }
308
319
309
    # Make sure it has not be paid yet
320
    # Make sure it has not be paid yet
310
    return if $self->amount != $self->amountoutstanding;
321
    if ( $self->amount != $self->amountoutstanding ) {
311
322
        Koha::Exceptions::Account->throw(
312
    if ( C4::Context->preference("FinesLog") ) {
323
            error => 'Account line ' . $self->id . 'is already offset' );
313
        logaction('FINES', 'CANCEL', $self->borrowernumber, Dumper({
314
            action => 'cancel_charge',
315
            borrowernumber => $self->borrowernumber,
316
            amount => $self->amount,
317
            amountoutstanding => $self->amountoutstanding,
318
            description => $self->description,
319
            debit_type_code => $self->debit_type_code,
320
            note => $self->note,
321
            itemnumber => $self->itemnumber,
322
            manager_id => $self->manager_id,
323
        }));
324
    }
324
    }
325
325
326
    $self->set({
326
    # Check for mandatory parameters
327
        status => 'CANCELLED',
327
    my @mandatory = ( 'staff_id', 'branch' );
328
        amountoutstanding => 0,
328
    for my $param (@mandatory) {
329
        amount => 0,
329
        unless ( defined( $params->{$param} ) ) {
330
    });
330
            Koha::Exceptions::MissingParameter->throw(
331
    $self->store();
331
                error => "The $param parameter is mandatory" );
332
        }
333
    }
332
334
333
    return $self;
335
    my $cancellation;
336
    $self->_result->result_source->schema->txn_do(
337
        sub {
338
339
            # A 'cancellation' is a 'credit'
340
            $cancellation = Koha::Account::Line->new(
341
                {
342
                    date              => \'NOW()',
343
                    amount            => 0 - $self->amount,
344
                    credit_type_code  => 'CANCELLATION',
345
                    status            => 'ADDED',
346
                    amountoutstanding => 0 - $self->amount,
347
                    manager_id        => $params->{staff_id},
348
                    borrowernumber    => $self->borrowernumber,
349
                    interface         => 'intranet',
350
                    branchcode        => $params->{branch},
351
                }
352
            )->store();
353
354
            my $cancellation_offset = Koha::Account::Offset->new(
355
                {
356
                    credit_id => $cancellation->accountlines_id,
357
                    type      => 'CANCELLATION',
358
                    amount    => $self->amount
359
                }
360
            )->store();
361
362
            # Link cancellation to charge
363
            $cancellation->apply(
364
                {
365
                    debits      => [$self],
366
                    offset_type => 'CANCELLATION'
367
                }
368
            );
369
            $cancellation->status('APPLIED')->store();
370
371
            # Update status of original debit
372
            $self->status('CANCELLED')->store;
373
        }
374
    );
375
376
    $cancellation->discard_changes;
377
    return $cancellation;
334
}
378
}
335
379
336
=head3 reduce
380
=head3 reduce
Lines 453-459 sub reduce { Link Here
453
            }
497
            }
454
            else {
498
            else {
455
499
456
        # Zero amount offset used to link original 'debit' to reduction 'credit'
500
                # Zero amount offset used to link original 'debit' to
501
                # reduction 'credit'
457
                my $link_reduction_offset = Koha::Account::Offset->new(
502
                my $link_reduction_offset = Koha::Account::Offset->new(
458
                    {
503
                    {
459
                        credit_id => $reduction->accountlines_id,
504
                        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 87-98 Link Here
87
        [% END %]
87
        [% END %]
88
        <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>
88
        <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>
89
        [% IF account.is_debit && account.amountoutstanding > 0 %]
89
        [% IF account.is_debit && account.amountoutstanding > 0 %]
90
            <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>
90
            <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>
91
        [% END %]
91
        [% END %]
92
        [% IF account.is_credit && account.status != 'VOID' %]
92
        [% IF account.is_credit && account.status != 'VOID' %]
93
          <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>
93
          <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>
94
        [% END %]
94
        [% END %]
95
        [% IF account.is_debit && account.amount == account.amountoutstanding && account.status != 'CANCELLED' %]
95
        [% IF account.is_debit && account.amount == account.amountoutstanding && account.status != 'CANCELLED' && !(account.debit_type_code == 'PAYOUT') %]
96
          <form method="post" action="/cgi-bin/koha/members/cancel-charge.pl">
96
          <form method="post" action="/cgi-bin/koha/members/cancel-charge.pl">
97
            <input type="hidden" name="csrf_token" value="[% csrf_token | html %]">
97
            <input type="hidden" name="csrf_token" value="[% csrf_token | html %]">
98
            <input type="hidden" name="borrowernumber" value="[% patron.borrowernumber | html %]">
98
            <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 901-907 subtest "payout() tests" => sub { Link Here
901
901
902
subtest "reduce() tests" => sub {
902
subtest "reduce() tests" => sub {
903
903
904
    plan tests => 27;
904
    plan tests => 29;
905
905
906
    $schema->storage->txn_begin;
906
    $schema->storage->txn_begin;
907
907
Lines 961-967 subtest "reduce() tests" => sub { Link Here
961
961
962
    my $reduce_params = {
962
    my $reduce_params = {
963
        interface      => 'commandline',
963
        interface      => 'commandline',
964
        reduction_type => 'REFUND',
964
        reduction_type => 'DISCOUNT',
965
        amount         => 5,
965
        amount         => 5,
966
        staff_id       => $staff->borrowernumber,
966
        staff_id       => $staff->borrowernumber,
967
        branch         => $branchcode
967
        branch         => $branchcode
Lines 1009-1015 subtest "reduce() tests" => sub { Link Here
1009
      '->reduce() cannot reduce more than original amount';
1009
      '->reduce() cannot reduce more than original amount';
1010
1010
1011
    # Partial Reduction
1011
    # Partial Reduction
1012
    # (Refund 5 on debt of 20)
1012
    # (Discount 5 on debt of 20)
1013
    my $reduction = $debit1->reduce($reduce_params);
1013
    my $reduction = $debit1->reduce($reduce_params);
1014
1014
1015
    is( ref($reduction), 'Koha::Account::Line',
1015
    is( ref($reduction), 'Koha::Account::Line',
Lines 1019-1024 subtest "reduce() tests" => sub { Link Here
1019
        0, "Reduce amountoutstanding is 0" );
1019
        0, "Reduce amountoutstanding is 0" );
1020
    is( $debit1->amountoutstanding() * 1,
1020
    is( $debit1->amountoutstanding() * 1,
1021
        15, "Debit amountoutstanding reduced by 5 to 15" );
1021
        15, "Debit amountoutstanding reduced by 5 to 15" );
1022
    is( $debit1->status(), 'DISCOUNTED', "Debit status updated to DISCOUNTED");
1022
    is( $account->balance() * 1, -5,        "Account balance is -5" );
1023
    is( $account->balance() * 1, -5,        "Account balance is -5" );
1023
    is( $reduction->status(),    'APPLIED', "Reduction status is 'APPLIED'" );
1024
    is( $reduction->status(),    'APPLIED', "Reduction status is 'APPLIED'" );
1024
1025
Lines 1028-1044 subtest "reduce() tests" => sub { Link Here
1028
    my $THE_offset = $offsets->next;
1029
    my $THE_offset = $offsets->next;
1029
    is( $THE_offset->amount * 1,
1030
    is( $THE_offset->amount * 1,
1030
        -5, 'Correct amount was applied against debit' );
1031
        -5, 'Correct amount was applied against debit' );
1031
    is( $THE_offset->type, 'REFUND', "Offset type set to 'REFUND'" );
1032
    is( $THE_offset->type, 'DISCOUNT', "Offset type set to 'DISCOUNT'" );
1032
1033
1033
    # Zero offset created when zero outstanding
1034
    # Zero offset created when zero outstanding
1034
    # (Refund another 5 on paid debt of 20)
1035
    # (Refund another 5 on paid debt of 20)
1035
    $credit1->apply( { debits => [$debit1] } );
1036
    $credit1->apply( { debits => [$debit1] } );
1036
    is( $debit1->amountoutstanding + 0,
1037
    is( $debit1->amountoutstanding + 0,
1037
        0, 'Debit1 amountoutstanding reduced to 0' );
1038
        0, 'Debit1 amountoutstanding reduced to 0' );
1039
    $reduce_params->{reduction_type} = 'REFUND';
1038
    $reduction = $debit1->reduce($reduce_params);
1040
    $reduction = $debit1->reduce($reduce_params);
1039
    is( $reduction->amount() * 1, -5, "Reduce amount is -5" );
1041
    is( $reduction->amount() * 1, -5, "Reduce amount is -5" );
1040
    is( $reduction->amountoutstanding() * 1,
1042
    is( $reduction->amountoutstanding() * 1,
1041
        -5, "Reduce amountoutstanding is -5" );
1043
        -5, "Reduce amountoutstanding is -5" );
1044
    is( $debit1->status(), 'REFUNDED', "Debit status updated to REFUNDED");
1042
1045
1043
    $offsets = Koha::Account::Offsets->search(
1046
    $offsets = Koha::Account::Offsets->search(
1044
        { credit_id => $reduction->id, debit_id => $debit1->id } );
1047
        { credit_id => $reduction->id, debit_id => $debit1->id } );
Lines 1077-1104 subtest "reduce() tests" => sub { Link Here
1077
};
1080
};
1078
1081
1079
subtest "cancel() tests" => sub {
1082
subtest "cancel() tests" => sub {
1080
    plan tests => 9;
1083
    plan tests => 16;
1081
1084
1082
    $schema->storage->txn_begin;
1085
    $schema->storage->txn_begin;
1083
1086
1084
    # Create a borrower
1087
    # Create a borrower
1085
    my $categorycode = $builder->build({ source => 'Category' })->{ categorycode };
1088
    my $categorycode =
1086
    my $branchcode   = $builder->build({ source => 'Branch' })->{ branchcode };
1089
      $builder->build( { source => 'Category' } )->{categorycode};
1090
    my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode};
1087
1091
1088
    my $borrower = Koha::Patron->new( {
1092
    my $borrower = Koha::Patron->new(
1089
        cardnumber => 'dariahall',
1093
        {
1090
        surname => 'Hall',
1094
            cardnumber => 'dariahall',
1091
        firstname => 'Daria',
1095
            surname    => 'Hall',
1092
    } );
1096
            firstname  => 'Daria',
1093
    $borrower->categorycode( $categorycode );
1097
        }
1094
    $borrower->branchcode( $branchcode );
1098
    );
1099
    $borrower->categorycode($categorycode);
1100
    $borrower->branchcode($branchcode);
1095
    $borrower->store;
1101
    $borrower->store;
1096
1102
1097
    t::lib::Mocks::mock_userenv({ branchcode => $branchcode, borrowernumber => $borrower->borrowernumber });
1103
    my $staff = Koha::Patron->new(
1104
        {
1105
            cardnumber => 'bobby',
1106
            surname    => 'Bloggs',
1107
            firstname  => 'Bobby',
1108
        }
1109
    );
1110
    $staff->categorycode($categorycode);
1111
    $staff->branchcode($branchcode);
1112
    $staff->store;
1113
1114
    t::lib::Mocks::mock_userenv(
1115
        {
1116
            branchcode     => $branchcode,
1117
            borrowernumber => $borrower->borrowernumber
1118
        }
1119
    );
1098
1120
1099
    my $account = Koha::Account->new({ patron_id => $borrower->id });
1121
    my $account = Koha::Account->new( { patron_id => $borrower->id } );
1100
1122
1101
    my $line1 = Koha::Account::Line->new(
1123
    my $debit1 = Koha::Account::Line->new(
1102
        {
1124
        {
1103
            borrowernumber    => $borrower->borrowernumber,
1125
            borrowernumber    => $borrower->borrowernumber,
1104
            amount            => 10,
1126
            amount            => 10,
Lines 1107-1113 subtest "cancel() tests" => sub { Link Here
1107
            debit_type_code   => 'OVERDUE',
1129
            debit_type_code   => 'OVERDUE',
1108
        }
1130
        }
1109
    )->store();
1131
    )->store();
1110
    my $line2 = Koha::Account::Line->new(
1132
    my $debit2 = Koha::Account::Line->new(
1111
        {
1133
        {
1112
            borrowernumber    => $borrower->borrowernumber,
1134
            borrowernumber    => $borrower->borrowernumber,
1113
            amount            => 20,
1135
            amount            => 20,
Lines 1117-1143 subtest "cancel() tests" => sub { Link Here
1117
        }
1139
        }
1118
    )->store();
1140
    )->store();
1119
1141
1120
    my $id = $account->pay({
1142
    my $ret = $account->pay(
1121
        lines  => [$line2],
1143
        {
1122
        amount => 5,
1144
            lines  => [$debit2],
1123
    });
1145
            amount => 5,
1146
        }
1147
    );
1148
    my $credit = Koha::Account::Lines->find({ accountlines_id => $ret->{payment_id} });
1124
1149
1125
    is( $account->balance(), 25, "Account balance is 25" );
1150
    is( $account->balance(), 25, "Account balance is 25" );
1126
    is( $line1->amountoutstanding+0, 10, 'First fee has amount outstanding of 10' );
1151
    is( $debit1->amountoutstanding + 0,
1127
    is( $line2->amountoutstanding+0, 15, 'Second fee has amount outstanding of 15' );
1152
        10, 'First fee has amount outstanding of 10' );
1153
    is( $debit2->amountoutstanding + 0,
1154
        15, 'Second fee has amount outstanding of 15' );
1155
    throws_ok {
1156
        $credit->cancel(
1157
            { staff_id => $staff->borrowernumber, branch => $branchcode } );
1158
    }
1159
    'Koha::Exceptions::Account::IsNotDebit',
1160
      '->cancel() can only be used with debits';
1128
1161
1129
    my $ret = $line1->cancel();
1162
    throws_ok {
1130
    is( ref($ret), 'Koha::Account::Line', 'Cancel returns the account line' );
1163
        $debit1->reduce( { staff_id => $staff->borrowernumber } );
1131
    is( $account->balance(), 15, "Account balance is 15" );
1164
    }
1132
    is( $line1->amount+0, 0, 'First fee has amount of 0' );
1165
    'Koha::Exceptions::MissingParameter',
1133
    is( $line1->amountoutstanding+0, 0, 'First fee has amount outstanding of 0' );
1166
      "->cancel() requires the `branch` parameter is passed";
1167
    throws_ok {
1168
        $debit1->reduce( { branch => $branchcode } );
1169
    }
1170
    'Koha::Exceptions::MissingParameter',
1171
      "->cancel() requires the `staff_id` parameter is passed";
1172
1173
    throws_ok {
1174
        $debit2->cancel(
1175
            { staff_id => $staff->borrowernumber, branch => $branchcode } );
1176
    }
1177
    'Koha::Exceptions::Account',
1178
      '->cancel() can only be used with debits that have not been offset';
1134
1179
1135
    $ret = $line2->cancel();
1180
    my $cancellation = $debit1->cancel(
1136
    is ($ret, undef, 'cancel returns undef when line cannot be cancelled');
1181
        { staff_id => $staff->borrowernumber, branch => $branchcode } );
1182
    is( ref($cancellation), 'Koha::Account::Line',
1183
        'Cancel returns an account line' );
1184
    is(
1185
        $cancellation->amount() * 1,
1186
        $debit1->amount * -1,
1187
        "Cancellation amount is " . $debit1->amount
1188
    );
1189
    is( $cancellation->amountoutstanding() * 1,
1190
        0, "Cancellation amountoutstanding is 0" );
1191
    is( $debit1->amountoutstanding() * 1,
1192
        0, "Debit amountoutstanding reduced to 0" );
1193
    is( $debit1->status(), 'CANCELLED', "Debit status updated to CANCELLED" );
1194
    is( $account->balance() * 1, 15, "Account balance is 15" );
1137
1195
1138
    my $account_payment = Koha::Account::Lines->find($id);
1196
    my $offsets = Koha::Account::Offsets->search(
1139
    $ret = $account_payment->cancel();
1197
        { credit_id => $cancellation->id, debit_id => $debit1->id } );
1140
    is ($ret, undef, 'payment cannot be cancelled');
1198
    is( $offsets->count, 1, 'Only one offset is generated' );
1199
    my $THE_offset = $offsets->next;
1200
    is( $THE_offset->amount * 1,
1201
        -10, 'Correct amount was applied against debit' );
1202
    is( $THE_offset->type, 'CANCELLATION',
1203
        "Offset type set to 'CANCELLATION'" );
1141
1204
1142
    $schema->storage->txn_rollback;
1205
    $schema->storage->txn_rollback;
1143
};
1206
};
1144
- 

Return to bug 24603