@@ -, +, @@ --- Koha/Account/Line.pm | 137 ++++++++++++++++++++++++++++++---------- Koha/Account/Offsets.pm | 44 +++++++++++++ 2 files changed, 148 insertions(+), 33 deletions(-) --- a/Koha/Account/Line.pm +++ a/Koha/Account/Line.pm @@ -172,7 +172,7 @@ sub debit_offsets { Return the credits linked to this account line if some exist. Search conditions and attributes may be passed if you wish to filter -the resultant resultant resultset. +the resultant resultset. =cut @@ -200,7 +200,7 @@ sub credits { Return the debits linked to this account line if some exist. Search conditions and attributes may be passed if you wish to filter -the resultant resultant resultset. +the resultant resultset. =cut @@ -362,13 +362,13 @@ sub void { =head3 cancel - $debit_accountline->cancel({ cancellation_type => $type }); + $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 will be refunded upto -the amount paid excluding discounts, writeoffs and refunds. +Charges that have been fully or partially offset will be refunded upto +the amount paid excluding discounts, writeoffs and prior refunds. Returns the cancellation accountline. @@ -393,22 +393,43 @@ sub cancel { error => 'Account line ' . $self->id . 'is already cancelled' ); } - my $cancellation_type = $params->{cancellation_type} // 'CANCELLATION'; - - my $cancellation_amount = $self->amountoutstanding; + my $cancellation_amount = $self->amount; + my @applied_offsets; if ( $self->amount != $self->amountoutstanding ) { - my $credit_offsets = Koha::Account::Offsets->search( + # Reductions are 'Writeoff', 'Refund' or 'Discount' lines applied against the debt already. + # We can ignore 'VOID' status as it will be accounted for in the offsets total + my $reduction_offsets = Koha::Account::Offsets->search( { - debit_id => $self->id, - credit_id => { '!=' => undef }, # not the debt itself - type => { '!=' => [ 'Writeoff', 'REFUND', 'DISCOUNT' ] } - , # Don't refund writeoffs, refunds or discounts - amount => { '<' => 0 } # credits are negative on the DB + debit_id => $self->id, + credit_id => { '!=' => undef }, + 'credit.credit_type_code' => + [ 'WRITEOFF', 'REFUND', 'DISCOUNT' ], + }, + { + join => 'credit' } ); $cancellation_amount += - $credit_offsets->count > 0 ? $credit_offsets->total * -1 : 0; + $reduction_offsets->count > 0 ? $reduction_offsets->total : 0; + + # Payments are any credits applied (and not subsequently voided) that are + # not 'Writeoff', 'Refund' or 'Discount' + @applied_offsets = Koha::Account::Offsets->search( + { + debit_id => $self->id, + credit_id => { '!=' => undef }, + 'credit.credit_type_code' => [ + '-and' => { '!=' => 'WRITEOFF' }, + { '!=' => 'REFUND' }, { '!=' => 'DISCOUNT' } + ], + 'credit.status' => [ { '!=' => 'VOID' }, undef ], + 'me.amount' => { '<' => 0 } + }, + { + join => 'credit' + } + ); } my $cancellation; @@ -416,15 +437,53 @@ sub cancel { sub { # A 'cancellation' is a 'credit' - $cancellation = $self->reduce( + $cancellation = Koha::Account::Line->new( + { + date => \'NOW()', + amount => 0 - $cancellation_amount, + credit_type_code => 'CANCELLATION', + status => 'ADDED', + amountoutstanding => 0 - $cancellation_amount, + manager_id => $params->{staff_id}, + borrowernumber => $self->borrowernumber, + interface => 'intranet', + branchcode => $params->{branch}, + } + )->store(); + + Koha::Account::Offset->new( { - reduction_type => $cancellation_type, - amount => $cancellation_amount, - staff_id => $params->{staff_id}, - interface => $params->{interface}, - branch => $params->{branch} + credit_id => $cancellation->accountlines_id, + type => 'CREATE', + amount => $cancellation_amount } - ); + )->store(); + + # Link cancellation to charge + $self->set( + { + amountoutstanding => $cancellation_amount, + status => 'CANCELLED' + } + )->store(); + $cancellation->apply( { debits => [$self] } ); + + # Reverse any applied payments + for my $applied_offset ( @applied_offsets ) { + my $credit = $applied_offset->credit; + $credit->amountoutstanding( + $credit->amountoutstanding - $applied_offset->amount ) + ->store(); + + Koha::Account::Offset->new( + { + credit_id => $applied_offset->credit_id, + debit_id => $self->id, + amount => $applied_offset->amount * -1, + type => 'VOID', + } + )->store(); + } } ); @@ -454,7 +513,6 @@ Reduction type may be one of: * REFUND * DISCOUNT -* CANCELLATION Returns the reduction accountline (which will be a credit) @@ -482,6 +540,11 @@ sub reduce { } } + # Amount should always be passed as positive + Koha::Exceptions::Account::AmountNotPositive->throw( + error => 'Reduce amount passed is not positive' ) + unless ( $params->{amount} > 0 ); + # More mandatory parameters if ( $params->{interface} eq 'intranet' ) { my @optional = ( 'staff_id', 'branch' ); @@ -496,25 +559,33 @@ sub reduce { # Make sure the reduction isn't more than the original my $original = $self->amount; - Koha::Exceptions::Account::AmountNotPositive->throw( - error => 'Reduce amount passed is not positive' ) - unless ( $params->{amount} > 0 ); Koha::Exceptions::ParameterTooHigh->throw( error => "Amount to reduce ($params->{amount}) is higher than original amount ($original)" ) unless ( $original >= $params->{amount} ); - my $reduced = - $self->credits( { credit_type_code => [ 'WRITEOFF', 'DISCOUNT', 'REFUND' ] } )->total; + + # Make sure combined reduction isn't more than the original + my $reduced = 0; + if ( $self->amount != $self->amountoutstanding ) { + my $reduction_offsets = Koha::Account::Offsets->search( + { + debit_id => $self->id, + credit_id => { '!=' => undef }, # not the debt itself + type => { '!=' => [ 'Writeoff', 'REFUND', 'DISCOUNT' ] } + , # Don't refund writeoffs, refunds or discounts + amount => { '<' => 0 } # credits are negative on the DB + } + ); + + $reduced = $reduction_offsets->count > 0 ? $reduction_offsets->total * -1 : 0; + } + Koha::Exceptions::ParameterTooHigh->throw( error => "Combined reduction ($params->{amount} + $reduced) is higher than original amount (" . abs($original) . ")" ) unless ( $original >= ( $params->{amount} + abs($reduced) ) ); - my $status = { - 'REFUND' => 'REFUNDED', - 'DISCOUNT' => 'DISCOUNTED', - 'CANCELLATION' => 'CANCELLED' - }; + my $status = { 'REFUND' => 'REFUNDED', 'DISCOUNT' => 'DISCOUNTED' }; my $reduction; $self->_result->result_source->schema->txn_do( --- a/Koha/Account/Offsets.pm +++ a/Koha/Account/Offsets.pm @@ -61,6 +61,50 @@ sub total { : 0; } + +=head3 + +=cut + +sub filter_by_non_reversable { + my ($self) = @_; + + my $me = $self->_resultset()->current_source_alias . "."; + my $where = { + debit_id => { '!=' => undef }, + credit_id => { '!=' => undef }, + 'credit.credit_type_code' => [ 'WRITEOFF', 'REFUND', 'DISCOUNT' ], + 'credit.status' => [ { '!=' => 'VOID' }, undef ], + $me.amount => { '<' => 0 } + }; + my $attr = { join => 'credit' }; + + return $self->search( $where, $attr ); +} + +=head3 + +=cut + +sub filter_by_reversable { + my ($self) = @_; + + my $me = $self->_resultset()->current_source_alias . "."; + my $where = { + debit_id => { '!=' => undef }, + credit_id => { '!=' => undef }, + 'credit.credit_type_code' => [ + '-and' => { '!=' => 'WRITEOFF' }, + { '!=' => 'REFUND' }, { '!=' => 'DISCOUNT' } + ], + 'credit.status' => [ { '!=' => 'VOID' }, undef ], + $me.amount => { '<' => 0 } + }; + my $attr = { join => 'credit' }; + + return $self->search( $where, $attr ); +} + =head2 Internal methods =head3 _type --