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