Bug 14687: Patron's transaction history changes items' order after paying fines.
[koha-ffzg.git] / C4 / Overdues.pm
index 66cb108..0fc6232 100644 (file)
@@ -6,71 +6,79 @@ package C4::Overdues;
 #
 # This file is part of Koha.
 #
-# Koha is free software; you can redistribute it and/or modify it under the
-# terms of the GNU General Public License as published by the Free Software
-# Foundation; either version 2 of the License, or (at your option) any later
-# version.
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
 #
-# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License along
-# with Koha; if not, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
 
 use strict;
 #use warnings; FIXME - Bug 2505
 use Date::Calc qw/Today Date_to_Days/;
 use Date::Manip qw/UnixDate/;
+use List::MoreUtils qw( uniq );
+use POSIX qw( floor ceil );
+use Locale::Currency::Format 1.28;
+
 use C4::Circulation;
 use C4::Context;
 use C4::Accounts;
 use C4::Log; # logaction
 use C4::Debug;
+use C4::Budgets qw(GetCurrency);
 
 use vars qw($VERSION @ISA @EXPORT);
 
 BEGIN {
-       # set the version for version checking
+    # set the version for version checking
     $VERSION = 3.07.00.049;
-       require Exporter;
-       @ISA    = qw(Exporter);
-       # subs to rename (and maybe merge some...)
-       push @EXPORT, qw(
-        &CalcFine
-        &Getoverdues
-        &checkoverdues
-        &NumberNotifyId
-        &AmountNotify
-        &UpdateFine
-        &GetFine
-        
-        &CheckItemNotify
-        &GetOverduesForBranch
-        &RemoveNotifyLine
-        &AddNotifyLine
-        &GetOverdueMessageTransportTypes
-       );
-       # subs to remove
-       push @EXPORT, qw(
-        &BorType
-       );
-
-       # check that an equivalent don't exist already before moving
-
-       # subs to move to Circulation.pm
-       push @EXPORT, qw(
-        &GetIssuesIteminfo
-       );
-
-     # &GetIssuingRules - delete.
-   # use C4::Circulation::GetIssuingRule instead.
-
-       # subs to move to Biblio.pm
-       push @EXPORT, qw(
-        &GetItems
-       );
+    require Exporter;
+    @ISA = qw(Exporter);
+
+    # subs to rename (and maybe merge some...)
+    push @EXPORT, qw(
+      &CalcFine
+      &Getoverdues
+      &checkoverdues
+      &NumberNotifyId
+      &AmountNotify
+      &UpdateFine
+      &GetFine
+      &get_chargeable_units
+      &CheckItemNotify
+      &GetOverduesForBranch
+      &RemoveNotifyLine
+      &AddNotifyLine
+      &GetOverdueMessageTransportTypes
+      &parse_overdues_letter
+    );
+
+    # subs to remove
+    push @EXPORT, qw(
+      &BorType
+    );
+
+    # check that an equivalent don't exist already before moving
+
+    # subs to move to Circulation.pm
+    push @EXPORT, qw(
+      &GetIssuesIteminfo
+    );
+
+    # &GetIssuingRules - delete.
+    # use C4::Circulation::GetIssuingRule instead.
+
+    # subs to move to Biblio.pm
+    push @EXPORT, qw(
+      &GetItems
+    );
 }
 
 =head1 NAME
