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