Bug 14826: Resurrect account offsets table
[koha.git] / C4 / Overdues.pm
1 package C4::Overdues;
2
3
4 # Copyright 2000-2002 Katipo Communications
5 # copyright 2010 BibLibre
6 #
7 # This file is part of Koha.
8 #
9 # Koha is free software; you can redistribute it and/or modify it
10 # under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # Koha is distributed in the hope that it will be useful, but
15 # WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with Koha; if not, see <http://www.gnu.org/licenses>.
21
22 use strict;
23 #use warnings; FIXME - Bug 2505
24 use Date::Calc qw/Today Date_to_Days/;
25 use Date::Manip qw/UnixDate/;
26 use List::MoreUtils qw( uniq );
27 use POSIX qw( floor ceil );
28 use Locale::Currency::Format 1.28;
29 use Carp;
30
31 use C4::Circulation;
32 use C4::Context;
33 use C4::Accounts;
34 use C4::Log; # logaction
35 use C4::Debug;
36 use Koha::DateUtils;
37 use Koha::Account::Line;
38 use Koha::Account::Lines;
39 use Koha::Account::Offset;
40 use Koha::IssuingRules;
41 use Koha::Libraries;
42
43 use vars qw(@ISA @EXPORT);
44
45 BEGIN {
46     require Exporter;
47     @ISA = qw(Exporter);
48
49     # subs to rename (and maybe merge some...)
50     push @EXPORT, qw(
51       &CalcFine
52       &Getoverdues
53       &checkoverdues
54       &NumberNotifyId
55       &AmountNotify
56       &UpdateFine
57       &GetFine
58       &get_chargeable_units
59       &CheckItemNotify
60       &GetOverduesForBranch
61       &RemoveNotifyLine
62       &AddNotifyLine
63       &GetOverdueMessageTransportTypes
64       &parse_overdues_letter
65     );
66
67     # subs to remove
68     push @EXPORT, qw(
69       &BorType
70     );
71
72     # check that an equivalent don't exist already before moving
73
74     # subs to move to Circulation.pm
75     push @EXPORT, qw(
76       &GetIssuesIteminfo
77     );
78
79     # subs to move to Biblio.pm
80     push @EXPORT, qw(
81       &GetItems
82     );
83 }
84
85 =head1 NAME
86
87 C4::Circulation::Fines - Koha module dealing with fines
88
89 =head1 SYNOPSIS
90
91   use C4::Overdues;
92
93 =head1 DESCRIPTION
94
95 This module contains several functions for dealing with fines for
96 overdue items. It is primarily used by the 'misc/fines2.pl' script.
97
98 =head1 FUNCTIONS
99
100 =head2 Getoverdues
101
102   $overdues = Getoverdues( { minimumdays => 1, maximumdays => 30 } );
103
104 Returns the list of all overdue books, with their itemtype.
105
106 C<$overdues> is a reference-to-array. Each element is a
107 reference-to-hash whose keys are the fields of the issues table in the
108 Koha database.
109
110 =cut
111
112 #'
113 sub Getoverdues {
114     my $params = shift;
115     my $dbh = C4::Context->dbh;
116     my $statement;
117     if ( C4::Context->preference('item-level_itypes') ) {
118         $statement = "
119    SELECT issues.*, items.itype as itemtype, items.homebranch, items.barcode, items.itemlost, items.replacementprice
120      FROM issues 
121 LEFT JOIN items       USING (itemnumber)
122     WHERE date_due < NOW()
123 ";
124     } else {
125         $statement = "
126    SELECT issues.*, biblioitems.itemtype, items.itype, items.homebranch, items.barcode, items.itemlost, replacementprice
127      FROM issues 
128 LEFT JOIN items       USING (itemnumber)
129 LEFT JOIN biblioitems USING (biblioitemnumber)
130     WHERE date_due < NOW()
131 ";
132     }
133
134     my @bind_parameters;
135     if ( exists $params->{'minimumdays'} and exists $params->{'maximumdays'} ) {
136         $statement .= ' AND TO_DAYS( NOW() )-TO_DAYS( date_due ) BETWEEN ? and ? ';
137         push @bind_parameters, $params->{'minimumdays'}, $params->{'maximumdays'};
138     } elsif ( exists $params->{'minimumdays'} ) {
139         $statement .= ' AND ( TO_DAYS( NOW() )-TO_DAYS( date_due ) ) > ? ';
140         push @bind_parameters, $params->{'minimumdays'};
141     } elsif ( exists $params->{'maximumdays'} ) {
142         $statement .= ' AND ( TO_DAYS( NOW() )-TO_DAYS( date_due ) ) < ? ';
143         push @bind_parameters, $params->{'maximumdays'};
144     }
145     $statement .= 'ORDER BY borrowernumber';
146     my $sth = $dbh->prepare( $statement );
147     $sth->execute( @bind_parameters );
148     return $sth->fetchall_arrayref({});
149 }
150
151
152 =head2 checkoverdues
153
154     ($count, $overdueitems) = checkoverdues($borrowernumber);
155
156 Returns a count and a list of overdueitems for a given borrowernumber
157
158 =cut
159
160 sub checkoverdues {
161     my $borrowernumber = shift or return;
162     my $sth = C4::Context->dbh->prepare(
163         "SELECT biblio.*, items.*, issues.*,
164                 biblioitems.volume,
165                 biblioitems.number,
166                 biblioitems.itemtype,
167                 biblioitems.isbn,
168                 biblioitems.issn,
169                 biblioitems.publicationyear,
170                 biblioitems.publishercode,
171                 biblioitems.volumedate,
172                 biblioitems.volumedesc,
173                 biblioitems.collectiontitle,
174                 biblioitems.collectionissn,
175                 biblioitems.collectionvolume,
176                 biblioitems.editionstatement,
177                 biblioitems.editionresponsibility,
178                 biblioitems.illus,
179                 biblioitems.pages,
180                 biblioitems.notes,
181                 biblioitems.size,
182                 biblioitems.place,
183                 biblioitems.lccn,
184                 biblioitems.url,
185                 biblioitems.cn_source,
186                 biblioitems.cn_class,
187                 biblioitems.cn_item,
188                 biblioitems.cn_suffix,
189                 biblioitems.cn_sort,
190                 biblioitems.totalissues
191          FROM issues
192          LEFT JOIN items       ON issues.itemnumber      = items.itemnumber
193          LEFT JOIN biblio      ON items.biblionumber     = biblio.biblionumber
194          LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
195             WHERE issues.borrowernumber  = ?
196             AND   issues.date_due < NOW()"
197     );
198     $sth->execute($borrowernumber);
199     my $results = $sth->fetchall_arrayref({});
200     return ( scalar(@$results), $results);  # returning the count and the results is silly
201 }
202
203 =head2 CalcFine
204
205     ($amount, $chargename,  $units_minus_grace, $chargeable_units) = &CalcFine($item,
206                                   $categorycode, $branch,
207                                   $start_dt, $end_dt );
208
209 Calculates the fine for a book.
210
211 The issuingrules table in the Koha database is a fine matrix, listing
212 the penalties for each type of patron for each type of item and each branch (e.g., the
213 standard fine for books might be $0.50, but $1.50 for DVDs, or staff
214 members might get a longer grace period between the first and second
215 reminders that a book is overdue).
216
217
218 C<$item> is an item object (hashref).
219
220 C<$categorycode> is the category code (string) of the patron who currently has
221 the book.
222
223 C<$branchcode> is the library (string) whose issuingrules govern this transaction.
224
225 C<$start_date> & C<$end_date> are DateTime objects
226 defining the date range over which to determine the fine.
227
228 Fines scripts should just supply the date range over which to calculate the fine.
229
230 C<&CalcFine> returns four values:
231
232 C<$amount> is the fine owed by the patron (see above).
233
234 C<$chargename> is the chargename field from the applicable record in
235 the categoryitem table, whatever that is.
236
237 C<$units_minus_grace> is the number of chargeable units minus the grace period
238
239 C<$chargeable_units> is the number of chargeable units (days between start and end dates, Calendar adjusted where needed,
240 minus any applicable grace period, or hours)
241
242 FIXME: previously attempted to return C<$message> as a text message, either "First Notice", "Second Notice",
243 or "Final Notice".  But CalcFine never defined any value.
244
245 =cut
246
247 sub CalcFine {
248     my ( $item, $bortype, $branchcode, $due_dt, $end_date  ) = @_;
249     my $start_date = $due_dt->clone();
250     # get issuingrules (fines part will be used)
251     my $itemtype = $item->{itemtype} || $item->{itype};
252     my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule({ categorycode => $bortype, itemtype => $itemtype, branchcode => $branchcode });
253
254     return unless $issuing_rule; # If not rule exist, there is no fine
255
256     my $fine_unit = $issuing_rule->lengthunit || 'days';
257
258     my $chargeable_units = get_chargeable_units($fine_unit, $start_date, $end_date, $branchcode);
259     my $units_minus_grace = $chargeable_units - $issuing_rule->firstremind;
260     my $amount = 0;
261     if ( $issuing_rule->chargeperiod && ( $units_minus_grace > 0 ) ) {
262         my $units = C4::Context->preference('FinesIncludeGracePeriod') ? $chargeable_units : $units_minus_grace;
263         my $charge_periods = $units / $issuing_rule->chargeperiod;
264         # If chargeperiod_charge_at = 1, we charge a fine at the start of each charge period
265         # if chargeperiod_charge_at = 0, we charge at the end of each charge period
266         $charge_periods = $issuing_rule->chargeperiod_charge_at == 1 ? ceil($charge_periods) : floor($charge_periods);
267         $amount = $charge_periods * $issuing_rule->fine;
268     } # else { # a zero (or null) chargeperiod or negative units_minus_grace value means no charge. }
269
270     $amount = $issuing_rule->overduefinescap if $issuing_rule->overduefinescap && $amount > $issuing_rule->overduefinescap;
271     $amount = $item->{replacementprice} if ( $issuing_rule->cap_fine_to_replacement_price && $item->{replacementprice} && $amount > $item->{replacementprice} );
272     $debug and warn sprintf("CalcFine returning (%s, %s, %s, %s)", $amount, $issuing_rule->chargename, $units_minus_grace, $chargeable_units);
273     return ($amount, $issuing_rule->chargename, $units_minus_grace, $chargeable_units);
274     # FIXME: chargename is NEVER populated anywhere.
275 }
276
277
278 =head2 get_chargeable_units
279
280     get_chargeable_units($unit, $start_date_ $end_date, $branchcode);
281
282 return integer value of units between C<$start_date> and C<$end_date>, factoring in holidays for C<$branchcode>.
283
284 C<$unit> is 'days' or 'hours' (default is 'days').
285
286 C<$start_date> and C<$end_date> are the two DateTimes to get the number of units between.
287
288 C<$branchcode> is the branch whose calendar to use for finding holidays.
289
290 =cut
291
292 sub get_chargeable_units {
293     my ($unit, $date_due, $date_returned, $branchcode) = @_;
294
295     # If the due date is later than the return date
296     return 0 unless ( $date_returned > $date_due );
297
298     my $charge_units = 0;
299     my $charge_duration;
300     if ($unit eq 'hours') {
301         if(C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed') {
302             my $calendar = Koha::Calendar->new( branchcode => $branchcode );
303             $charge_duration = $calendar->hours_between( $date_due, $date_returned );
304         } else {
305             $charge_duration = $date_returned->delta_ms( $date_due );
306         }
307         if($charge_duration->in_units('hours') == 0 && $charge_duration->in_units('seconds') > 0){
308             return 1;
309         }
310         return $charge_duration->in_units('hours');
311     }
312     else { # days
313         if(C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed') {
314             my $calendar = Koha::Calendar->new( branchcode => $branchcode );
315             $charge_duration = $calendar->days_between( $date_due, $date_returned );
316         } else {
317             $charge_duration = $date_returned->delta_days( $date_due );
318         }
319         return $charge_duration->in_units('days');
320     }
321 }
322
323
324 =head2 GetSpecialHolidays
325
326     &GetSpecialHolidays($date_dues,$itemnumber);
327
328 return number of special days  between date of the day and date due
329
330 C<$date_dues> is the envisaged date of book return.
331
332 C<$itemnumber> is the book's item number.
333
334 =cut
335
336 sub GetSpecialHolidays {
337     my ( $date_dues, $itemnumber ) = @_;
338
339     # calcul the today date
340     my $today = join "-", &Today();
341
342     # return the holdingbranch
343     my $iteminfo = GetIssuesIteminfo($itemnumber);
344
345     # use sql request to find all date between date_due and today
346     my $dbh = C4::Context->dbh;
347     my $query =
348       qq|SELECT DATE_FORMAT(concat(year,'-',month,'-',day),'%Y-%m-%d') as date
349 FROM `special_holidays`
350 WHERE DATE_FORMAT(concat(year,'-',month,'-',day),'%Y-%m-%d') >= ?
351 AND   DATE_FORMAT(concat(year,'-',month,'-',day),'%Y-%m-%d') <= ?
352 AND branchcode=?
353 |;
354     my @result = GetWdayFromItemnumber($itemnumber);
355     my @result_date;
356     my $wday;
357     my $dateinsec;
358     my $sth = $dbh->prepare($query);
359     $sth->execute( $date_dues, $today, $iteminfo->{'branchcode'} )
360       ;    # FIXME: just use NOW() in SQL instead of passing in $today
361
362     while ( my $special_date = $sth->fetchrow_hashref ) {
363         push( @result_date, $special_date );
364     }
365
366     my $specialdaycount = scalar(@result_date);
367
368     for ( my $i = 0 ; $i < scalar(@result_date) ; $i++ ) {
369         $dateinsec = UnixDate( $result_date[$i]->{'date'}, "%o" );
370         ( undef, undef, undef, undef, undef, undef, $wday, undef, undef ) =
371           localtime($dateinsec);
372         for ( my $j = 0 ; $j < scalar(@result) ; $j++ ) {
373             if ( $wday == ( $result[$j]->{'weekday'} ) ) {
374                 $specialdaycount--;
375             }
376         }
377     }
378
379     return $specialdaycount;
380 }
381
382 =head2 GetRepeatableHolidays
383
384     &GetRepeatableHolidays($date_dues, $itemnumber, $difference,);
385
386 return number of day closed between date of the day and date due
387
388 C<$date_dues> is the envisaged date of book return.
389
390 C<$itemnumber> is item number.
391
392 C<$difference> numbers of between day date of the day and date due
393
394 =cut
395
396 sub GetRepeatableHolidays {
397     my ( $date_dues, $itemnumber, $difference ) = @_;
398     my $dateinsec = UnixDate( $date_dues, "%o" );
399     my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
400       localtime($dateinsec);
401     my @result = GetWdayFromItemnumber($itemnumber);
402     my @dayclosedcount;
403     my $j;
404
405     for ( my $i = 0 ; $i < scalar(@result) ; $i++ ) {
406         my $k = $wday;
407
408         for ( $j = 0 ; $j < $difference ; $j++ ) {
409             if ( $result[$i]->{'weekday'} == $k ) {
410                 push( @dayclosedcount, $k );
411             }
412             $k++;
413             ( $k = 0 ) if ( $k eq 7 );
414         }
415     }
416     return scalar(@dayclosedcount);
417 }
418
419
420 =head2 GetWayFromItemnumber
421
422     &Getwdayfromitemnumber($itemnumber);
423
424 return the different week day from repeatable_holidays table
425
426 C<$itemnumber> is  item number.
427
428 =cut
429
430 sub GetWdayFromItemnumber {
431     my ($itemnumber) = @_;
432     my $iteminfo = GetIssuesIteminfo($itemnumber);
433     my @result;
434     my $query = qq|SELECT weekday
435     FROM repeatable_holidays
436     WHERE branchcode=?
437 |;
438     my $sth = C4::Context->dbh->prepare($query);
439
440     $sth->execute( $iteminfo->{'branchcode'} );
441     while ( my $weekday = $sth->fetchrow_hashref ) {
442         push( @result, $weekday );
443     }
444     return @result;
445 }
446
447
448 =head2 GetIssuesIteminfo
449
450     &GetIssuesIteminfo($itemnumber);
451
452 return all data from issues about item
453
454 C<$itemnumber> is  item number.
455
456 =cut
457
458 sub GetIssuesIteminfo {
459     my ($itemnumber) = @_;
460     my $dbh          = C4::Context->dbh;
461     my $query        = qq|SELECT *
462     FROM issues
463     WHERE itemnumber=?
464     |;
465     my $sth = $dbh->prepare($query);
466     $sth->execute($itemnumber);
467     my ($issuesinfo) = $sth->fetchrow_hashref;
468     return $issuesinfo;
469 }
470
471
472 =head2 UpdateFine
473
474     &UpdateFine({ issue_id => $issue_id, itemnumber => $itemnumber, borrwernumber => $borrowernumber, amount => $amount, type => $type, $due => $date_due });
475
476 (Note: the following is mostly conjecture and guesswork.)
477
478 Updates the fine owed on an overdue book.
479
480 C<$itemnumber> is the book's item number.
481
482 C<$borrowernumber> is the borrower number of the patron who currently
483 has the book on loan.
484
485 C<$amount> is the current amount owed by the patron.
486
487 C<$type> will be used in the description of the fine.
488
489 C<$due> is the due date formatted to the currently specified date format
490
491 C<&UpdateFine> looks up the amount currently owed on the given item
492 and sets it to C<$amount>, creating, if necessary, a new entry in the
493 accountlines table of the Koha database.
494
495 =cut
496
497 #
498 # Question: Why should the caller have to
499 # specify both the item number and the borrower number? A book can't
500 # be on loan to two different people, so the item number should be
501 # sufficient.
502 #
503 # Possible Answer: You might update a fine for a damaged item, *after* it is returned.
504 #
505 sub UpdateFine {
506     my ($params) = @_;
507
508     my $issue_id       = $params->{issue_id};
509     my $itemnum        = $params->{itemnumber};
510     my $borrowernumber = $params->{borrowernumber};
511     my $amount         = $params->{amount};
512     my $type           = $params->{type};
513     my $due            = $params->{due};
514
515     $debug and warn "UpdateFine({ itemnumber => $itemnum, borrowernumber => $borrowernumber, type => $type, due => $due, issue_id => $issue_id})";
516
517     unless ( $issue_id ) {
518         carp("No issue_id passed in!");
519         return;
520     }
521
522     my $dbh = C4::Context->dbh;
523     # FIXME - What exactly is this query supposed to do? It looks up an
524     # entry in accountlines that matches the given item and borrower
525     # numbers, where the description contains $due, and where the
526     # account type has one of several values, but what does this _mean_?
527     # Does it look up existing fines for this item?
528     # FIXME - What are these various account types? ("FU", "O", "F", "M")
529     #   "L"   is LOST item
530     #   "A"   is Account Management Fee
531     #   "N"   is New Card
532     #   "M"   is Sundry
533     #   "O"   is Overdue ??
534     #   "F"   is Fine ??
535     #   "FU"  is Fine UPDATE??
536     #   "Pay" is Payment
537     #   "REF" is Cash Refund
538     my $sth = $dbh->prepare(
539         "SELECT * FROM accountlines
540         WHERE borrowernumber=? AND
541         (( accounttype IN ('O','F','M') AND amountoutstanding<>0 ) OR
542            accounttype = 'FU' )"
543     );
544     $sth->execute( $borrowernumber );
545     my $data;
546     my $total_amount_other = 0.00;
547     my $due_qr = qr/$due/;
548     # Cycle through the fines and
549     # - find line that relates to the requested $itemnum
550     # - accumulate fines for other items
551     # so we can update $itemnum fine taking in account fine caps
552     while (my $rec = $sth->fetchrow_hashref) {
553         if ( $rec->{issue_id} == $issue_id && $rec->{accounttype} eq 'FU' ) {
554             if ($data) {
555                 warn "Not a unique accountlines record for issue_id $issue_id";
556                 #FIXME Should we still count this one in total_amount ??
557             }
558             else {
559                 $data = $rec;
560                 next;
561             }
562         }
563         $total_amount_other += $rec->{'amountoutstanding'};
564     }
565
566     if (my $maxfine = C4::Context->preference('MaxFine')) {
567         if ($total_amount_other + $amount > $maxfine) {
568             my $new_amount = $maxfine - $total_amount_other;
569             return if $new_amount <= 0.00;
570             warn "Reducing fine for item $itemnum borrower $borrowernumber from $amount to $new_amount - MaxFine reached";
571             $amount = $new_amount;
572         }
573     }
574
575     if ( $data ) {
576         # we're updating an existing fine.  Only modify if amount changed
577         # Note that in the current implementation, you cannot pay against an accruing fine
578         # (i.e. , of accounttype 'FU').  Doing so will break accrual.
579         if ( $data->{'amount'} != $amount ) {
580             my $accountline = Koha::Account::Lines->find( $data->{accountlines_id} );
581             my $diff = $amount - $data->{'amount'};
582
583             #3341: diff could be positive or negative!
584             my $out   = $data->{'amountoutstanding'} + $diff;
585
586             $accountline->set(
587                 {
588                     date          => dt_from_string(),
589                     amount        => $amount,
590                     amountoutstanding   => $out,
591                     lastincrement => $diff,
592                     accounttype   => 'FU',
593                 }
594             )->store();
595
596             Koha::Account::Offset->new(
597                 {
598                     debit_id => $accountline->id,
599                     type     => 'Fine Update',
600                     amount   => $diff,
601                 }
602             )->store();
603         }
604     } else {
605         if ( $amount ) { # Don't add new fines with an amount of 0
606             my $sth4 = $dbh->prepare(
607                 "SELECT title FROM biblio LEFT JOIN items ON biblio.biblionumber=items.biblionumber WHERE items.itemnumber=?"
608             );
609             $sth4->execute($itemnum);
610             my $title = $sth4->fetchrow;
611
612             my $nextaccntno = C4::Accounts::getnextacctno($borrowernumber);
613
614             my $desc = ( $type ? "$type " : '' ) . "$title $due";    # FIXEDME, avoid whitespace prefix on empty $type
615
616             my $accountline = Koha::Account::Line->new(
617                 {
618                     borrowernumber    => $borrowernumber,
619                     itemnumber        => $itemnum,
620                     date              => dt_from_string(),
621                     amount            => $amount,
622                     description       => $desc,
623                     accounttype       => 'FU',
624                     amountoutstanding => $amount,
625                     lastincrement     => $amount,
626                     accountno         => $nextaccntno,
627                     issue_id          => $issue_id,
628                 }
629             )->store();
630
631             Koha::Account::Offset->new(
632                 {
633                     debit_id => $accountline->id,
634                     type     => 'Fine',
635                     amount   => $amount,
636                 }
637             )->store();
638         }
639     }
640     # logging action
641     &logaction(
642         "FINES",
643         $type,
644         $borrowernumber,
645         "due=".$due."  amount=".$amount." itemnumber=".$itemnum
646         ) if C4::Context->preference("FinesLog");
647 }
648
649 =head2 BorType
650
651     $borrower = &BorType($borrowernumber);
652
653 Looks up a patron by borrower number.
654
655 C<$borrower> is a reference-to-hash whose keys are all of the fields
656 from the borrowers and categories tables of the Koha database. Thus,
657 C<$borrower> contains all information about both the borrower and
658 category they belong to.
659
660 =cut
661
662 sub BorType {
663     my ($borrowernumber) = @_;
664     my $dbh              = C4::Context->dbh;
665     my $sth              = $dbh->prepare(
666         "SELECT * from borrowers
667       LEFT JOIN categories ON borrowers.categorycode=categories.categorycode 
668       WHERE borrowernumber=?"
669     );
670     $sth->execute($borrowernumber);
671     return $sth->fetchrow_hashref;
672 }
673
674 =head2 GetFine
675
676     $data->{'sum(amountoutstanding)'} = &GetFine($itemnum,$borrowernumber);
677
678 return the total of fine
679
680 C<$itemnum> is item number
681
682 C<$borrowernumber> is the borrowernumber
683
684 =cut 
685
686 sub GetFine {
687     my ( $itemnum, $borrowernumber ) = @_;
688     my $dbh   = C4::Context->dbh();
689     my $query = q|SELECT sum(amountoutstanding) as fineamount FROM accountlines
690     where accounttype like 'F%'
691   AND amountoutstanding > 0 AND borrowernumber=?|;
692     my @query_param;
693     push @query_param, $borrowernumber;
694     if (defined $itemnum )
695     {
696         $query .= " AND itemnumber=?";
697         push @query_param, $itemnum;
698     }
699     my $sth = $dbh->prepare($query);
700     $sth->execute( @query_param );
701     my $fine = $sth->fetchrow_hashref();
702     if ($fine->{fineamount}) {
703         return $fine->{fineamount};
704     }
705     return 0;
706 }
707
708 =head2 NumberNotifyId
709
710     (@notify) = &NumberNotifyId($borrowernumber);
711
712 Returns amount for all file per borrowers
713 C<@notify> array contains all file per borrowers
714
715 C<$notify_id> contains the file number for the borrower number nad item number
716
717 =cut
718
719 sub NumberNotifyId{
720     my ($borrowernumber)=@_;
721     my $dbh = C4::Context->dbh;
722     my $query=qq|    SELECT distinct(notify_id)
723             FROM accountlines
724             WHERE borrowernumber=?|;
725     my @notify;
726     my $sth = $dbh->prepare($query);
727     $sth->execute($borrowernumber);
728     while ( my ($numberofnotify) = $sth->fetchrow ) {
729         push( @notify, $numberofnotify );
730     }
731     return (@notify);
732 }
733
734 =head2 AmountNotify
735
736     ($totalnotify) = &AmountNotify($notifyid);
737
738 Returns amount for all file per borrowers
739 C<$notifyid> is the file number
740
741 C<$totalnotify> contains amount of a file
742
743 C<$notify_id> contains the file number for the borrower number and item number
744
745 =cut
746
747 sub AmountNotify{
748     my ($notifyid,$borrowernumber)=@_;
749     my $dbh = C4::Context->dbh;
750     my $query=qq|    SELECT sum(amountoutstanding)
751             FROM accountlines
752             WHERE notify_id=? AND borrowernumber = ?|;
753     my $sth=$dbh->prepare($query);
754         $sth->execute($notifyid,$borrowernumber);
755         my $totalnotify=$sth->fetchrow;
756     $sth->finish;
757     return ($totalnotify);
758 }
759
760 =head2 GetItems
761
762     ($items) = &GetItems($itemnumber);
763
764 Returns the list of all delays from overduerules.
765
766 C<$items> is a reference-to-hash whose keys are all of the fields
767 from the items tables of the Koha database. Thus,
768
769 C<$itemnumber> contains the borrower categorycode
770
771 =cut
772
773 # FIXME: This is a bad function to have here.
774 # Shouldn't it be in C4::Items?
775 # Shouldn't it be called GetItem since you only get 1 row?
776 # Shouldn't it be called GetItem since you give it only 1 itemnumber?
777
778 sub GetItems {
779     my $itemnumber = shift or return;
780     my $query = qq|SELECT *
781              FROM items
782               WHERE itemnumber=?|;
783     my $sth = C4::Context->dbh->prepare($query);
784     $sth->execute($itemnumber);
785     my ($items) = $sth->fetchrow_hashref;
786     return ($items);
787 }
788
789 =head2 GetBranchcodesWithOverdueRules
790
791     my @branchcodes = C4::Overdues::GetBranchcodesWithOverdueRules()
792
793 returns a list of branch codes for branches with overdue rules defined.
794
795 =cut
796
797 sub GetBranchcodesWithOverdueRules {
798     my $dbh               = C4::Context->dbh;
799     my $branchcodes = $dbh->selectcol_arrayref(q|
800         SELECT DISTINCT(branchcode)
801         FROM overduerules
802         WHERE delay1 IS NOT NULL
803         ORDER BY branchcode
804     |);
805     if ( $branchcodes->[0] eq '' ) {
806         # If a default rule exists, all branches should be returned
807         return map { $_->branchcode } Koha::Libraries->search({}, { order_by => 'branchname' });
808     }
809     return @$branchcodes;
810 }
811
812 =head2 CheckItemNotify
813
814 Sql request to check if the document has alreday been notified
815 this function is not exported, only used with GetOverduesForBranch
816
817 =cut
818
819 sub CheckItemNotify {
820     my ($notify_id,$notify_level,$itemnumber) = @_;
821     my $dbh = C4::Context->dbh;
822     my $sth = $dbh->prepare("
823     SELECT COUNT(*)
824      FROM notifys
825     WHERE notify_id    = ?
826      AND  notify_level = ? 
827      AND  itemnumber   = ? ");
828     $sth->execute($notify_id,$notify_level,$itemnumber);
829     my $notified = $sth->fetchrow;
830     return ($notified);
831 }
832
833 =head2 GetOverduesForBranch
834
835 Sql request for display all information for branchoverdues.pl
836 2 possibilities : with or without location .
837 display is filtered by branch
838
839 FIXME: This function should be renamed.
840
841 =cut
842
843 sub GetOverduesForBranch {
844     my ( $branch, $location) = @_;
845         my $itype_link =  (C4::Context->preference('item-level_itypes')) ?  " items.itype " :  " biblioitems.itemtype ";
846     my $dbh = C4::Context->dbh;
847     my $select = "
848     SELECT
849             borrowers.cardnumber,
850             borrowers.borrowernumber,
851             borrowers.surname,
852             borrowers.firstname,
853             borrowers.phone,
854             borrowers.email,
855                biblio.title,
856                biblio.author,
857                biblio.biblionumber,
858                issues.date_due,
859                issues.returndate,
860                issues.branchcode,
861              branches.branchname,
862                 items.barcode,
863                 items.homebranch,
864                 items.itemcallnumber,
865                 items.location,
866                 items.itemnumber,
867             itemtypes.description,
868          accountlines.notify_id,
869          accountlines.notify_level,
870          accountlines.amountoutstanding
871     FROM  accountlines
872     LEFT JOIN issues      ON    issues.itemnumber     = accountlines.itemnumber
873                           AND   issues.borrowernumber = accountlines.borrowernumber
874     LEFT JOIN borrowers   ON borrowers.borrowernumber = accountlines.borrowernumber
875     LEFT JOIN items       ON     items.itemnumber     = issues.itemnumber
876     LEFT JOIN biblio      ON      biblio.biblionumber =  items.biblionumber
877     LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
878     LEFT JOIN itemtypes   ON itemtypes.itemtype       = $itype_link
879     LEFT JOIN branches    ON  branches.branchcode     = issues.branchcode
880     WHERE (accountlines.amountoutstanding  != '0.000000')
881       AND (accountlines.accounttype         = 'FU'      )
882       AND (issues.branchcode =  ?   )
883       AND (issues.date_due  < NOW())
884     ";
885     my @getoverdues;
886     my $i = 0;
887     my $sth;
888     if ($location) {
889         $sth = $dbh->prepare("$select AND items.location = ? ORDER BY borrowers.surname, borrowers.firstname");
890         $sth->execute($branch, $location);
891     } else {
892         $sth = $dbh->prepare("$select ORDER BY borrowers.surname, borrowers.firstname");
893         $sth->execute($branch);
894     }
895     while ( my $data = $sth->fetchrow_hashref ) {
896     #check if the document has already been notified
897         my $countnotify = CheckItemNotify($data->{'notify_id'}, $data->{'notify_level'}, $data->{'itemnumber'});
898         if ($countnotify eq '0') {
899             $getoverdues[$i] = $data;
900             $i++;
901         }
902     }
903     return (@getoverdues);
904 }
905
906
907 =head2 AddNotifyLine
908
909     &AddNotifyLine($borrowernumber, $itemnumber, $overduelevel, $method, $notifyId)
910
911 Create a line into notify, if the method is phone, the notification_send_date is implemented to
912
913 =cut
914
915 sub AddNotifyLine {
916     my ( $borrowernumber, $itemnumber, $overduelevel, $method, $notifyId ) = @_;
917     my $dbh = C4::Context->dbh;
918     if ( $method eq "phone" ) {
919         my $sth = $dbh->prepare(
920             "INSERT INTO notifys (borrowernumber,itemnumber,notify_date,notify_send_date,notify_level,method,notify_id)
921         VALUES (?,?,now(),now(),?,?,?)"
922         );
923         $sth->execute( $borrowernumber, $itemnumber, $overduelevel, $method,
924             $notifyId );
925     }
926     else {
927         my $sth = $dbh->prepare(
928             "INSERT INTO notifys (borrowernumber,itemnumber,notify_date,notify_level,method,notify_id)
929         VALUES (?,?,now(),?,?,?)"
930         );
931         $sth->execute( $borrowernumber, $itemnumber, $overduelevel, $method,
932             $notifyId );
933     }
934     return 1;
935 }
936
937 =head2 RemoveNotifyLine
938
939     &RemoveNotifyLine( $borrowernumber, $itemnumber, $notify_date );
940
941 Cancel a notification
942
943 =cut
944
945 sub RemoveNotifyLine {
946     my ( $borrowernumber, $itemnumber, $notify_date ) = @_;
947     my $dbh = C4::Context->dbh;
948     my $sth = $dbh->prepare(
949         "DELETE FROM notifys 
950             WHERE
951             borrowernumber=?
952             AND itemnumber=?
953             AND notify_date=?"
954     );
955     $sth->execute( $borrowernumber, $itemnumber, $notify_date );
956     return 1;
957 }
958
959 =head2 GetOverdueMessageTransportTypes
960
961     my $message_transport_types = GetOverdueMessageTransportTypes( $branchcode, $categorycode, $letternumber);
962
963     return a arrayref with all message_transport_type for given branchcode, categorycode and letternumber(1,2 or 3)
964
965 =cut
966
967 sub GetOverdueMessageTransportTypes {
968     my ( $branchcode, $categorycode, $letternumber ) = @_;
969     return unless $categorycode and $letternumber;
970     my $dbh = C4::Context->dbh;
971     my $sth = $dbh->prepare("
972         SELECT message_transport_type
973         FROM overduerules odr LEFT JOIN overduerules_transport_types ott USING (overduerules_id)
974         WHERE branchcode = ?
975           AND categorycode = ?
976           AND letternumber = ?
977     ");
978     $sth->execute( $branchcode, $categorycode, $letternumber );
979     my @mtts;
980     while ( my $mtt = $sth->fetchrow ) {
981         push @mtts, $mtt;
982     }
983
984     # Put 'print' in first if exists
985     # It avoid to sent a print notice with an email or sms template is no email or sms is defined
986     @mtts = uniq( 'print', @mtts )
987         if grep {/^print$/} @mtts;
988
989     return \@mtts;
990 }
991
992 =head2 parse_overdues_letter
993
994 parses the letter template, replacing the placeholders with data
995 specific to this patron, biblio, or item for overdues
996
997 named parameters:
998   letter - required hashref
999   borrowernumber - required integer
1000   substitute - optional hashref of other key/value pairs that should
1001     be substituted in the letter content
1002
1003 returns the C<letter> hashref, with the content updated to reflect the
1004 substituted keys and values.
1005
1006 =cut
1007
1008 sub parse_overdues_letter {
1009     my $params = shift;
1010     foreach my $required (qw( letter_code borrowernumber )) {
1011         return unless ( exists $params->{$required} && $params->{$required} );
1012     }
1013
1014     my $patron = Koha::Patrons->find( $params->{borrowernumber} );
1015
1016     my $substitute = $params->{'substitute'} || {};
1017
1018     my %tables = ( 'borrowers' => $params->{'borrowernumber'} );
1019     if ( my $p = $params->{'branchcode'} ) {
1020         $tables{'branches'} = $p;
1021     }
1022
1023     my $active_currency = Koha::Acquisition::Currencies->get_active;
1024
1025     my $currency_format;
1026     $currency_format = $active_currency->currency if defined($active_currency);
1027
1028     my @item_tables;
1029     if ( my $i = $params->{'items'} ) {
1030         foreach my $item (@$i) {
1031             my $fine = GetFine($item->{'itemnumber'}, $params->{'borrowernumber'});
1032             $item->{'fine'} = currency_format($currency_format, "$fine", FMT_SYMBOL);
1033             # if active currency isn't correct ISO code fallback to sprintf
1034             $item->{'fine'} = sprintf('%.2f', $fine) unless $item->{'fine'};
1035
1036             push @item_tables, {
1037                 'biblio' => $item->{'biblionumber'},
1038                 'biblioitems' => $item->{'biblionumber'},
1039                 'items' => $item,
1040                 'issues' => $item->{'itemnumber'},
1041             };
1042         }
1043     }
1044
1045     return C4::Letters::GetPreparedLetter (
1046         module => 'circulation',
1047         letter_code => $params->{'letter_code'},
1048         branchcode => $params->{'branchcode'},
1049         lang => $patron->lang,
1050         tables => \%tables,
1051         loops => {
1052             overdues => [ map { $_->{items}->{itemnumber} } @item_tables ],
1053         },
1054         substitute => $substitute,
1055         repeat => { item => \@item_tables },
1056         message_transport_type => $params->{message_transport_type},
1057     );
1058 }
1059
1060 1;
1061 __END__
1062
1063 =head1 AUTHOR
1064
1065 Koha Development Team <http://koha-community.org/>
1066
1067 =cut