@@ -107,14 +115,14 @@ sub Getoverdues {
     my $statement;
     if ( C4::Context->preference('item-level_itypes') ) {
         $statement = "
-   SELECT issues.*, items.itype as itemtype, items.homebranch, items.barcode
+   SELECT issues.*, items.itype as itemtype, items.homebranch, items.barcode, items.itemlost
      FROM issues 
 LEFT JOIN items       USING (itemnumber)
     WHERE date_due < NOW()
 ";
     } else {
         $statement = "
-   SELECT issues.*, biblioitems.itemtype, items.itype, items.homebranch, items.barcode
+   SELECT issues.*, biblioitems.itemtype, items.itype, items.homebranch, items.barcode, items.itemlost
      FROM issues 
 LEFT JOIN items       USING (itemnumber)
 LEFT JOIN biblioitems USING (biblioitemnumber)
@@ -246,18 +254,18 @@ sub CalcFine {
     my $fine_unit = $data->{lengthunit};
     $fine_unit ||= 'days';
 
-    my $chargeable_units = _get_chargeable_units($fine_unit, $start_date, $end_date, $branchcode);
+    my $chargeable_units = get_chargeable_units($fine_unit, $start_date, $end_date, $branchcode);
     my $units_minus_grace = $chargeable_units - $data->{firstremind};
     my $amount = 0;
-    if ($data->{'chargeperiod'}  && ($units_minus_grace > 0)  ) {
-        if ( C4::Context->preference('FinesIncludeGracePeriod') ) {
-            $amount = int($chargeable_units / $data->{'chargeperiod'}) * $data->{'fine'};# TODO fine calc should be in cents
-        } else {
-            $amount = int($units_minus_grace / $data->{'chargeperiod'}) * $data->{'fine'};
-        }
-    } else {
-        # a zero (or null) chargeperiod or negative units_minus_grace value means no charge.
-    }
+    if ( $data->{'chargeperiod'} && ( $units_minus_grace > 0 ) ) {
+        my $units = C4::Context->preference('FinesIncludeGracePeriod') ? $chargeable_units : $units_minus_grace;
+        my $charge_periods = $units / $data->{'chargeperiod'};
+        # If chargeperiod_charge_at = 1, we charge a fine at the start of each charge period
+        # if chargeperiod_charge_at = 0, we charge at the end of each charge period
+        $charge_periods = $data->{'chargeperiod_charge_at'} == 1 ? ceil($charge_periods) : floor($charge_periods);
+        $amount = $charge_periods * $data->{'fine'};
+    } # else { # a zero (or null) chargeperiod or negative units_minus_grace value means no charge. }
+
     $amount = $data->{overduefinescap} if $data->{overduefinescap} && $amount > $data->{overduefinescap};
     $debug and warn sprintf("CalcFine returning (%s, %s, %s, %s)", $amount, $data->{'chargename'}, $units_minus_grace, $chargeable_units);
     return ($amount, $data->{'chargename'}, $units_minus_grace, $chargeable_units);
@@ -265,9 +273,9 @@ sub CalcFine {
 }
 
 
-=head2 _get_chargeable_units
+=head2 get_chargeable_units
 
-    _get_chargeable_units($unit, $start_date_ $end_date, $branchcode);
+    get_chargeable_units($unit, $start_date_ $end_date, $branchcode);
 
 return integer value of units between C<$start_date> and C<$end_date>, factoring in holidays for C<$branchcode>.
 
@@ -279,16 +287,20 @@ C<$branchcode> is the branch whose calendar to use for finding holidays.
 
 =cut
 
-sub _get_chargeable_units {
-    my ($unit, $dt1, $dt2, $branchcode) = @_;
+sub get_chargeable_units {
+    my ($unit, $date_due, $date_returned, $branchcode) = @_;
+
+    # If the due date is later than the return date
+    return 0 unless ( $date_returned > $date_due );
+
     my $charge_units = 0;
     my $charge_duration;
     if ($unit eq 'hours') {
         if(C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed') {
             my $calendar = Koha::Calendar->new( branchcode => $branchcode );
-            $charge_duration = $calendar->hours_between( $dt1, $dt2 );
+            $charge_duration = $calendar->hours_between( $date_due, $date_returned );
         } else {
-            $charge_duration = $dt2->delta_ms( $dt1 );
+            $charge_duration = $date_returned->delta_ms( $date_due );
         }
         if($charge_duration->in_units('hours') == 0 && $charge_duration->in_units('seconds') > 0){
             return 1;
@@ -298,9 +310,9 @@ sub _get_chargeable_units {
     else { # days
         if(C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed') {
             my $calendar = Koha::Calendar->new( branchcode => $branchcode );
-            $charge_duration = $calendar->days_between( $dt1, $dt2 );
+            $charge_duration = $calendar->days_between( $date_due, $date_returned );
         } else {
-            $charge_duration = $dt2->delta_days( $dt1 );
+            $charge_duration = $date_returned->delta_days( $date_due );
         }
         return $charge_duration->in_units('days');
     }
@@ -574,29 +586,24 @@ sub UpdateFine {
             #      print "no update needed $data->{'amount'}"
         }
     } else {
-        my $sth4 = $dbh->prepare(
-            "SELECT title FROM biblio LEFT JOIN items ON biblio.biblionumber=items.biblionumber WHERE items.itemnumber=?"
-        );
-        $sth4->execute($itemnum);
-        my $title = $sth4->fetchrow;
-
-#         #   print "not in account";
-#         my $sth3 = $dbh->prepare("Select max(accountno) from accountlines");
-#         $sth3->execute;
-# 
-#         # FIXME - Make $accountno a scalar.
-#         my @accountno = $sth3->fetchrow_array;
-#         $sth3->finish;
-#         $accountno[0]++;
-# begin transaction
-               my $nextaccntno = C4::Accounts::getnextacctno($borrowernumber);
-               my $desc = ($type ? "$type " : '') . "$title $due";     # FIXEDME, avoid whitespace prefix on empty $type
-               my $query = "INSERT INTO accountlines
-                   (borrowernumber,itemnumber,date,amount,description,accounttype,amountoutstanding,lastincrement,accountno)
-                           VALUES (?,?,now(),?,?,'FU',?,?,?)";
-               my $sth2 = $dbh->prepare($query);
-               $debug and print STDERR "UpdateFine query: $query\nw/ args: $borrowernumber, $itemnum, $amount, $desc, $amount, $amount, $nextaccntno\n";
-        $sth2->execute($borrowernumber, $itemnum, $amount, $desc, $amount, $amount, $nextaccntno);
+        if ( $amount ) { # Don't add new fines with an amount of 0
+            my $sth4 = $dbh->prepare(
+                "SELECT title FROM biblio LEFT JOIN items ON biblio.biblionumber=items.biblionumber WHERE items.itemnumber=?"
+            );
+            $sth4->execute($itemnum);
+            my $title = $sth4->fetchrow;
+
+            my $nextaccntno = C4::Accounts::getnextacctno($borrowernumber);
+
+            my $desc = ( $type ? "$type " : '' ) . "$title $due";    # FIXEDME, avoid whitespace prefix on empty $type
+
+            my $query = "INSERT INTO accountlines
+                         (borrowernumber,itemnumber,date,amount,description,accounttype,amountoutstanding,lastincrement,accountno)
+                         VALUES (?,?,now(),?,?,'FU',?,?,?)";
+            my $sth2 = $dbh->prepare($query);
+            $debug and print STDERR "UpdateFine query: $query\nw/ args: $borrowernumber, $itemnum, $amount, $desc, $amount, $amount, $nextaccntno\n";
+            $sth2->execute( $borrowernumber, $itemnum, $amount, $desc, $amount, $amount, $nextaccntno );
+        }
     }
     # logging action
     &logaction(
@@ -649,9 +656,16 @@ sub GetFine {
     my $dbh   = C4::Context->dbh();
     my $query = q|SELECT sum(amountoutstanding) as fineamount FROM accountlines
     where accounttype like 'F%'
-  AND amountoutstanding > 0 AND itemnumber = ? AND borrowernumber=?|;
+  AND amountoutstanding > 0 AND borrowernumber=?|;
+    my @query_param;
+    push @query_param, $borrowernumber;
+    if (defined $itemnum )
+    {
+        $query .= " AND itemnumber=?";
+        push @query_param, $itemnum;
+    }
     my $sth = $dbh->prepare($query);
-    $sth->execute( $itemnum, $borrowernumber );
+    $sth->execute( @query_param );
     my $fine = $sth->fetchrow_hashref();
     if ($fine->{fineamount}) {
         return $fine->{fineamount};
@@ -750,14 +764,18 @@ returns a list of branch codes for branches with overdue rules defined.
 
 sub GetBranchcodesWithOverdueRules {
     my $dbh               = C4::Context->dbh;
-    my $rqoverduebranches = $dbh->prepare("SELECT DISTINCT branchcode FROM overduerules WHERE delay1 IS NOT NULL AND branchcode <> '' ORDER BY branchcode");
-    $rqoverduebranches->execute;
-    my @branches = map { shift @$_ } @{ $rqoverduebranches->fetchall_arrayref };
-    if (!$branches[0]) {
-       my $availbranches = C4::Branch::GetBranches();
-       @branches = keys %$availbranches;
+    my $branchcodes = $dbh->selectcol_arrayref(q|
+        SELECT DISTINCT(branchcode)
+        FROM overduerules
+        WHERE delay1 IS NOT NULL
+        ORDER BY branchcode
+    |);
+    if ( $branchcodes->[0] eq '' ) {
+        # If a default rule exists, all branches should be returned
+        my $availbranches = C4::Branch::GetBranches();
+        return keys %$availbranches;
     }
-    return @branches;
+    return @$branchcodes;
 }
 
 =head2 CheckItemNotify
@@ -914,6 +932,7 @@ sub RemoveNotifyLine {
     return a arrayref with all message_transport_type for given branchcode, categorycode and letternumber(1,2 or 3)
 
 =cut
+
 sub GetOverdueMessageTransportTypes {
     my ( $branchcode, $categorycode, $letternumber ) = @_;
     return unless $categorycode and $letternumber;
@@ -927,9 +946,83 @@ sub GetOverdueMessageTransportTypes {
     while ( my $mtt = $sth->fetchrow ) {
         push @mtts, $mtt;
     }
+
+    # Put 'print' in first if exists
+    # It avoid to sent a print notice with an email or sms template is no email or sms is defined
+    @mtts = uniq( 'print', @mtts )
+        if grep {/^print$/} @mtts;
+
     return \@mtts;
 }
 
+=head2 parse_overdues_letter
+
+parses the letter template, replacing the placeholders with data
+specific to this patron, biblio, or item for overdues
+
+named parameters:
+  letter - required hashref
+  borrowernumber - required integer
+  substitute - optional hashref of other key/value pairs that should
+    be substituted in the letter content
+
+returns the C<letter> hashref, with the content updated to reflect the
+substituted keys and values.
+
+=cut
+
+sub parse_overdues_letter {
+    my $params = shift;
+    foreach my $required (qw( letter_code borrowernumber )) {
+        return unless ( exists $params->{$required} && $params->{$required} );
+    }
+
+    my $substitute = $params->{'substitute'} || {};
+    $substitute->{today} ||= C4::Dates->new()->output("syspref");
+
+    my %tables = ( 'borrowers' => $params->{'borrowernumber'} );
+    if ( my $p = $params->{'branchcode'} ) {
+        $tables{'branches'} = $p;
+    }
+
+    my $currencies = GetCurrency();
+    my $currency_format;
+    $currency_format = $currencies->{currency} if defined($currencies);
+
+    my @item_tables;
+    if ( my $i = $params->{'items'} ) {
+        my $item_format = '';
+        foreach my $item (@$i) {
+            my $fine = GetFine($item->{'itemnumber'}, $params->{'borrowernumber'});
+            if ( !$item_format and defined $params->{'letter'}->{'content'} ) {
+                $params->{'letter'}->{'content'} =~ m/(<item>.*<\/item>)/;
+                $item_format = $1;
+            }
+
+            $item->{'fine'} = currency_format($currency_format, "$fine", FMT_SYMBOL);
+            # if active currency isn't correct ISO code fallback to sprintf
+            $item->{'fine'} = sprintf('%.2f', $fine) unless $item->{'fine'};
+
+            push @item_tables, {
+                'biblio' => $item->{'biblionumber'},
+                'biblioitems' => $item->{'biblionumber'},
+                'items' => $item,
+                'issues' => $item->{'itemnumber'},
+            };
+        }
+    }
+
+    return C4::Letters::GetPreparedLetter (
+        module => 'circulation',
+        letter_code => $params->{'letter_code'},
+        branchcode => $params->{'branchcode'},
+        tables => \%tables,
+        substitute => $substitute,
+        repeat => { item => \@item_tables },
+        message_transport_type => $params->{message_transport_type},
+    );
+}
+
 1;
 __END__