3 # Copyright 2000-2002 Katipo Communications
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 #use warnings; FIXME - Bug 2505
26 use C4
::Circulation
qw(ReturnLostItem);
27 use C4
::Log
qw(logaction);
30 use Data
::Dumper
qw(Dumper);
32 use vars
qw(@ISA @EXPORT);
48 &recordpayment_selectaccts
50 &purge_zero_balance_fees
56 C4::Accounts - Functions for dealing with Koha accounts
64 The functions in this module deal with the monetary aspect of Koha,
65 including looking up and modifying the amount of money owed by a
72 &makepayment($accountlines_id, $borrowernumber, $acctnumber, $amount, $branchcode);
74 Records the fact that a patron has paid off the entire amount he or
77 C<$borrowernumber> is the patron's borrower number. C<$acctnumber> is
78 the account that was credited. C<$amount> is the amount paid (this is
79 only used to record the payment. It is assumed to be equal to the
80 amount owed). C<$branchcode> is the code of the branch where payment
86 # FIXME - I'm not at all sure about the above, because I don't
87 # understand what the acct* tables in the Koha database are for.
89 my ( $accountlines_id, $borrowernumber, $accountno, $amount, $user, $branch, $payment_note ) = @_;
91 return Koha
::Account
->new( { patron_id
=> $borrowernumber } )
92 ->pay( { accountlines_id
=> $accountlines_id, amount
=> $amount, library_id
=> $branch, note
=> $payment_note } );
97 $nextacct = &getnextacctno($borrowernumber);
99 Returns the next unused account number for the patron with the given
105 # FIXME - Okay, so what does the above actually _mean_?
107 my ($borrowernumber) = shift or return;
108 my $sth = C4
::Context
->dbh->prepare(
109 "SELECT accountno+1 FROM accountlines
110 WHERE (borrowernumber = ?)
111 ORDER BY accountno DESC
114 $sth->execute($borrowernumber);
115 return ($sth->fetchrow || 1);
118 =head2 fixaccounts (removed)
120 &fixaccounts($accountlines_id, $borrowernumber, $accountnumber, $amount);
123 # FIXME - I don't understand what this function does.
125 my ( $accountlines_id, $borrowernumber, $accountno, $amount ) = @_;
126 my $dbh = C4::Context->dbh;
127 my $sth = $dbh->prepare(
128 "SELECT * FROM accountlines WHERE accountlines_id=?"
130 $sth->execute( $accountlines_id );
131 my $data = $sth->fetchrow_hashref;
133 # FIXME - Error-checking
134 my $diff = $amount - $data->{'amount'};
135 my $outstanding = $data->{'amountoutstanding'} + $diff;
140 SET amount = '$amount',
141 amountoutstanding = '$outstanding'
142 WHERE accountlines_id = $accountlines_id
144 # FIXME: exceedingly bad form. Use prepare with placholders ("?") in query and execute args.
150 # lost ==1 Lost, lost==2 longoverdue, lost==3 lost and paid for
151 # FIXME: itemlost should be set to 3 after payment is made, should be a warning to the interface that
152 # a charge has been added
153 # FIXME : if no replacement price, borrower just doesn't get charged?
154 my $dbh = C4
::Context
->dbh();
155 my ($borrowernumber, $itemnumber, $amount, $description) = @_;
157 # first make sure the borrower hasn't already been charged for this item
158 my $sth1=$dbh->prepare("SELECT * from accountlines
159 WHERE borrowernumber=? AND itemnumber=? and accounttype='L'");
160 $sth1->execute($borrowernumber,$itemnumber);
161 my $existing_charge_hashref=$sth1->fetchrow_hashref();
164 unless ($existing_charge_hashref) {
166 $manager_id = C4
::Context
->userenv->{'number'} if C4
::Context
->userenv;
167 # This item is on issue ... add replacement cost to the borrower's record and mark it returned
168 # Note that we add this to the account even if there's no replacement price, allowing some other
169 # process (or person) to update it, since we don't handle any defaults for replacement prices.
170 my $accountno = getnextacctno
($borrowernumber);
171 my $sth2=$dbh->prepare("INSERT INTO accountlines
172 (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding,itemnumber,manager_id)
173 VALUES (?,?,now(),?,?,'L',?,?,?)");
174 $sth2->execute($borrowernumber,$accountno,$amount,
175 $description,$amount,$itemnumber,$manager_id);
177 if ( C4
::Context
->preference("FinesLog") ) {
178 logaction
("FINES", 'CREATE', $borrowernumber, Dumper
({
179 action
=> 'create_fee',
180 borrowernumber
=> $borrowernumber,
181 accountno
=> $accountno,
183 amountoutstanding
=> $amount,
184 description
=> $description,
186 itemnumber
=> $itemnumber,
187 manager_id
=> $manager_id,
196 &manualinvoice($borrowernumber, $itemnumber, $description, $type,
199 C<$borrowernumber> is the patron's borrower number.
200 C<$description> is a description of the transaction.
201 C<$type> may be one of C<CS>, C<CB>, C<CW>, C<CF>, C<CL>, C<N>, C<L>,
203 C<$itemnumber> is the item involved, if pertinent; otherwise, it
204 should be the empty string.
209 # FIXME: In Koha 3.0 , the only account adjustment 'types' passed to this function
212 # 'FOR' = FORGIVEN (Formerly 'F', but 'F' is taken to mean 'FINE' elsewhere)
215 # 'A' = Account Management fee
221 my ( $borrowernumber, $itemnum, $desc, $type, $amount, $note ) = @_;
223 $manager_id = C4
::Context
->userenv->{'number'} if C4
::Context
->userenv;
224 my $dbh = C4
::Context
->dbh;
227 my $accountno = getnextacctno
($borrowernumber);
228 my $amountleft = $amount;
230 if ( ( $type eq 'L' )
234 or ( $type eq 'M' ) )
240 $desc .= ' ' . $itemnum;
241 my $sth = $dbh->prepare(
242 'INSERT INTO accountlines
243 (borrowernumber, accountno, date, amount, description, accounttype, amountoutstanding, itemnumber,notify_id, note, manager_id)
244 VALUES (?, ?, now(), ?,?, ?,?,?,?,?,?)');
245 $sth->execute($borrowernumber, $accountno, $amount, $desc, $type, $amountleft, $itemnum,$notifyid, $note, $manager_id) || return $sth->errstr;
247 my $sth=$dbh->prepare("INSERT INTO accountlines
248 (borrowernumber, accountno, date, amount, description, accounttype, amountoutstanding,notify_id, note, manager_id)
249 VALUES (?, ?, now(), ?, ?, ?, ?,?,?,?)"
251 $sth->execute( $borrowernumber, $accountno, $amount, $desc, $type,
252 $amountleft, $notifyid, $note, $manager_id );
255 if ( C4
::Context
->preference("FinesLog") ) {
256 logaction
("FINES", 'CREATE',$borrowernumber,Dumper
({
257 action
=> 'create_fee',
258 borrowernumber
=> $borrowernumber,
259 accountno
=> $accountno,
261 description
=> $desc,
262 accounttype
=> $type,
263 amountoutstanding
=> $amountleft,
264 notify_id
=> $notifyid,
266 itemnumber
=> $itemnum,
267 manager_id
=> $manager_id,
275 my ( $borrowerno, $timestamp, $accountno ) = @_;
276 my $dbh = C4
::Context
->dbh;
277 my $timestamp2 = $timestamp - 1;
279 my $sth = $dbh->prepare(
280 "SELECT * FROM accountlines WHERE borrowernumber=? AND accountno = ?"
282 $sth->execute( $borrowerno, $accountno );
285 while ( my $data = $sth->fetchrow_hashref ) {
292 my ( $accountlines_id, $note ) = @_;
293 my $dbh = C4
::Context
->dbh;
294 my $sth = $dbh->prepare('UPDATE accountlines SET note = ? WHERE accountlines_id = ?');
295 $sth->execute( $note, $accountlines_id );
299 my ( $date, $date2 ) = @_;
300 my $dbh = C4
::Context
->dbh;
301 my $sth = $dbh->prepare(
302 "SELECT * FROM accountlines,borrowers
303 WHERE amount < 0 AND accounttype not like 'Pay%' AND accountlines.borrowernumber = borrowers.borrowernumber
304 AND timestamp >=TIMESTAMP(?) AND timestamp < TIMESTAMP(?)"
307 $sth->execute( $date, $date2 );
309 while ( my $data = $sth->fetchrow_hashref ) {
310 $data->{'date'} = $data->{'timestamp'};
318 my ( $date, $date2 ) = @_;
319 my $dbh = C4
::Context
->dbh;
321 my $sth = $dbh->prepare(
322 "SELECT *,timestamp AS datetime
323 FROM accountlines,borrowers
324 WHERE (accounttype = 'REF'
325 AND accountlines.borrowernumber = borrowers.borrowernumber
326 AND date >=? AND date <?)"
329 $sth->execute( $date, $date2 );
332 while ( my $data = $sth->fetchrow_hashref ) {
340 my ( $accountlines_id ) = @_;
341 my $dbh = C4
::Context
->dbh;
343 my $sth = $dbh->prepare('SELECT * FROM accountlines WHERE accountlines_id = ?');
344 $sth->execute( $accountlines_id );
345 my $row = $sth->fetchrow_hashref();
346 my $amount_outstanding = $row->{'amountoutstanding'};
348 if ( $amount_outstanding <= 0 ) {
349 $sth = $dbh->prepare('UPDATE accountlines SET amountoutstanding = amount * -1, description = CONCAT( description, " Reversed -" ) WHERE accountlines_id = ?');
350 $sth->execute( $accountlines_id );
352 $sth = $dbh->prepare('UPDATE accountlines SET amountoutstanding = 0, description = CONCAT( description, " Reversed -" ) WHERE accountlines_id = ?');
353 $sth->execute( $accountlines_id );
356 if ( C4
::Context
->preference("FinesLog") ) {
358 $manager_id = C4
::Context
->userenv->{'number'} if C4
::Context
->userenv;
360 if ( $amount_outstanding <= 0 ) {
361 $row->{'amountoutstanding'} *= -1;
363 $row->{'amountoutstanding'} = '0';
365 $row->{'description'} .= ' Reversed -';
366 logaction
("FINES", 'MODIFY', $row->{'borrowernumber'}, Dumper
({
367 action
=> 'reverse_fee_payment',
368 borrowernumber
=> $row->{'borrowernumber'},
369 old_amountoutstanding
=> $row->{'amountoutstanding'},
370 new_amountoutstanding
=> 0 - $amount_outstanding,,
371 accountlines_id
=> $row->{'accountlines_id'},
372 accountno
=> $row->{'accountno'},
373 manager_id
=> $manager_id,
380 =head2 recordpayment_selectaccts
382 recordpayment_selectaccts($borrowernumber, $payment,$accts);
384 Record payment by a patron. C<$borrowernumber> is the patron's
385 borrower number. C<$payment> is a floating-point number, giving the
386 amount that was paid. C<$accts> is an array ref to a list of
387 accountnos which the payment can be recorded against
389 Amounts owed are paid off oldest first. That is, if the patron has a
390 $1 fine from Feb. 1, another $1 fine from Mar. 1, and makes a payment
391 of $1.50, then the oldest fine will be paid off in full, and $0.50
392 will be credited to the next one.
396 sub recordpayment_selectaccts
{
397 my ( $borrowernumber, $amount, $accts, $note ) = @_;
399 my $dbh = C4
::Context
->dbh;
402 my $branch = C4
::Context
->userenv->{branch
};
403 my $amountleft = $amount;
405 $manager_id = C4
::Context
->userenv->{'number'} if C4
::Context
->userenv;
406 my $sql = 'SELECT * FROM accountlines WHERE (borrowernumber = ?) ' .
407 'AND (amountoutstanding<>0) ';
409 $sql .= ' AND accountlines_id IN ( ' . join ',', @
{$accts};
412 $sql .= ' ORDER BY date';
414 my $nextaccntno = getnextacctno
($borrowernumber);
416 # get lines with outstanding amounts to offset
417 my $rows = $dbh->selectall_arrayref($sql, { Slice
=> {} }, $borrowernumber);
419 # offset transactions
420 my $sth = $dbh->prepare('UPDATE accountlines SET amountoutstanding= ? ' .
421 'WHERE accountlines_id=?');
424 for my $accdata ( @
{$rows} ) {
425 if ($amountleft == 0) {
428 if ( $accdata->{amountoutstanding
} < $amountleft ) {
430 $amountleft -= $accdata->{amountoutstanding
};
433 $newamtos = $accdata->{amountoutstanding
} - $amountleft;
436 my $thisacct = $accdata->{accountlines_id
};
437 $sth->execute( $newamtos, $thisacct );
439 if ( C4
::Context
->preference("FinesLog") ) {
440 logaction
("FINES", 'MODIFY', $borrowernumber, Dumper
({
441 action
=> 'fee_payment',
442 borrowernumber
=> $borrowernumber,
443 old_amountoutstanding
=> $accdata->{'amountoutstanding'},
444 new_amountoutstanding
=> $newamtos,
445 amount_paid
=> $accdata->{'amountoutstanding'} - $newamtos,
446 accountlines_id
=> $accdata->{'accountlines_id'},
447 accountno
=> $accdata->{'accountno'},
448 manager_id
=> $manager_id,
450 push( @ids, $accdata->{'accountlines_id'} );
456 $sql = 'INSERT INTO accountlines ' .
457 '(borrowernumber, accountno,date,amount,description,accounttype,amountoutstanding,manager_id,note) ' .
458 q
|VALUES
(?
,?
,now
(),?
,'','Pay',?
,?
,?
)|;
459 $dbh->do($sql,{},$borrowernumber, $nextaccntno, 0 - $amount, 0 - $amountleft, $manager_id, $note );
464 borrowernumber
=> $borrowernumber,
465 accountno
=> $nextaccntno}
468 if ( C4
::Context
->preference("FinesLog") ) {
469 logaction
("FINES", 'CREATE',$borrowernumber,Dumper
({
470 action
=> 'create_payment',
471 borrowernumber
=> $borrowernumber,
472 accountno
=> $nextaccntno,
473 amount
=> 0 - $amount,
474 amountoutstanding
=> 0 - $amountleft,
475 accounttype
=> 'Pay',
476 accountlines_paid
=> \
@ids,
477 manager_id
=> $manager_id,
484 # makepayment needs to be fixed to handle partials till then this separate subroutine
486 sub makepartialpayment
{
487 my ( $accountlines_id, $borrowernumber, $accountno, $amount, $user, $branch, $payment_note ) = @_;
489 $manager_id = C4
::Context
->userenv->{'number'} if C4
::Context
->userenv;
490 if (!$amount || $amount < 0) {
493 $payment_note //= "";
494 my $dbh = C4
::Context
->dbh;
496 my $nextaccntno = getnextacctno
($borrowernumber);
499 my $data = $dbh->selectrow_hashref(
500 'SELECT * FROM accountlines WHERE accountlines_id=?',undef,$accountlines_id);
501 my $new_outstanding = $data->{amountoutstanding
} - $amount;
503 my $update = 'UPDATE accountlines SET amountoutstanding = ? WHERE accountlines_id = ? ';
504 $dbh->do( $update, undef, $new_outstanding, $accountlines_id);
506 if ( C4
::Context
->preference("FinesLog") ) {
507 logaction
("FINES", 'MODIFY', $borrowernumber, Dumper
({
508 action
=> 'fee_payment',
509 borrowernumber
=> $borrowernumber,
510 old_amountoutstanding
=> $data->{'amountoutstanding'},
511 new_amountoutstanding
=> $new_outstanding,
512 amount_paid
=> $data->{'amountoutstanding'} - $new_outstanding,
513 accountlines_id
=> $data->{'accountlines_id'},
514 accountno
=> $data->{'accountno'},
515 manager_id
=> $manager_id,
520 my $insert = 'INSERT INTO accountlines (borrowernumber, accountno, date, amount, '
521 . 'description, accounttype, amountoutstanding, itemnumber, manager_id, note) '
522 . ' VALUES (?, ?, now(), ?, ?, ?, 0, ?, ?, ?)';
524 $dbh->do( $insert, undef, $borrowernumber, $nextaccntno, -$amount,
525 '', 'Pay', $data->{'itemnumber'}, $manager_id, $payment_note);
531 borrowernumber
=> $borrowernumber,
532 accountno
=> $accountno
535 if ( C4
::Context
->preference("FinesLog") ) {
536 logaction
("FINES", 'CREATE',$borrowernumber,Dumper
({
537 action
=> 'create_payment',
538 borrowernumber
=> $user,
539 accountno
=> $nextaccntno,
540 amount
=> 0 - $amount,
541 accounttype
=> 'Pay',
542 itemnumber
=> $data->{'itemnumber'},
543 accountlines_paid
=> [ $data->{'accountlines_id'} ],
544 manager_id
=> $manager_id,
553 WriteOffFee( $borrowernumber, $accountline_id, $itemnum, $accounttype, $amount, $branch, $payment_note );
555 Write off a fine for a patron.
556 C<$borrowernumber> is the patron's borrower number.
557 C<$accountline_id> is the accountline_id of the fee to write off.
558 C<$itemnum> is the itemnumber of of item whose fine is being written off.
559 C<$accounttype> is the account type of the fine being written off.
560 C<$amount> is a floating-point number, giving the amount that is being written off.
561 C<$branch> is the branchcode of the library where the writeoff occurred.
562 C<$payment_note> is the note to attach to this payment
567 my ( $borrowernumber, $accountlines_id, $itemnum, $accounttype, $amount, $branch, $payment_note ) = @_;
568 $payment_note //= "";
569 $branch ||= C4
::Context
->userenv->{branch
};
571 $manager_id = C4
::Context
->userenv->{'number'} if C4
::Context
->userenv;
573 # if no item is attached to fine, make sure to store it as a NULL
577 my $dbh = C4
::Context
->dbh();
580 UPDATE accountlines SET amountoutstanding = 0
581 WHERE accountlines_id = ? AND borrowernumber = ?
583 $sth = $dbh->prepare( $query );
584 $sth->execute( $accountlines_id, $borrowernumber );
586 if ( C4
::Context
->preference("FinesLog") ) {
587 logaction
("FINES", 'MODIFY', $borrowernumber, Dumper
({
588 action
=> 'fee_writeoff',
589 borrowernumber
=> $borrowernumber,
590 accountlines_id
=> $accountlines_id,
591 manager_id
=> $manager_id,
596 INSERT INTO accountlines
597 ( borrowernumber, accountno, itemnumber, date, amount, description, accounttype, manager_id, note )
598 VALUES ( ?, ?, ?, NOW(), ?, 'Writeoff', 'W', ?, ? )
600 $sth = $dbh->prepare( $query );
601 my $acct = getnextacctno
($borrowernumber);
602 $sth->execute( $borrowernumber, $acct, $itemnum, $amount, $manager_id, $payment_note );
604 if ( C4
::Context
->preference("FinesLog") ) {
605 logaction
("FINES", 'CREATE',$borrowernumber,Dumper
({
606 action
=> 'create_writeoff',
607 borrowernumber
=> $borrowernumber,
609 amount
=> 0 - $amount,
611 itemnumber
=> $itemnum,
612 accountlines_paid
=> [ $accountlines_id ],
613 manager_id
=> $manager_id,
621 borrowernumber
=> $borrowernumber}
626 =head2 purge_zero_balance_fees
628 purge_zero_balance_fees( $days );
630 Delete accountlines entries where amountoutstanding is 0 or NULL which are more than a given number of days old.
632 B<$days> -- Zero balance fees older than B<$days> days old will be deleted.
634 B<Warning:> Because fines and payments are not linked in accountlines, it is
635 possible for a fine to be deleted without the accompanying payment,
636 or vise versa. This won't affect the account balance, but might be
641 sub purge_zero_balance_fees
{
645 my $dbh = C4
::Context
->dbh;
646 my $sth = $dbh->prepare(
648 DELETE FROM accountlines
649 WHERE date < date_sub(curdate(), INTERVAL ? DAY)
650 AND ( amountoutstanding = 0 or amountoutstanding IS NULL );
653 $sth->execute($days) or die $dbh->errstr;
656 END { } # module clean-up code here (global destructor)