@@ -, +, @@ --- C4/Accounts.pm | 269 ++++++++++++++++++++++++++++++++++-------------------- C4/Circulation.pm | 174 +++++++++++++++++------------------ C4/Overdues.pm | 17 ++++ Koha/Account.pm | 46 ++++++++-- 4 files changed, 309 insertions(+), 197 deletions(-) --- a/C4/Accounts.pm +++ a/C4/Accounts.pm @@ -26,6 +26,9 @@ use C4::Members; use C4::Circulation qw(ReturnLostItem); use C4::Log qw(logaction); use Koha::Account; +use Koha::Account::Line; +use Koha::Account::Lines; +use Koha::Account::Offset; use Data::Dumper qw(Dumper); @@ -118,33 +121,61 @@ EOT =cut +=head2 chargelostitem + +In a default install of Koha the following lost values are set +1 = Lost +2 = Long overdue +3 = Lost and paid for + +FIXME: itemlost should be set to 3 after payment is made, should be a warning to the interface that a charge has been added +FIXME : if no replacement price, borrower just doesn't get charged? + +=cut + sub chargelostitem{ -# lost ==1 Lost, lost==2 longoverdue, lost==3 lost and paid for -# FIXME: itemlost should be set to 3 after payment is made, should be a warning to the interface that -# a charge has been added -# FIXME : if no replacement price, borrower just doesn't get charged? my $dbh = C4::Context->dbh(); my ($borrowernumber, $itemnumber, $amount, $description) = @_; # first make sure the borrower hasn't already been charged for this item - my $sth1=$dbh->prepare("SELECT * from accountlines - WHERE borrowernumber=? AND itemnumber=? and accounttype='L'"); - $sth1->execute($borrowernumber,$itemnumber); - my $existing_charge_hashref=$sth1->fetchrow_hashref(); + my $existing_charges = Koha::Account::Lines->search( + { + borrowernumber => $borrowernumber, + itemnumber => $itemnumber, + accounttype => 'L', + } + )->count(); # OK, they haven't - unless ($existing_charge_hashref) { + unless ($existing_charges) { my $manager_id = 0; $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv; # This item is on issue ... add replacement cost to the borrower's record and mark it returned # Note that we add this to the account even if there's no replacement price, allowing some other # process (or person) to update it, since we don't handle any defaults for replacement prices. my $accountno = getnextacctno($borrowernumber); - my $sth2=$dbh->prepare("INSERT INTO accountlines - (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding,itemnumber,manager_id) - VALUES (?,?,now(),?,?,'L',?,?,?)"); - $sth2->execute($borrowernumber,$accountno,$amount, - $description,$amount,$itemnumber,$manager_id); + + my $accountline = Koha::Account::Line->new( + { + borrowernumber => $borrowernumber, + accountno => $accountno, + date => \'NOW()', + amount => $amount, + description => $description, + accounttype => 'L', + amountoutstanding => $amount, + itemnumber => $itemnumber, + manager_id => $manager_id, + } + )->store(); + + my $account_offset = Koha::Account::Offset->new( + { + debit_id => $accountline->id, + type => 'Lost Item', + amount => $amount, + } + )->store(); if ( C4::Context->preference("FinesLog") ) { logaction("FINES", 'CREATE', $borrowernumber, Dumper({ @@ -208,21 +239,29 @@ sub manualinvoice { $notifyid = 1; } - if ( $itemnum ) { - $desc .= ' ' . $itemnum; - my $sth = $dbh->prepare( - 'INSERT INTO accountlines - (borrowernumber, accountno, date, amount, description, accounttype, amountoutstanding, itemnumber,notify_id, note, manager_id) - VALUES (?, ?, now(), ?,?, ?,?,?,?,?,?)'); - $sth->execute($borrowernumber, $accountno, $amount, $desc, $type, $amountleft, $itemnum,$notifyid, $note, $manager_id) || return $sth->errstr; - } else { - my $sth=$dbh->prepare("INSERT INTO accountlines - (borrowernumber, accountno, date, amount, description, accounttype, amountoutstanding,notify_id, note, manager_id) - VALUES (?, ?, now(), ?, ?, ?, ?,?,?,?)" - ); - $sth->execute( $borrowernumber, $accountno, $amount, $desc, $type, - $amountleft, $notifyid, $note, $manager_id ); - } + my $accountline = Koha::Account::Line->new( + { + borrowernumber => $borrowernumber, + accountno => $accountno, + date => \'NOW()', + amount => $amount, + description => $desc, + accounttype => $type, + amountoutstanding => $amountleft, + itemnumber => $itemnum || undef, + notify_id => $notifyid, + note => $note, + manager_id => $manager_id, + } + )->store(); + + my $account_offset = Koha::Account::Offset->new( + { + debit_id => $accountline->id, + type => 'Manual Debit', + amount => $amount, + } + )->store(); if ( C4::Context->preference("FinesLog") ) { logaction("FINES", 'CREATE',$borrowernumber,Dumper({ @@ -308,45 +347,50 @@ sub getrefunds { return (@results); } +#FIXME: ReversePayment should be replaced with a Void Payment feature sub ReversePayment { - my ( $accountlines_id ) = @_; + my ($accountlines_id) = @_; my $dbh = C4::Context->dbh; - my $sth = $dbh->prepare('SELECT * FROM accountlines WHERE accountlines_id = ?'); - $sth->execute( $accountlines_id ); - my $row = $sth->fetchrow_hashref(); - my $amount_outstanding = $row->{'amountoutstanding'}; - - if ( $amount_outstanding <= 0 ) { - $sth = $dbh->prepare('UPDATE accountlines SET amountoutstanding = amount * -1, description = CONCAT( description, " Reversed -" ) WHERE accountlines_id = ?'); - $sth->execute( $accountlines_id ); - } else { - $sth = $dbh->prepare('UPDATE accountlines SET amountoutstanding = 0, description = CONCAT( description, " Reversed -" ) WHERE accountlines_id = ?'); - $sth->execute( $accountlines_id ); - } + my $accountline = Koha::Account::Lines->find($accountlines_id); + my $amount_outstanding = $accountline->amountoutstanding; + + my $new_amountoutstanding = + $amount_outstanding <= 0 ? $accountline->amount * -1 : 0; + + $accountline->description( $accountline->description . " Reversed -" ); + $accountline->amountoutstanding($new_amountoutstanding); + $accountline->store(); + + my $account_offset = Koha::Account::Offset->new( + { + credit_id => $accountline->id, + type => 'Reverse Payment', + amount => $amount_outstanding - $new_amountoutstanding, + } + )->store(); if ( C4::Context->preference("FinesLog") ) { my $manager_id = 0; $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv; - if ( $amount_outstanding <= 0 ) { - $row->{'amountoutstanding'} *= -1; - } else { - $row->{'amountoutstanding'} = '0'; - } - $row->{'description'} .= ' Reversed -'; - logaction("FINES", 'MODIFY', $row->{'borrowernumber'}, Dumper({ - action => 'reverse_fee_payment', - borrowernumber => $row->{'borrowernumber'}, - old_amountoutstanding => $row->{'amountoutstanding'}, - new_amountoutstanding => 0 - $amount_outstanding,, - accountlines_id => $row->{'accountlines_id'}, - accountno => $row->{'accountno'}, - manager_id => $manager_id, - })); - + logaction( + "FINES", 'MODIFY', + $accountline->borrowernumber, + Dumper( + { + action => 'reverse_fee_payment', + borrowernumber => $accountline->borrowernumber, + old_amountoutstanding => $amount_outstanding, + new_amountoutstanding => $new_amountoutstanding, + , + accountlines_id => $accountline->id, + accountno => $accountline->accountno, + manager_id => $manager_id, + } + ) + ); } - } =head2 WriteOffFee @@ -366,60 +410,89 @@ C<$payment_note> is the note to attach to this payment sub WriteOffFee { my ( $borrowernumber, $accountlines_id, $itemnum, $accounttype, $amount, $branch, $payment_note ) = @_; + $payment_note //= ""; - $branch ||= C4::Context->userenv->{branch}; my $manager_id = 0; - $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv; + + if ( C4::Context->userenv ) { + $manager_id = C4::Context->userenv->{number}; + $branch ||= C4::Context->userenv->{branch}; + } # if no item is attached to fine, make sure to store it as a NULL $itemnum ||= undef; - my ( $sth, $query ); - my $dbh = C4::Context->dbh(); - - $query = " - UPDATE accountlines SET amountoutstanding = 0 - WHERE accountlines_id = ? AND borrowernumber = ? - "; - $sth = $dbh->prepare( $query ); - $sth->execute( $accountlines_id, $borrowernumber ); + my $accountline = Koha::Account::Lines->find($accountlines_id); + return unless $accountline; + $accountline->amountoutstanding(0); + $accountline->store(); if ( C4::Context->preference("FinesLog") ) { - logaction("FINES", 'MODIFY', $borrowernumber, Dumper({ - action => 'fee_writeoff', - borrowernumber => $borrowernumber, - accountlines_id => $accountlines_id, - manager_id => $manager_id, - })); + logaction( + "FINES", 'MODIFY', + $borrowernumber, + Dumper( + { + action => 'fee_writeoff', + borrowernumber => $borrowernumber, + accountlines_id => $accountlines_id, + manager_id => $manager_id, + } + ) + ); } - $query =" - INSERT INTO accountlines - ( borrowernumber, accountno, itemnumber, date, amount, description, accounttype, manager_id, note ) - VALUES ( ?, ?, ?, NOW(), ?, 'Writeoff', 'W', ?, ? ) - "; - $sth = $dbh->prepare( $query ); my $acct = getnextacctno($borrowernumber); - $sth->execute( $borrowernumber, $acct, $itemnum, $amount, $manager_id, $payment_note ); + + my $writeoff = Koha::Account::Line->new( + { + borrowernumber => $borrowernumber, + accountno => $acct, + itemnumber => $itemnum || undef, + date => \'NOW()', + amount => $amount * -1, + description => 'Writeoff', + accounttype => 'W', + manager_id => $manager_id, + note => $payment_note, + } + )->store(); + + Koha::Account::Offset->new( + { + debit_id => $accountline->id, + credit_id => $writeoff->id, + type => 'Writeoff', + amount => $amount * -1, + } + )->store(); if ( C4::Context->preference("FinesLog") ) { - logaction("FINES", 'CREATE',$borrowernumber,Dumper({ - action => 'create_writeoff', - borrowernumber => $borrowernumber, - accountno => $acct, - amount => 0 - $amount, - accounttype => 'W', - itemnumber => $itemnum, - accountlines_paid => [ $accountlines_id ], - manager_id => $manager_id, - })); + logaction( + "FINES", 'CREATE', + $borrowernumber, + Dumper( + { + action => 'create_writeoff', + borrowernumber => $borrowernumber, + accountno => $acct, + amount => 0 - $amount, + accounttype => 'W', + itemnumber => $itemnum, + accountlines_paid => [$accountlines_id], + manager_id => $manager_id, + } + ) + ); } - UpdateStats({ - branch => $branch, - type => 'writeoff', - amount => $amount, - borrowernumber => $borrowernumber} + UpdateStats( + { + branch => $branch, + type => 'writeoff', + amount => $amount, + borrowernumber => $borrowernumber + } ); } --- a/C4/Circulation.pm +++ a/C4/Circulation.pm @@ -56,6 +56,9 @@ use Koha::Libraries; use Koha::Holds; use Koha::RefundLostItemFeeRule; use Koha::RefundLostItemFeeRules; +use Koha::Account::Lines; +use Koha::Account::Line; +use Koha::Account::Offset; use Carp; use List::MoreUtils qw( uniq ); use Scalar::Util qw( looks_like_number ); @@ -2451,39 +2454,65 @@ sub _FixOverduesOnReturn { my $dbh = C4::Context->dbh; # check for overdue fine - my $sth = $dbh->prepare( -"SELECT * FROM accountlines WHERE (borrowernumber = ?) AND (itemnumber = ?) AND (accounttype='FU' OR accounttype='O')" - ); - $sth->execute( $borrowernumber, $item ); - - # alter fine to show that the book has been returned - my $data = $sth->fetchrow_hashref; - return 0 unless $data; # no warning, there's just nothing to fix + my $accountline = Koha::Account::Lines->search( + { + borrowernumber => $borrowernumber, + itemnumber => $item, + -or => [ + accounttype => 'FU', + accounttype => 'O', + ], + } + )->next(); + return 0 unless $accountline; # no warning, there's just nothing to fix my $uquery; - my @bind = ($data->{'accountlines_id'}); if ($exemptfine) { - $uquery = "update accountlines set accounttype='FFOR', amountoutstanding=0"; + my $amountoutstanding = $accountline->amountoutstanding; + + $accountline->accounttype('FFOR'); + $accountline->amountoutstanding(0); + + Koha::Account::Offset->new( + { + debit_id => $accountline->id, + type => 'Forgiven', + amount => $amountoutstanding * -1, + } + ); + if (C4::Context->preference("FinesLog")) { &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item"); } - } elsif ($dropbox && $data->{lastincrement}) { - my $outstanding = $data->{amountoutstanding} - $data->{lastincrement} ; - my $amt = $data->{amount} - $data->{lastincrement} ; - if (C4::Context->preference("FinesLog")) { - &logaction("FINES", 'MODIFY',$borrowernumber,"Dropbox adjustment $amt, item $item"); + } elsif ($dropbox && $accountline->lastincrement) { + my $outstanding = $accountline->amountoutstanding - $accountline->lastincrement; + my $amt = $accountline->amount - $accountline->lastincrement; + + Koha::Account::Offset->new( + { + debit_id => $accountline->id, + type => 'Dropbox', + amount => $accountline->lastincrement * -1, + } + ); + + if ( C4::Context->preference("FinesLog") ) { + &logaction( "FINES", 'MODIFY', $borrowernumber, + "Dropbox adjustment $amt, item $item" ); } - $uquery = "update accountlines set accounttype='F' "; - if($outstanding >= 0 && $amt >=0) { - $uquery .= ", amount = ? , amountoutstanding=? "; - unshift @bind, ($amt, $outstanding) ; + + $accountline->accounttype('F'); + + if ( $outstanding >= 0 && $amt >= 0 ) { + $accountline->amount($amt); + $accountline->amountoutstanding($outstanding); } + } else { - $uquery = "update accountlines set accounttype='F' "; + $accountline->accounttype('F'); } - $uquery .= " where (accountlines_id = ?)"; - my $usth = $dbh->prepare($uquery); - return $usth->execute(@bind); + + return $accountline->store(); } =head2 _FixAccountForLostAndReturned @@ -2494,83 +2523,44 @@ Calculates the charge for a book lost and returned. Internal function, not exported, called only by AddReturn. -FIXME: This function reflects how inscrutable fines logic is. Fix both. -FIXME: Give a positive return value on success. It might be the $borrowernumber who received credit, or the amount forgiven. - =cut sub _FixAccountForLostAndReturned { my $itemnumber = shift or return; my $borrowernumber = @_ ? shift : undef; my $item_id = @_ ? shift : $itemnumber; # Send the barcode if you want that logged in the description - my $dbh = C4::Context->dbh; + # check for charge made for lost book - my $sth = $dbh->prepare("SELECT * FROM accountlines WHERE itemnumber = ? AND accounttype IN ('L', 'Rep', 'W') ORDER BY date DESC, accountno DESC"); - $sth->execute($itemnumber); - my $data = $sth->fetchrow_hashref; - $data or return; # bail if there is nothing to do - $data->{accounttype} eq 'W' and return; # Written off - - # writeoff this amount - my $offset; - my $amount = $data->{'amount'}; - my $acctno = $data->{'accountno'}; - my $amountleft; # Starts off undef/zero. - if ($data->{'amountoutstanding'} == $amount) { - $offset = $data->{'amount'}; - $amountleft = 0; # Hey, it's zero here, too. - } else { - $offset = $amount - $data->{'amountoutstanding'}; # Um, isn't this the same as ZERO? We just tested those two things are == - $amountleft = $data->{'amountoutstanding'} - $amount; # Um, isn't this the same as ZERO? We just tested those two things are == - } - my $usth = $dbh->prepare("UPDATE accountlines SET accounttype = 'LR',amountoutstanding='0' - WHERE (accountlines_id = ?)"); - $usth->execute($data->{'accountlines_id'}); # We might be adjusting an account for some OTHER borrowernumber now. Not the one we passed in. - #check if any credit is left if so writeoff other accounts - my $nextaccntno = getnextacctno($data->{'borrowernumber'}); - $amountleft *= -1 if ($amountleft < 0); - if ($amountleft > 0) { - my $msth = $dbh->prepare("SELECT * FROM accountlines WHERE (borrowernumber = ?) - AND (amountoutstanding >0) ORDER BY date"); # might want to order by amountoustanding ASC (pay smallest first) - $msth->execute($data->{'borrowernumber'}); - # offset transactions - my $newamtos; - my $accdata; - while (($accdata=$msth->fetchrow_hashref) and ($amountleft>0)){ - if ($accdata->{'amountoutstanding'} < $amountleft) { - $newamtos = 0; - $amountleft -= $accdata->{'amountoutstanding'}; - } else { - $newamtos = $accdata->{'amountoutstanding'} - $amountleft; - $amountleft = 0; - } - my $thisacct = $accdata->{'accountlines_id'}; - # FIXME: move prepares outside while loop! - my $usth = $dbh->prepare("UPDATE accountlines SET amountoutstanding= ? - WHERE (accountlines_id = ?)"); - $usth->execute($newamtos,$thisacct); - $usth = $dbh->prepare("INSERT INTO accountoffsets - (borrowernumber, accountno, offsetaccount, offsetamount) - VALUES - (?,?,?,?)"); - $usth->execute($data->{'borrowernumber'},$accdata->{'accountno'},$nextaccntno,$newamtos); + my $accountline = Koha::Account::Lines->search( + { + itemnumber => $itemnumber, + accounttype => { -in => [ 'L', 'Rep', 'W' ] }, + }, + { + order_by => { -desc => [ 'date', 'accountno' ] } } - } - $amountleft *= -1 if ($amountleft > 0); - my $desc = "Item Returned " . $item_id; - $usth = $dbh->prepare("INSERT INTO accountlines - (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding) - VALUES (?,?,now(),?,?,'CR',?)"); - $usth->execute($data->{'borrowernumber'},$nextaccntno,0-$amount,$desc,$amountleft); - if ($borrowernumber) { - # FIXME: same as query above. use 1 sth for both - $usth = $dbh->prepare("INSERT INTO accountoffsets - (borrowernumber, accountno, offsetaccount, offsetamount) - VALUES (?,?,?,?)"); - $usth->execute($borrowernumber, $data->{'accountno'}, $nextaccntno, $offset); - } - ModItem({ paidfor => '' }, undef, $itemnumber); - return; + )->next(); + + return unless $accountline; + return if $accountline->accounttype eq 'W'; # Written off + + $accountline->accounttype('LR'); + $accountline->store(); + + my $account = Koha::Account->new( { patron_id => $accountline->borrowernumber } ); + my $credit_id = $account->pay( + { + amount => $accountline->amount, + description => "Item Returned " . $item_id, + type => 'CR', + accounlines => [$accountline], + + } + ); + + ModItem( { paidfor => '' }, undef, $itemnumber ); + + return $credit_id; } =head2 _GetCircControlBranch --- a/C4/Overdues.pm +++ a/C4/Overdues.pm @@ -36,6 +36,7 @@ use C4::Debug; use Koha::DateUtils; use Koha::Account::Line; use Koha::Account::Lines; +use Koha::Account::Offset; use vars qw(@ISA @EXPORT); @@ -590,6 +591,14 @@ sub UpdateFine { accounttype => 'FU', } )->store(); + + Koha::Account::Offset->new( + { + debit_id => $accountline->id, + type => 'Fine Update', + amount => $diff, + } + )->store(); } } else { if ( $amount ) { # Don't add new fines with an amount of 0 @@ -617,6 +626,14 @@ sub UpdateFine { issue_id => $issue_id, } )->store(); + + Koha::Account::Offset->new( + { + debit_id => $accountline->id, + type => 'Fine', + amount => $amount, + } + )->store(); } } # logging action --- a/Koha/Account.pm +++ a/Koha/Account.pm @@ -27,6 +27,7 @@ use C4::Stats qw( UpdateStats ); use Koha::Account::Line; use Koha::Account::Lines; +use Koha::Account::Offset; use Koha::DateUtils qw( dt_from_string ); =head1 NAME @@ -49,12 +50,14 @@ This method allows payments to be made against feees Koha::Account->new( { patron_id => $borrowernumber } )->pay( { - amount => $amount, - sip => $sipmode, - note => $note, - id => $accountlines_id, - library_id => $branchcode, - lines => $lines, # Arrayref of Koha::Account::Line objects to pay + amount => $amount, + sip => $sipmode, + note => $note, + id => $accountlines_id, + library_id => $branchcode, + lines => $lines, # Arrayref of Koha::Account::Line objects to pay + type => $type, # accounttype code + description => $description, } ); @@ -68,6 +71,10 @@ sub pay { my $note = $params->{note} || q{}; my $library_id = $params->{library_id}; my $lines = $params->{lines}, + my $type = $params->{type}; + my $description = $params->{description}; + + $type ||= 'Payment'; my $userenv = C4::Context->userenv; @@ -89,6 +96,8 @@ sub pay { my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary $balance_remaining ||= 0; + my @account_offsets; + # We were passed a specific line to pay foreach my $fine ( @$lines ) { # If accountline id is passed but no amount, we pay that line in full @@ -99,6 +108,15 @@ sub pay { $fine->amountoutstanding( $new_amountoutstanding )->store(); $balance_remaining = $balance_remaining - $amount; + my $account_offset = Koha::Account::Offset->new( + { + debit_id => $fine->id, + type => $type, + amount => $amount * -1, + } + ); + push( @account_offsets, $account_offset ); + if ( $fine->accounttype eq 'Rep' || $fine->accounttype eq 'L' ) { C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber ); @@ -147,6 +165,15 @@ sub pay { $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay ); $fine->store(); + my $account_offset = Koha::Account::Offset->new( + { + debit_id => $fine->id, + type => 'Payment', + amount => $amount_to_pay * -1, + } + ); + push( @account_offsets, $account_offset ); + if ( C4::Context->preference("FinesLog") ) { logaction( "FINES", 'MODIFY', @@ -180,7 +207,7 @@ sub pay { accountno => $accountno, date => dt_from_string(), amount => 0 - $amount, - description => q{}, + description => $description, accounttype => $account_type, amountoutstanding => 0 - $balance_remaining, manager_id => $manager_id, @@ -188,6 +215,11 @@ sub pay { } )->store(); + foreach my $o ( @account_offsets ) { + $o->credit_id( $payment->id() ); + $o->store(); + } + $library_id ||= $userenv ? $userenv->{'branch'} : undef; UpdateStats( --