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