Bug 28854: Record and display who lost the item
[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 Modern::Perl;
23 use Date::Calc qw( Today );
24 use Date::Manip qw( UnixDate );
25 use List::MoreUtils qw( uniq );
26 use POSIX qw( ceil floor );
27 use Locale::Currency::Format 1.28 qw( currency_format FMT_SYMBOL );
28 use Carp qw( carp );
29
30 use C4::Accounts;
31 use C4::Context;
32 use Koha::Account::Lines;
33 use Koha::Account::Offsets;
34 use Koha::Libraries;
35 use Koha::Recalls;
36 use Koha::Logger;
37 use Koha::Patrons;
38
39 our (@ISA, @EXPORT_OK);
40 BEGIN {
41     require Exporter;
42     @ISA = qw(Exporter);
43
44     # subs to rename (and maybe merge some...)
45     @EXPORT_OK = qw(
46       CalcFine
47       Getoverdues
48       checkoverdues
49       UpdateFine
50       GetFine
51       GetBranchcodesWithOverdueRules
52       get_chargeable_units
53       GetOverduesForBranch
54       GetOverdueMessageTransportTypes
55       parse_overdues_letter
56       GetIssuesIteminfo
57     );
58 }
59
60 =head1 NAME
61
62 C4::Circulation::Fines - Koha module dealing with fines
63
64 =head1 SYNOPSIS
65
66   use C4::Overdues;
67
68 =head1 DESCRIPTION
69
70 This module contains several functions for dealing with fines for
71 overdue items. It is primarily used by the 'misc/fines2.pl' script.
72
73 =head1 FUNCTIONS
74
75 =head2 Getoverdues
76
77   $overdues = Getoverdues( { minimumdays => 1, maximumdays => 30 } );
78
79 Returns the list of all overdue books, with their itemtype.
80
81 C<$overdues> is a reference-to-array. Each element is a
82 reference-to-hash whose keys are the fields of the issues table in the
83 Koha database.
84
85 =cut
86
87 #'
88 sub Getoverdues {
89     my $params = shift;
90     my $dbh = C4::Context->dbh;
91     my $statement;
92     if ( C4::Context->preference('item-level_itypes') ) {
93         $statement = "
94    SELECT issues.*, items.itype as itemtype, items.homebranch, items.barcode, items.itemlost, items.replacementprice, items.biblionumber
95      FROM issues 
96 LEFT JOIN items       USING (itemnumber)
97     WHERE date_due < NOW()
98 ";
99     } else {
100         $statement = "
101    SELECT issues.*, biblioitems.itemtype, items.itype, items.homebranch, items.barcode, items.itemlost, replacementprice, items.biblionumber
102      FROM issues 
103 LEFT JOIN items       USING (itemnumber)
104 LEFT JOIN biblioitems USING (biblioitemnumber)
105     WHERE date_due < NOW()
106 ";
107     }
108
109     my @bind_parameters;
110     if ( exists $params->{'minimumdays'} and exists $params->{'maximumdays'} ) {
111         $statement .= ' AND TO_DAYS( NOW() )-TO_DAYS( date_due ) BETWEEN ? and ? ';
112         push @bind_parameters, $params->{'minimumdays'}, $params->{'maximumdays'};
113     } elsif ( exists $params->{'minimumdays'} ) {
114         $statement .= ' AND ( TO_DAYS( NOW() )-TO_DAYS( date_due ) ) > ? ';
115         push @bind_parameters, $params->{'minimumdays'};
116     } elsif ( exists $params->{'maximumdays'} ) {
117         $statement .= ' AND ( TO_DAYS( NOW() )-TO_DAYS( date_due ) ) < ? ';
118         push @bind_parameters, $params->{'maximumdays'};
119     }
120     $statement .= 'ORDER BY borrowernumber';
121     my $sth = $dbh->prepare( $statement );
122     $sth->execute( @bind_parameters );
123     return $sth->fetchall_arrayref({});
124 }
125
126
127 =head2 checkoverdues
128
129     ($count, $overdueitems) = checkoverdues($borrowernumber);
130
131 Returns a count and a list of overdueitems for a given borrowernumber
132
133 =cut
134
135 sub checkoverdues {
136     my $borrowernumber = shift or return;
137     my $sth = C4::Context->dbh->prepare(
138         "SELECT biblio.*, items.*, issues.*,
139                 biblioitems.volume,
140                 biblioitems.number,
141                 biblioitems.itemtype,
142                 biblioitems.isbn,
143                 biblioitems.issn,
144                 biblioitems.publicationyear,
145                 biblioitems.publishercode,
146                 biblioitems.volumedate,
147                 biblioitems.volumedesc,
148                 biblioitems.collectiontitle,
149                 biblioitems.collectionissn,
150                 biblioitems.collectionvolume,
151                 biblioitems.editionstatement,
152                 biblioitems.editionresponsibility,
153                 biblioitems.illus,
154                 biblioitems.pages,
155                 biblioitems.notes,
156                 biblioitems.size,
157                 biblioitems.place,
158                 biblioitems.lccn,
159                 biblioitems.url,
160                 biblioitems.cn_source,
161                 biblioitems.cn_class,
162                 biblioitems.cn_item,
163                 biblioitems.cn_suffix,
164                 biblioitems.cn_sort,
165                 biblioitems.totalissues
166          FROM issues
167          LEFT JOIN items       ON issues.itemnumber      = items.itemnumber
168          LEFT JOIN biblio      ON items.biblionumber     = biblio.biblionumber
169          LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
170             WHERE issues.borrowernumber  = ?
171             AND   issues.date_due < NOW()"
172     );
173     $sth->execute($borrowernumber);
174     my $results = $sth->fetchall_arrayref({});
175     return ( scalar(@$results), $results);  # returning the count and the results is silly
176 }
177
178 =head2 CalcFine
179
180     ($amount, $units_minus_grace, $chargeable_units) = &CalcFine($item,
181                                   $categorycode, $branch,
182                                   $start_dt, $end_dt );
183
184 Calculates the fine for a book.
185
186 The issuingrules table in the Koha database is a fine matrix, listing
187 the penalties for each type of patron for each type of item and each branch (e.g., the
188 standard fine for books might be $0.50, but $1.50 for DVDs, or staff
189 members might get a longer grace period between the first and second
190 reminders that a book is overdue).
191
192
193 C<$item> is an item object (hashref).
194
195 C<$categorycode> is the category code (string) of the patron who currently has
196 the book.
197
198 C<$branchcode> is the library (string) whose issuingrules govern this transaction.
199
200 C<$start_date> & C<$end_date> are DateTime objects
201 defining the date range over which to determine the fine.
202
203 Fines scripts should just supply the date range over which to calculate the fine.
204
205 C<&CalcFine> returns three values:
206
207 C<$amount> is the fine owed by the patron (see above).
208
209 C<$units_minus_grace> is the number of chargeable units minus the grace period
210
211 C<$chargeable_units> is the number of chargeable units (days between start and end dates, Calendar adjusted where needed,
212 minus any applicable grace period, or hours)
213
214 FIXME: previously attempted to return C<$message> as a text message, either "First Notice", "Second Notice",
215 or "Final Notice".  But CalcFine never defined any value.
216
217 =cut
218
219 sub CalcFine {
220     my ( $item, $bortype, $branchcode, $due_dt, $end_date  ) = @_;
221
222     # Skip calculations if item is not overdue
223     return ( 0, 0, 0 ) unless (DateTime->compare( $due_dt, $end_date ) == -1);
224
225     my $start_date = $due_dt->clone();
226     # get issuingrules (fines part will be used)
227     my $itemtype = $item->{itemtype} || $item->{itype};
228     my $issuing_rule = Koha::CirculationRules->get_effective_rules(
229         {
230             categorycode => $bortype,
231             itemtype     => $itemtype,
232             branchcode   => $branchcode,
233             rules => [
234                 'lengthunit',
235                 'firstremind',
236                 'chargeperiod',
237                 'chargeperiod_charge_at',
238                 'fine',
239                 'overduefinescap',
240                 'cap_fine_to_replacement_price',
241                 'recall_overdue_fine',
242             ]
243         }
244     );
245
246     $itemtype = Koha::ItemTypes->find($itemtype);
247
248     return unless $issuing_rule; # If not rule exist, there is no fine
249
250     my $fine_unit = $issuing_rule->{lengthunit} || 'days';
251
252     my $chargeable_units = get_chargeable_units($fine_unit, $start_date, $end_date, $branchcode);
253     my $units_minus_grace = $chargeable_units - ($issuing_rule->{firstremind} || 0);
254     my $amount = 0;
255     if ( $issuing_rule->{chargeperiod} && ( $units_minus_grace > 0 ) ) {
256         my $units = C4::Context->preference('FinesIncludeGracePeriod') ? $chargeable_units : $units_minus_grace;
257         my $charge_periods = $units / $issuing_rule->{chargeperiod};
258         # If chargeperiod_charge_at = 1, we charge a fine at the start of each charge period
259         # if chargeperiod_charge_at = 0, we charge at the end of each charge period
260         $charge_periods = defined $issuing_rule->{chargeperiod_charge_at} && $issuing_rule->{chargeperiod_charge_at} == 1 ? ceil($charge_periods) : floor($charge_periods);
261
262         # check if item has been recalled. recall should have been marked Overdue by cronjob, so only look at overdue recalls
263         # only charge using recall_overdue_fine if there is an item-level recall for this particular item, OR a biblio-level recall
264         my @recalls = Koha::Recalls->search({ biblio_id => $item->{biblionumber}, status => 'overdue' })->as_list;
265         my $bib_level_recall = 0;
266         $bib_level_recall = 1 if scalar @recalls > 0;
267         foreach my $recall ( @recalls ) {
268             if ( $recall->item_level and $recall->item_id == $item->{itemnumber} and $issuing_rule->{recall_overdue_fine} ) {
269                 $bib_level_recall = 0;
270                 $amount = $charge_periods * $issuing_rule->{recall_overdue_fine};
271                 last;
272             }
273         }
274         if ( $bib_level_recall and $issuing_rule->{recall_overdue_fine} ) {
275             # biblio-level recall
276             $amount = $charge_periods * $issuing_rule->{recall_overdue_fine};
277         }
278         if ( scalar @recalls == 0 && $issuing_rule->{fine}) {
279             # no recall, use normal fine amount
280             $amount = $charge_periods * $issuing_rule->{fine};
281         }
282     } # else { # a zero (or null) chargeperiod or negative units_minus_grace value means no charge. }
283
284     $amount = $issuing_rule->{overduefinescap} if $issuing_rule->{overduefinescap} && $amount > $issuing_rule->{overduefinescap};
285
286     # This must be moved to Koha::Item (see also similar code in C4::Accounts::chargelostitem
287     $item->{replacementprice} ||= $itemtype->defaultreplacecost
288       if $itemtype
289       && ( ! defined $item->{replacementprice} || $item->{replacementprice} == 0 )
290       && C4::Context->preference("useDefaultReplacementCost");
291
292     $amount = $item->{replacementprice} if ( $issuing_rule->{cap_fine_to_replacement_price} && $item->{replacementprice} && $amount > $item->{replacementprice} );
293
294     return ($amount, $units_minus_grace, $chargeable_units);
295 }
296
297
298 =head2 get_chargeable_units
299
300     get_chargeable_units($unit, $start_date_ $end_date, $branchcode);
301
302 return integer value of units between C<$start_date> and C<$end_date>, factoring in holidays for C<$branchcode>.
303
304 C<$unit> is 'days' or 'hours' (default is 'days').
305
306 C<$start_date> and C<$end_date> are the two DateTimes to get the number of units between.
307
308 C<$branchcode> is the branch whose calendar to use for finding holidays.
309
310 =cut
311
312 sub get_chargeable_units {
313     my ($unit, $date_due, $date_returned, $branchcode) = @_;
314
315     # If the due date is later than the return date
316     return 0 unless ( $date_returned > $date_due );
317
318     my $charge_units = 0;
319     my $charge_duration;
320     if ($unit eq 'hours') {
321         if(C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed') {
322             my $calendar = Koha::Calendar->new( branchcode => $branchcode );
323             $charge_duration = $calendar->hours_between( $date_due, $date_returned );
324         } else {
325             $charge_duration = $date_returned->delta_ms( $date_due );
326         }
327         if($charge_duration->in_units('hours') == 0 && $charge_duration->in_units('seconds') > 0){
328             return 1;
329         }
330         return $charge_duration->in_units('hours');
331     }
332     else { # days
333         if(C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed') {
334             my $calendar = Koha::Calendar->new( branchcode => $branchcode );
335             $charge_duration = $calendar->days_between( $date_due, $date_returned );
336         } else {
337             $charge_duration = $date_returned->delta_days( $date_due );
338         }
339         return $charge_duration->in_units('days');
340     }
341 }
342
343
344 =head2 GetSpecialHolidays
345
346     &GetSpecialHolidays($date_dues,$itemnumber);
347
348 return number of special days  between date of the day and date due
349
350 C<$date_dues> is the envisaged date of book return.
351
352 C<$itemnumber> is the book's item number.
353
354 =cut
355
356 sub GetSpecialHolidays {
357     my ( $date_dues, $itemnumber ) = @_;
358
359     # calcul the today date
360     my $today = join "-", &Today();
361
362     # return the holdingbranch
363     my $iteminfo = GetIssuesIteminfo($itemnumber);
364
365     # use sql request to find all date between date_due and today
366     my $dbh = C4::Context->dbh;
367     my $query =
368       qq|SELECT DATE_FORMAT(concat(year,'-',month,'-',day),'%Y-%m-%d') as date
369 FROM `special_holidays`
370 WHERE DATE_FORMAT(concat(year,'-',month,'-',day),'%Y-%m-%d') >= ?
371 AND   DATE_FORMAT(concat(year,'-',month,'-',day),'%Y-%m-%d') <= ?
372 AND branchcode=?
373 |;
374     my @result = GetWdayFromItemnumber($itemnumber);
375     my @result_date;
376     my $wday;
377     my $dateinsec;
378     my $sth = $dbh->prepare($query);
379     $sth->execute( $date_dues, $today, $iteminfo->{'branchcode'} )
380       ;    # FIXME: just use NOW() in SQL instead of passing in $today
381
382     while ( my $special_date = $sth->fetchrow_hashref ) {
383         push( @result_date, $special_date );
384     }
385
386     my $specialdaycount = scalar(@result_date);
387
388     for ( my $i = 0 ; $i < scalar(@result_date) ; $i++ ) {
389         $dateinsec = UnixDate( $result_date[$i]->{'date'}, "%o" );
390         ( undef, undef, undef, undef, undef, undef, $wday, undef, undef ) =
391           localtime($dateinsec);
392         for ( my $j = 0 ; $j < scalar(@result) ; $j++ ) {
393             if ( $wday == ( $result[$j]->{'weekday'} ) ) {
394                 $specialdaycount--;
395             }
396         }
397     }
398
399     return $specialdaycount;
400 }
401
402 =head2 GetRepeatableHolidays
403
404     &GetRepeatableHolidays($date_dues, $itemnumber, $difference,);
405
406 return number of day closed between date of the day and date due
407
408 C<$date_dues> is the envisaged date of book return.
409
410 C<$itemnumber> is item number.
411
412 C<$difference> numbers of between day date of the day and date due
413
414 =cut
415
416 sub GetRepeatableHolidays {
417     my ( $date_dues, $itemnumber, $difference ) = @_;
418     my $dateinsec = UnixDate( $date_dues, "%o" );
419     my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
420       localtime($dateinsec);
421     my @result = GetWdayFromItemnumber($itemnumber);
422     my @dayclosedcount;
423     my $j;
424
425     for ( my $i = 0 ; $i < scalar(@result) ; $i++ ) {
426         my $k = $wday;
427
428         for ( $j = 0 ; $j < $difference ; $j++ ) {
429             if ( $result[$i]->{'weekday'} == $k ) {
430                 push( @dayclosedcount, $k );
431             }
432             $k++;
433             ( $k = 0 ) if ( $k eq 7 );
434         }
435     }
436     return scalar(@dayclosedcount);
437 }
438
439
440 =head2 GetWayFromItemnumber
441
442     &Getwdayfromitemnumber($itemnumber);
443
444 return the different week day from repeatable_holidays table
445
446 C<$itemnumber> is  item number.
447
448 =cut
449
450 sub GetWdayFromItemnumber {
451     my ($itemnumber) = @_;
452     my $iteminfo = GetIssuesIteminfo($itemnumber);
453     my @result;
454     my $query = qq|SELECT weekday
455     FROM repeatable_holidays
456     WHERE branchcode=?
457 |;
458     my $sth = C4::Context->dbh->prepare($query);
459
460     $sth->execute( $iteminfo->{'branchcode'} );
461     while ( my $weekday = $sth->fetchrow_hashref ) {
462         push( @result, $weekday );
463     }
464     return @result;
465 }
466
467
468 =head2 GetIssuesIteminfo
469
470     &GetIssuesIteminfo($itemnumber);
471
472 return all data from issues about item
473
474 C<$itemnumber> is  item number.
475
476 =cut
477
478 sub GetIssuesIteminfo {
479     my ($itemnumber) = @_;
480     my $dbh          = C4::Context->dbh;
481     my $query        = qq|SELECT *
482     FROM issues
483     WHERE itemnumber=?
484     |;
485     my $sth = $dbh->prepare($query);
486     $sth->execute($itemnumber);
487     my ($issuesinfo) = $sth->fetchrow_hashref;
488     return $issuesinfo;
489 }
490
491
492 =head2 UpdateFine
493
494     &UpdateFine(
495         {
496             issue_id       => $issue_id,
497             itemnumber     => $itemnumber,
498             borrowernumber => $borrowernumber,
499             amount         => $amount,
500             due            => $date_due
501         }
502     );
503
504 (Note: the following is mostly conjecture and guesswork.)
505
506 Updates the fine owed on an overdue book.
507
508 C<$itemnumber> is the book's item number.
509
510 C<$borrowernumber> is the borrower number of the patron who currently
511 has the book on loan.
512
513 C<$amount> is the current amount owed by the patron.
514
515 C<$due> is the due date formatted to the currently specified date format
516
517 C<&UpdateFine> looks up the amount currently owed on the given item
518 and sets it to C<$amount>, creating, if necessary, a new entry in the
519 accountlines table of the Koha database.
520
521 =cut
522
523 #
524 # Question: Why should the caller have to
525 # specify both the item number and the borrower number? A book can't
526 # be on loan to two different people, so the item number should be
527 # sufficient.
528 #
529 # Possible Answer: You might update a fine for a damaged item, *after* it is returned.
530 #
531 sub UpdateFine {
532     my ($params) = @_;
533
534     my $issue_id       = $params->{issue_id};
535     my $itemnum        = $params->{itemnumber};
536     my $borrowernumber = $params->{borrowernumber};
537     my $amount         = $params->{amount};
538     my $due            = $params->{due} // q{};
539
540     unless ( $issue_id ) {
541         carp("No issue_id passed in!");
542         return;
543     }
544
545     my $dbh = C4::Context->dbh;
546     my $overdues = Koha::Account::Lines->search(
547         {
548             borrowernumber    => $borrowernumber,
549             debit_type_code   => 'OVERDUE'
550         }
551     );
552
553     my $accountline;
554     my $total_amount_other = 0.00;
555     my $due_qr = qr/$due/;
556     # Cycle through the fines and
557     # - find line that relates to the requested $itemnum
558     # - accumulate fines for other items
559     # so we can update $itemnum fine taking in account fine caps
560     while (my $overdue = $overdues->next) {
561         if ( defined $overdue->issue_id && $overdue->issue_id == $issue_id && $overdue->status eq 'UNRETURNED' ) {
562             if ($accountline) {
563                 Koha::Logger->get->debug("Not a unique accountlines record for issue_id $issue_id"); # FIXME Do we really need to log that?
564                 #FIXME Should we still count this one in total_amount ??
565             }
566             else {
567                 $accountline = $overdue;
568             }
569         }
570         $total_amount_other += $overdue->amountoutstanding;
571     }
572
573     if ( my $maxfine = C4::Context->preference('MaxFine') ) {
574         my $maxIncrease = $maxfine - $total_amount_other;
575         return if Koha::Number::Price->new($maxIncrease)->round <= 0.00;
576         if ($accountline) {
577             if ( ( $amount - $accountline->amount ) > $maxIncrease ) {
578                 my $new_amount = $accountline->amount + $maxIncrease;
579                 Koha::Logger->get->debug("Reducing fine for item $itemnum borrower $borrowernumber from $amount to $new_amount - MaxFine reached");
580                 $amount = $new_amount;
581             }
582         }
583         elsif ( $amount > $maxIncrease ) {
584             Koha::Logger->get->debug("Reducing fine for item $itemnum borrower $borrowernumber from $amount to $maxIncrease - MaxFine reached");
585             $amount = $maxIncrease;
586         }
587     }
588
589     if ( $accountline ) {
590         if ( Koha::Number::Price->new($accountline->amount)->round != Koha::Number::Price->new($amount)->round ) {
591             $accountline->adjust(
592                 {
593                     amount    => $amount,
594                     type      => 'overdue_update',
595                     interface => C4::Context->interface
596                 }
597             );
598         }
599     } else {
600         if ( $amount ) { # Don't add new fines with an amount of 0
601             my $patron = Koha::Patrons->find( $borrowernumber );
602             my $letter = eval { C4::Letters::GetPreparedLetter(
603                 module                 => 'circulation',
604                 letter_code            => 'OVERDUE_FINE_DESC',
605                 message_transport_type => 'print',
606                 lang                   => $patron->lang,
607                 tables                 => {
608                     issues    => $itemnum,
609                     borrowers => $borrowernumber,
610                     items     => $itemnum,
611                 },
612             ) };
613             my $desc = $letter ? $letter->{content} : "Item $itemnum - due $due";
614
615             my $account = Koha::Account->new({ patron_id => $borrowernumber });
616             $accountline = $account->add_debit(
617                 {
618                     amount      => $amount,
619                     description => $desc,
620                     note        => undef,
621                     user_id     => undef,
622                     interface   => C4::Context->interface,
623                     library_id  => undef, #FIXME: Should we grab the checkout or circ-control branch here perhaps?
624                     type        => 'OVERDUE',
625                     item_id     => $itemnum,
626                     issue_id    => $issue_id,
627                 }
628             );
629         }
630     }
631 }
632
633 =head2 GetFine
634
635     $data->{'sum(amountoutstanding)'} = &GetFine($itemnum,$borrowernumber);
636
637 return the total of fine
638
639 C<$itemnum> is item number
640
641 C<$borrowernumber> is the borrowernumber
642
643 =cut 
644
645 sub GetFine {
646     my ( $itemnum, $borrowernumber ) = @_;
647     my $dbh   = C4::Context->dbh();
648     my $query = q|SELECT sum(amountoutstanding) as fineamount FROM accountlines
649     WHERE debit_type_code = 'OVERDUE'
650   AND amountoutstanding > 0 AND borrowernumber=?|;
651     my @query_param;
652     push @query_param, $borrowernumber;
653     if (defined $itemnum )
654     {
655         $query .= " AND itemnumber=?";
656         push @query_param, $itemnum;
657     }
658     my $sth = $dbh->prepare($query);
659     $sth->execute( @query_param );
660     my $fine = $sth->fetchrow_hashref();
661     if ($fine->{fineamount}) {
662         return $fine->{fineamount};
663     }
664     return 0;
665 }
666
667 =head2 GetBranchcodesWithOverdueRules
668
669     my @branchcodes = C4::Overdues::GetBranchcodesWithOverdueRules()
670
671 returns a list of branch codes for branches with overdue rules defined.
672
673 =cut
674
675 sub GetBranchcodesWithOverdueRules {
676     my $dbh               = C4::Context->dbh;
677     my $branchcodes = $dbh->selectcol_arrayref(q|
678         SELECT DISTINCT(branchcode)
679         FROM overduerules
680         WHERE delay1 IS NOT NULL
681         ORDER BY branchcode
682     |);
683     if ( $branchcodes->[0] eq '' ) {
684         # If a default rule exists, all branches should be returned
685         return Koha::Libraries->search({}, { order_by => 'branchname' })->get_column('branchcode');
686     }
687     return @$branchcodes;
688 }
689
690 =head2 GetOverduesForBranch
691
692 Sql request for display all information for branchoverdues.pl
693 2 possibilities : with or without location .
694 display is filtered by branch
695
696 FIXME: This function should be renamed.
697
698 =cut
699
700 sub GetOverduesForBranch {
701     my ( $branch, $location) = @_;
702         my $itype_link =  (C4::Context->preference('item-level_itypes')) ?  " items.itype " :  " biblioitems.itemtype ";
703     my $dbh = C4::Context->dbh;
704     my $select = "
705     SELECT
706             borrowers.cardnumber,
707             borrowers.borrowernumber,
708             borrowers.surname,
709             borrowers.firstname,
710             borrowers.phone,
711             borrowers.email,
712                biblio.title,
713                biblio.subtitle,
714                biblio.medium,
715                biblio.part_number,
716                biblio.part_name,
717                biblio.author,
718                biblio.biblionumber,
719                issues.date_due,
720                issues.returndate,
721                issues.branchcode,
722              branches.branchname,
723                 items.barcode,
724                 items.homebranch,
725                 items.itemcallnumber,
726                 items.location,
727                 items.itemnumber,
728             itemtypes.description,
729          accountlines.amountoutstanding
730     FROM  accountlines
731     LEFT JOIN issues      ON    issues.itemnumber     = accountlines.itemnumber
732                           AND   issues.borrowernumber = accountlines.borrowernumber
733     LEFT JOIN borrowers   ON borrowers.borrowernumber = accountlines.borrowernumber
734     LEFT JOIN items       ON     items.itemnumber     = issues.itemnumber
735     LEFT JOIN biblio      ON      biblio.biblionumber =  items.biblionumber
736     LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
737     LEFT JOIN itemtypes   ON itemtypes.itemtype       = $itype_link
738     LEFT JOIN branches    ON  branches.branchcode     = issues.branchcode
739     WHERE (accountlines.amountoutstanding  != '0.000000')
740       AND (accountlines.debit_type_code     = 'OVERDUE' )
741       AND (accountlines.status              = 'UNRETURNED' )
742       AND (issues.branchcode =  ?   )
743       AND (issues.date_due  < NOW())
744     ";
745     if ($location) {
746         my $q = "$select AND items.location = ? ORDER BY borrowers.surname, borrowers.firstname";
747         return @{ $dbh->selectall_arrayref($q, { Slice => {} }, $branch, $location ) };
748     } else {
749         my $q = "$select ORDER BY borrowers.surname, borrowers.firstname";
750         return @{ $dbh->selectall_arrayref($q, { Slice => {} }, $branch ) };
751     }
752 }
753
754 =head2 GetOverdueMessageTransportTypes
755
756     my $message_transport_types = GetOverdueMessageTransportTypes( $branchcode, $categorycode, $letternumber);
757
758     return a arrayref with all message_transport_type for given branchcode, categorycode and letternumber(1,2 or 3)
759
760 =cut
761
762 sub GetOverdueMessageTransportTypes {
763     my ( $branchcode, $categorycode, $letternumber ) = @_;
764     return unless $categorycode and $letternumber;
765     my $dbh = C4::Context->dbh;
766     my $sth = $dbh->prepare("
767         SELECT message_transport_type
768         FROM overduerules odr LEFT JOIN overduerules_transport_types ott USING (overduerules_id)
769         WHERE branchcode = ?
770           AND categorycode = ?
771           AND letternumber = ?
772     ");
773     $sth->execute( $branchcode, $categorycode, $letternumber );
774     my @mtts;
775     while ( my $mtt = $sth->fetchrow ) {
776         push @mtts, $mtt;
777     }
778
779     # Put 'print' in first if exists
780     # It avoid to sent a print notice with an email or sms template is no email or sms is defined
781     @mtts = uniq( 'print', @mtts )
782         if grep { $_ eq 'print' } @mtts;
783
784     return \@mtts;
785 }
786
787 =head2 parse_overdues_letter
788
789 parses the letter template, replacing the placeholders with data
790 specific to this patron, biblio, or item for overdues
791
792 named parameters:
793   letter - required hashref
794   borrowernumber - required integer
795   substitute - optional hashref of other key/value pairs that should
796     be substituted in the letter content
797
798 returns the C<letter> hashref, with the content updated to reflect the
799 substituted keys and values.
800
801 =cut
802
803 sub parse_overdues_letter {
804     my $params = shift;
805     foreach my $required (qw( letter_code borrowernumber )) {
806         return unless ( exists $params->{$required} && $params->{$required} );
807     }
808
809     my $patron = Koha::Patrons->find( $params->{borrowernumber} );
810
811     my $substitute = $params->{'substitute'} || {};
812
813     my %tables = ( 'borrowers' => $params->{'borrowernumber'} );
814     if ( my $p = $params->{'branchcode'} ) {
815         $tables{'branches'} = $p;
816     }
817
818     my $active_currency = Koha::Acquisition::Currencies->get_active;
819
820     my $currency_format;
821     $currency_format = $active_currency->currency if defined($active_currency);
822
823     my @item_tables;
824     if ( my $i = $params->{'items'} ) {
825         foreach my $item (@$i) {
826             my $fine = GetFine($item->{'itemnumber'}, $params->{'borrowernumber'});
827             $item->{'fine'} = currency_format($currency_format, "$fine", FMT_SYMBOL);
828             # if active currency isn't correct ISO code fallback to sprintf
829             $item->{'fine'} = sprintf('%.2f', $fine) unless $item->{'fine'};
830
831             push @item_tables, {
832                 'biblio' => $item->{'biblionumber'},
833                 'biblioitems' => $item->{'biblionumber'},
834                 'items' => $item,
835                 'issues' => $item->{'itemnumber'},
836             };
837         }
838     }
839
840     return C4::Letters::GetPreparedLetter (
841         module => 'circulation',
842         letter_code => $params->{'letter_code'},
843         branchcode => $params->{'branchcode'},
844         lang => $patron->lang,
845         tables => \%tables,
846         loops => {
847             overdues => [ map { $_->{items}->{itemnumber} } @item_tables ],
848         },
849         substitute => $substitute,
850         repeat => { item => \@item_tables },
851         message_transport_type => $params->{message_transport_type},
852     );
853 }
854
855 1;
856 __END__
857
858 =head1 AUTHOR
859
860 Koha Development Team <http://koha-community.org/>
861
862 =cut