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