Integrated version of the Koha Offline Circulation file uploader. It needs some testi...
[koha.git] / C4 / Circulation.pm
index 5a39d84..5b2043f 100644 (file)
@@ -63,13 +63,17 @@ BEGIN {
                &CanBookBeIssued
                &CanBookBeRenewed
                &AddIssue
+                &ForceIssue
                &AddRenewal
+                &ForceRenewal
                &GetRenewCount
                &GetItemIssue
+                &GetOpenIssue
                &GetItemIssues
                &GetBorrowerIssues
                &GetIssuingCharges
                &GetIssuingRule
+        &GetBranchBorrowerCircRule
                &GetBiblioIssues
                &AnonymiseIssueHistory
        );
@@ -77,6 +81,7 @@ BEGIN {
        # subs to deal with returns
        push @EXPORT, qw(
                &AddReturn
+                &ForceReturn
         &MarkIssueReturned
        );
 
@@ -106,21 +111,26 @@ Also deals with stocktaking.
 
 =head1 FUNCTIONS
 
-=head2 decode
+=head2 barcodedecode
 
-=head3 $str = &decode($chunk);
+=head3 $str = &barcodedecode($barcode);
 
 =over 4
 
 =item Generic filter function for barcode string.
+Called on every circ if the System Pref itemBarcodeInputFilter is set.
+Will do some manipulation of the barcode for systems that deliver a barcode
+to circulation.pl that differs from the barcode stored for the item.
+For proper functioning of this filter, calling the function on the 
+correct barcode string (items.barcode) should return an unaltered barcode.
 
 =back
 
 =cut
 
-# FIXME From Paul : i don't understand what this sub does & why it has to be called on every circ. Speak of this with chris maybe ?
 # FIXME -- the &decode fcn below should be wrapped into this one.
-
+# FIXME -- these plugins should be moved out of Circulation.pm
+#
 sub barcodedecode {
     my ($barcode) = @_;
     my $filter = C4::Context->preference('itemBarcodeInputFilter');
@@ -138,8 +148,14 @@ sub barcodedecode {
                return $barcode;
            }
        } elsif($filter eq 'T-prefix') {
-               my $num = ( $barcode =~ /^[Tt] /) ? substr($barcode,2) + 0 : $barcode;
-               return sprintf( "T%07d",$num);
+               if ( $barcode =~ /^[Tt]/) {
+                       if (substr($barcode,1,1) eq '0') {
+                               return $barcode;
+                       } else {
+                               $barcode = substr($barcode,2) + 0 ;
+                       }
+               }
+               return sprintf( "T%07d",$barcode);
        }
 }
 
@@ -404,128 +420,107 @@ sub TooMany {
        my $type = (C4::Context->preference('item-level_itypes')) 
                        ? $item->{'itype'}         # item-level
                        : $item->{'itemtype'};     # biblio-level
-  
-       my $sth =
-      $dbh->prepare(
-                'SELECT * FROM issuingrules 
-                        WHERE categorycode = ? 
-                            AND itemtype = ? 
-                            AND branchcode = ?'
-      );
-
-    my $query2 = "SELECT  COUNT(*) FROM issues i, biblioitems s1, items s2 
-                WHERE i.borrowernumber = ? 
-                    AND i.itemnumber = s2.itemnumber 
-                    AND s1.biblioitemnumber = s2.biblioitemnumber";
-    if (C4::Context->preference('item-level_itypes')){
-          $query2.=" AND s2.itype=? ";
-    } else { 
-          $query2.=" AND s1.itemtype= ? ";
-    }
-    my $sth2=  $dbh->prepare($query2);
-    my $sth3 =
-      $dbh->prepare(
-            'SELECT COUNT(*) FROM issues
-                WHERE borrowernumber = ?'
-            );
-    my $alreadyissued;
-
-    # check the 3 parameters (branch / itemtype / category code
-    $sth->execute( $cat_borrower, $type, $branch );
-    my $result = $sth->fetchrow_hashref;
-#     warn "$cat_borrower, $type, $branch = ".Data::Dumper::Dumper($result);
-
-    if ( $result->{maxissueqty} ne '' ) {
-#         warn "checking on everything set";
-        $sth2->execute( $borrower->{'borrowernumber'}, $type );
-        my $alreadyissued = $sth2->fetchrow;
-        if ( $result->{'maxissueqty'} <= $alreadyissued ) {
-            return ( "$alreadyissued / ".( $result->{maxissueqty} + 0 )." (rule on branch/category/itemtype failed)" );
-        }
-        # now checking for total
-        $sth->execute( $cat_borrower, '*', $branch );
-        my $result = $sth->fetchrow_hashref;
-        if ( $result->{maxissueqty} ne '' ) {
-            $sth2->execute( $borrower->{'borrowernumber'}, $type );
-            my $alreadyissued = $sth2->fetchrow;
-            if ( $result->{'maxissueqty'} <= $alreadyissued ) {
-                return ( "$alreadyissued / ".( $result->{maxissueqty} + 0 )." (rule on branch/category/total failed)"  );
+    # given branch, patron category, and item type, determine
+    # applicable issuing rule
+    my $issuing_rule = GetIssuingRule($cat_borrower, $type, $branch);
+
+    # if a rule is found and has a loan limit set, count
+    # how many loans the patron already has that meet that
+    # rule
+    if (defined($issuing_rule) and defined($issuing_rule->{'maxissueqty'})) {
+        my @bind_params;
+        my $count_query = "SELECT COUNT(*) FROM issues
+                           JOIN items USING (itemnumber) ";
+
+        my $rule_itemtype = $issuing_rule->{itemtype};
+        if ($rule_itemtype eq "*") {
+            # matching rule has the default item type, so count only
+            # those existing loans that don't fall under a more
+            # specific rule
+            if (C4::Context->preference('item-level_itypes')) {
+                $count_query .= " WHERE items.itype NOT IN (
+                                    SELECT itemtype FROM issuingrules
+                                    WHERE branchcode = ?
+                                    AND   (categorycode = ? OR categorycode = ?)
+                                    AND   itemtype <> '*'
+                                  ) ";
+            } else { 
+                $count_query .= " JOIN  biblioitems USING (biblionumber) 
+                                  WHERE biblioitems.itemtype NOT IN (
+                                    SELECT itemtype FROM issuingrules
+                                    WHERE branchcode = ?
+                                    AND   (categorycode = ? OR categorycode = ?)
+                                    AND   itemtype <> '*'
+                                  ) ";
             }
+            push @bind_params, $issuing_rule->{branchcode};
+            push @bind_params, $issuing_rule->{categorycode};
+            push @bind_params, $cat_borrower;
+        } else {
+            # rule has specific item type, so count loans of that
+            # specific item type
+            if (C4::Context->preference('item-level_itypes')) {
+                $count_query .= " WHERE items.itype = ? ";
+            } else { 
+                $count_query .= " JOIN  biblioitems USING (biblionumber) 
+                                  WHERE biblioitems.itemtype= ? ";
+            }
+            push @bind_params, $type;
         }
-    }
 
-    # check the 2 parameters (branch / itemtype / default categorycode
-    $sth->execute( '*', $type, $branch );
-    $result = $sth->fetchrow_hashref;
-#     warn "*, $type, $branch = ".Data::Dumper::Dumper($result);
-
-    if ( $result->{maxissueqty} ne '' ) {
-#         warn "checking on 2 parameters (default categorycode)";
-        $sth2->execute( $borrower->{'borrowernumber'}, $type );
-        my $alreadyissued = $sth2->fetchrow;
-        if ( $result->{'maxissueqty'} <= $alreadyissued ) {
-            return ( "$alreadyissued / ".( $result->{maxissueqty} + 0 )." (rule on branch / default category / itemtype failed)"  );
-        }
-        # now checking for total
-        $sth->execute( '*', '*', $branch );
-        my $result = $sth->fetchrow_hashref;
-        if ( $result->{maxissueqty} ne '' ) {
-            $sth2->execute( $borrower->{'borrowernumber'}, $type );
-            my $alreadyissued = $sth2->fetchrow;
-            if ( $result->{'maxissueqty'} <= $alreadyissued ) {
-                return ( "$alreadyissued / ".( $result->{maxissueqty} + 0 )." (rule on branch / default category / total failed)" );
+        $count_query .= " AND borrowernumber = ? ";
+        push @bind_params, $borrower->{'borrowernumber'};
+        my $rule_branch = $issuing_rule->{branchcode};
+        if ($rule_branch ne "*") {
+            if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
+                $count_query .= " AND issues.branchcode = ? ";
+                push @bind_params, $branch;
+            } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
+                ; # if branch is the patron's home branch, then count all loans by patron
+            } else {
+                $count_query .= " AND items.homebranch = ? ";
+                push @bind_params, $branch;
             }
         }
-    }
-    
-    # check the 1 parameters (default branch / itemtype / categorycode
-    $sth->execute( $cat_borrower, $type, '*' );
-    $result = $sth->fetchrow_hashref;
-#     warn "$cat_borrower, $type, * = ".Data::Dumper::Dumper($result);
-    
-    if ( $result->{maxissueqty} ne '' ) {
-#         warn "checking on 1 parameter (default branch + categorycode)";
-        $sth2->execute( $borrower->{'borrowernumber'}, $type );
-        my $alreadyissued = $sth2->fetchrow;
-        if ( $result->{'maxissueqty'} <= $alreadyissued ) {
-            return ( "$alreadyissued / ".( $result->{maxissueqty} + 0 )." (rule on default branch/category/itemtype failed)"  );
-        }
-        # now checking for total
-        $sth->execute( $cat_borrower, '*', '*' );
-        my $result = $sth->fetchrow_hashref;
-        if ( $result->{maxissueqty} ne '' ) {
-            $sth2->execute( $borrower->{'borrowernumber'}, $type );
-            my $alreadyissued = $sth2->fetchrow;
-            if ( $result->{'maxissueqty'} <= $alreadyissued ) {
-                return ( "$alreadyissued / ".( $result->{maxissueqty} + 0 )." (rule on default branch / category / total failed)"  );
-            }
+
+        my $count_sth = $dbh->prepare($count_query);
+        $count_sth->execute(@bind_params);
+        my ($current_loan_count) = $count_sth->fetchrow_array;
+
+        my $max_loans_allowed = $issuing_rule->{'maxissueqty'};
+        if ($current_loan_count >= $max_loans_allowed) {
+            return "$current_loan_count / $max_loans_allowed";
         }
     }
 
-    # check the 0 parameters (default branch / itemtype / default categorycode
-    $sth->execute( '*', $type, '*' );
-    $result = $sth->fetchrow_hashref;
-#     warn "*, $type, * = ".Data::Dumper::Dumper($result);
-
-    if ( $result->{maxissueqty} ne '' ) {
-#         warn "checking on default branch and default categorycode";
-        $sth2->execute( $borrower->{'borrowernumber'}, $type );
-        my $alreadyissued = $sth2->fetchrow;
-        if ( $result->{'maxissueqty'} <= $alreadyissued ) {
-            return ( "$alreadyissued / ".( $result->{maxissueqty} + 0 )." (rule on default branch / default category / itemtype failed)"  );
+    # Now count total loans against the limit for the branch
+    my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower);
+    if (defined($branch_borrower_circ_rule->{maxissueqty})) {
+        my @bind_params = ();
+        my $branch_count_query = "SELECT COUNT(*) FROM issues 
+                                  JOIN items USING (itemnumber)
+                                  WHERE borrowernumber = ? ";
+        push @bind_params, $borrower->{borrowernumber};
+
+        if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
+            $branch_count_query .= " AND issues.branchcode = ? ";
+            push @bind_params, $branch;
+        } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
+            ; # if branch is the patron's home branch, then count all loans by patron
+        } else {
+            $branch_count_query .= " AND items.homebranch = ? ";
+            push @bind_params, $branch;
         }
-       }
-    # now checking for total
-    $sth->execute( '*', '*', '*' );
-    $result = $sth->fetchrow_hashref;
-    if ( $result->{maxissueqty} ne '' ) {
-               warn "checking total";
-               $sth2->execute( $borrower->{'borrowernumber'}, $type );
-               my $alreadyissued = $sth2->fetchrow;
-               if ( $result->{'maxissueqty'} <= $alreadyissued ) {
-                       return ( "$alreadyissued / ".( $result->{maxissueqty} + 0 )." (rule on default branch / default category / total failed)"  );
-               }
-       }
+        my $branch_count_sth = $dbh->prepare($branch_count_query);
+        $branch_count_sth->execute(@bind_params);
+        my ($current_loan_count) = $branch_count_sth->fetchrow_array;
+
+        my $max_loans_allowed = $branch_borrower_circ_rule->{maxissueqty};
+        if ($current_loan_count >= $max_loans_allowed) {
+            return "$current_loan_count / $max_loans_allowed";
+        }
+    }
 
     # OK, the patron can issue !!!
     return;
@@ -710,7 +705,7 @@ sub CanBookBeIssued {
     }
     else {
         if ( $amount > 0 ) {
-            $needsconfirmation{DEBT} = $amount;
+            $needsconfirmation{DEBT} = sprintf( "%.2f", $amount );
         }
     }
 
@@ -797,7 +792,7 @@ sub CanBookBeIssued {
     my ( $restype, $res ) = C4::Reserves::CheckReserves( $item->{'itemnumber'} );
     if ($restype) {
                my $resbor = $res->{'borrowernumber'};
-               my ( $resborrower, $flags ) = GetMemberDetails( $resbor, 0 );
+               my ( $resborrower ) = GetMemberDetails( $resbor, 0 );
                my $branches  = GetBranches();
                my $branchname = $branches->{ $res->{'branchcode'} }->{'branchname'};
         if ( $resbor ne $borrower->{'borrowernumber'} && $restype eq "Waiting" )
@@ -859,9 +854,8 @@ sub AddIssue {
        my $barcodecheck=CheckValidBarcode($barcode);
        if ($borrower and $barcode and $barcodecheck ne '0'){
                # find which item we issue
-               my $item = GetItem('', $barcode);
+               my $item = GetItem('', $barcode) or return undef;       # if we don't get an Item, abort.
                my $datedue; 
-               
                my $branch;
                # Get which branchcode we need
                if (C4::Context->preference('CircControl') eq 'PickupLibrary'){
@@ -919,19 +913,11 @@ sub AddIssue {
                                        # warn "Waiting";
                                        # The item is on reserve and waiting, but has been
                                        # reserved by some other patron.
-                                       my ( $resborrower, $flags ) = GetMemberDetails( $resbor, 0 );
-                                       my $branches   = GetBranches();
-                                       my $branchname =
-                                         $branches->{ $res->{'branchcode'} }->{'branchname'};
                                }
                                elsif ( $restype eq "Reserved" ) {
 
                                        # warn "Reserved";
                                        # The item is reserved by someone else.
-                                       my ( $resborrower, $flags ) =
-                                         GetMemberDetails( $resbor, 0 );
-                                       my $branches   = GetBranches();
-                                       my $branchname =  $branches->{ $res->{'branchcode'} }->{'branchname'};
                                        if ($cancelreserve) { # cancel reserves on this item
                                                CancelReserve( 0, $res->{'itemnumber'},
                                                        $res->{'borrowernumber'} );
@@ -1023,7 +1009,7 @@ sub AddIssue {
             C4::Context->userenv->{'branch'},
             'issue',                        $charge,
             '',                             $item->{'itemnumber'},
-            $item->{'itemtype'}, $borrower->{'borrowernumber'}
+            $item->{'itype'}, $borrower->{'borrowernumber'}
         );
     }
     
@@ -1033,6 +1019,29 @@ sub AddIssue {
   }
 }
 
+=head2 ForceIssue
+
+ForceIssue()
+
+Issues an item to a member, ignoring any problems that would normally dissallow the issue.
+
+=cut
+
+sub ForceIssue {
+  my ( $borrowernumber, $itemnumber, $date_due, $branchcode, $date ) = @_;
+warn "ForceIssue( $borrowernumber, $itemnumber, $date_due, $branchcode, $date );";
+  my $dbh = C4::Context->dbh;
+  my $sth = $dbh->prepare( "INSERT INTO `issues` ( `borrowernumber`, `itemnumber`, `date_due`, `branchcode`, `issuingbranch`, `returndate`, `lastreneweddate`, `return`,  `renewals`, `timestamp`, `issuedate` )
+                            VALUES ( ?, ?, ?, ?, ?, NULL, NULL, NULL, NULL, NOW(), ? )" );
+  $sth->execute( $borrowernumber, $itemnumber, $date_due, $branchcode, $branchcode, $date );
+  $sth->finish();
+
+  my $item = GetBiblioFromItemNumber( $itemnumber );
+
+  UpdateStats( $branchcode, 'issue', undef, undef, $itemnumber, $item->{ 'itemtype' }, $borrowernumber );
+}
+
+
 =head2 GetLoanLength
 
 Get loan length for an itemtype, a borrower type and a branch
@@ -1056,27 +1065,27 @@ sub GetLoanLength {
     return $loanlength->{issuelength}
       if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
 
-    $sth->execute( $borrowertype, $itemtype, "*" );
+    $sth->execute( $borrowertype, "*", $branchcode );
     $loanlength = $sth->fetchrow_hashref;
     return $loanlength->{issuelength}
       if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
 
-    $sth->execute( $borrowertype, "*", $branchcode );
+    $sth->execute( "*", $itemtype, $branchcode );
     $loanlength = $sth->fetchrow_hashref;
     return $loanlength->{issuelength}
       if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
 
-    $sth->execute( "*", $itemtype, $branchcode );
+    $sth->execute( "*", "*", $branchcode );
     $loanlength = $sth->fetchrow_hashref;
     return $loanlength->{issuelength}
       if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
 
-    $sth->execute( $borrowertype, "*", "*" );
+    $sth->execute( $borrowertype, $itemtype, "*" );
     $loanlength = $sth->fetchrow_hashref;
     return $loanlength->{issuelength}
       if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
 
-    $sth->execute( "*", "*", $branchcode );
+    $sth->execute( $borrowertype, "*", "*" );
     $loanlength = $sth->fetchrow_hashref;
     return $loanlength->{issuelength}
       if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
@@ -1118,23 +1127,23 @@ sub GetIssuingRule {
     $irule = $sth->fetchrow_hashref;
     return $irule if defined($irule) ;
 
-    $sth->execute( $borrowertype, $itemtype, "*" );
+    $sth->execute( $borrowertype, "*", $branchcode );
     $irule = $sth->fetchrow_hashref;
     return $irule if defined($irule) ;
 
-    $sth->execute( $borrowertype, "*", $branchcode );
+    $sth->execute( "*", $itemtype, $branchcode );
     $irule = $sth->fetchrow_hashref;
     return $irule if defined($irule) ;
 
-    $sth->execute( "*", $itemtype, $branchcode );
+    $sth->execute( "*", "*", $branchcode );
     $irule = $sth->fetchrow_hashref;
     return $irule if defined($irule) ;
 
-    $sth->execute( $borrowertype, "*", "*" );
+    $sth->execute( $borrowertype, $itemtype, "*" );
     $irule = $sth->fetchrow_hashref;
     return $irule if defined($irule) ;
 
-    $sth->execute( "*", "*", $branchcode );
+    $sth->execute( $borrowertype, "*", "*" );
     $irule = $sth->fetchrow_hashref;
     return $irule if defined($irule) ;
 
@@ -1150,6 +1159,93 @@ sub GetIssuingRule {
     return undef;
 }
 
+=head2 GetBranchBorrowerCircRule
+
+=over 4
+
+my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
+
+=back
+
+Retrieves circulation rule attributes that apply to the given
+branch and patron category, regardless of item type.  
+The return value is a hashref containing the following key:
+
+maxissueqty - maximum number of loans that a
+patron of the given category can have at the given
+branch.  If the value is undef, no limit.
+
+This will first check for a specific branch and
+category match from branch_borrower_circ_rules. 
+
+If no rule is found, it will then check default_branch_circ_rules
+(same branch, default category).  If no rule is found,
+it will then check default_borrower_circ_rules (default 
+branch, same category), then failing that, default_circ_rules
+(default branch, default category).
+
+If no rule has been found in the database, it will default to
+the buillt in rule:
+
+maxissueqty - undef
+
+C<$branchcode> and C<$categorycode> should contain the
+literal branch code and patron category code, respectively - no
+wildcards.
+
+=cut
+
+sub GetBranchBorrowerCircRule {
+    my $branchcode = shift;
+    my $categorycode = shift;
+
+    my $branch_cat_query = "SELECT maxissueqty
+                            FROM branch_borrower_circ_rules
+                            WHERE branchcode = ?
+                            AND   categorycode = ?";
+    my $dbh = C4::Context->dbh();
+    my $sth = $dbh->prepare($branch_cat_query);
+    $sth->execute($branchcode, $categorycode);
+    my $result;
+    if ($result = $sth->fetchrow_hashref()) {
+        return $result;
+    }
+
+    # try same branch, default borrower category
+    my $branch_query = "SELECT maxissueqty
+                        FROM default_branch_circ_rules
+                        WHERE branchcode = ?";
+    $sth = $dbh->prepare($branch_query);
+    $sth->execute($branchcode);
+    if ($result = $sth->fetchrow_hashref()) {
+        return $result;
+    }
+
+    # try default branch, same borrower category
+    my $category_query = "SELECT maxissueqty
+                          FROM default_borrower_circ_rules
+                          WHERE categorycode = ?";
+    $sth = $dbh->prepare($category_query);
+    $sth->execute($categorycode);
+    if ($result = $sth->fetchrow_hashref()) {
+        return $result;
+    }
+  
+    # try default branch, default borrower category
+    my $default_query = "SELECT maxissueqty
+                          FROM default_circ_rules";
+    $sth = $dbh->prepare($default_query);
+    $sth->execute();
+    if ($result = $sth->fetchrow_hashref()) {
+        return $result;
+    }
+    
+    # built-in default circulation rule
+    return {
+        maxissueqty => undef,
+    };
+}
+
 =head2 AddReturn
 
 ($doreturn, $messages, $iteminformation, $borrower) =
@@ -1160,8 +1256,10 @@ Returns a book.
 C<$barcode> is the bar code of the book being returned. C<$branch> is
 the code of the branch where the book is being returned.  C<$exemptfine>
 indicates that overdue charges for the item will be removed.  C<$dropbox>
-indicates that the check-in date is assumed to be yesterday.  If overdue
+indicates that the check-in date is assumed to be yesterday, or the last
+non-holiday as defined in C4::Calendar .  If overdue
 charges are applied and C<$dropbox> is true, the last charge will be removed.
+This assumes that the fines accrual script has run for _today_.
 
 C<&AddReturn> returns a list of four items:
 
@@ -1234,6 +1332,7 @@ sub AddReturn {
         # check if the book is in a permanent collection....
         my $hbr      = $iteminformation->{C4::Context->preference("HomeOrHoldingBranch")};
         my $branches = GetBranches();
+               # FIXME -- This 'PE' attribute is largely undocumented.  afaict, there's no user interface that reflects this functionality.
         if ( $hbr && $branches->{$hbr}->{'PE'} ) {
             $messages->{'IsPermanent'} = $hbr;
         }
@@ -1258,11 +1357,20 @@ sub AddReturn {
     # case of a return of document (deal with issues and holdingbranch)
     
         if ($doreturn) {
+                       my $circControlBranch;
                        if($dropbox) {
-                               # don't allow dropbox mode to create an invalid entry in issues ( issuedate > returndate)
+                               # don't allow dropbox mode to create an invalid entry in issues (issuedate > returndate) FIXME: actually checks eq, not gt
                                undef($dropbox) if ( $iteminformation->{'issuedate'} eq C4::Dates->today('iso') );
+                               if (C4::Context->preference('CircControl') eq 'ItemHomeBranch' ) {
+                                       $circControlBranch = $iteminformation->{homebranch};
+                               } elsif ( C4::Context->preference('CircControl') eq 'PatronLibrary') {
+                                       $circControlBranch = $borrower->{branchcode};
+                               } else { # CircControl must be PickupLibrary.
+                                       $circControlBranch = $iteminformation->{holdingbranch};
+                                       # FIXME - is this right ? are we sure that the holdingbranch is still the pickup branch?
+                               }
                        }
-            MarkIssueReturned($borrower->{'borrowernumber'}, $iteminformation->{'itemnumber'},$dropbox);
+            MarkIssueReturned($borrower->{'borrowernumber'}, $iteminformation->{'itemnumber'},$circControlBranch);
             $messages->{'WasReturned'} = 1;    # FIXME is the "= 1" right?
         }
     
@@ -1356,19 +1464,65 @@ sub AddReturn {
     return ( $doreturn, $messages, $iteminformation, $borrower );
 }
 
+=head2 ForceReturn
+
+ForceReturn( $barcode, $date, $branchcode );
+
+Returns an item is if it were returned on C<$date>.
+
+This function is non-interactive and does not check for reserves.
+
+C<$barcode> is the barcode of the item being returned.
+
+C<$date> is the date of the actual return, in the format YYYY-MM-DD.
+
+C<$branchcode> is the branchcode for the library the item was returned to.
+
+=cut
+
+sub ForceReturn {
+  my ( $barcode, $date, $branchcode ) = @_;
+  my $dbh = C4::Context->dbh;
+    
+  my $item = GetBiblioFromItemNumber( undef, $barcode );
+      
+  ## FIXME: Is there a way to get the borrower of an item through the Koha API?
+  my $sth=$dbh->prepare( "SELECT borrowernumber FROM issues WHERE itemnumber = ? AND returndate IS NULL");
+  $sth->execute( $item->{'itemnumber'} );
+  my ( $borrowernumber ) = $sth->fetchrow;
+  $sth->finish();
+                
+  ## Move the issue from issues to old_issues
+  $sth = $dbh->prepare( "INSERT INTO old_issues ( SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL )" );
+  $sth->execute( $item->{'itemnumber'} );
+  $sth->finish();
+  ## Delete the row in issues
+  $sth = $dbh->prepare( "DELETE FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
+  $sth->execute( $item->{'itemnumber'} );
+  $sth->finish();
+  ## Now set the returndate
+  $sth = $dbh->prepare( 'UPDATE old_issues SET returndate = ? WHERE itemnumber = ? AND returndate IS NULL' );
+  $sth->execute( $date, $item->{'itemnumber'} );
+  $sth->finish();
+                                          
+  UpdateStats( $branchcode, 'return', my $amount, my $other, $item->{ 'itemnumber' }, $item->{ 'itemtype' }, $borrowernumber );
+}
+
+
 =head2 MarkIssueReturned
 
 =over 4
 
-MarkIssueReturned($borrowernumber, $itemnumber);
+MarkIssueReturned($borrowernumber, $itemnumber, $dropbox_branch);
 
 =back
 
 Unconditionally marks an issue as being returned by
 moving the C<issues> row to C<old_issues> and
 setting C<returndate> to the current date, or
-yesterday if C<dropbox> is true.  Assumes you've 
-already checked that yesterday > issuedate.
+the last non-holiday date of the branccode specified in
+C<dropbox> .  Assumes you've already checked that 
+it's safe to do this, i.e. last non-holiday > issuedate.
 
 Ideally, this function would be internal to C<C4::Circulation>,
 not exported, but it is currently needed by one 
@@ -1377,14 +1531,14 @@ routine in C<C4::Accounts>.
 =cut
 
 sub MarkIssueReturned {
-    my ($borrowernumber, $itemnumber, $dropbox) = @_;
+    my ($borrowernumber, $itemnumber, $dropbox_branch ) = @_;
        my $dbh = C4::Context->dbh;
        my $query = "UPDATE issues SET returndate=";
        my @bind = ($borrowernumber,$itemnumber);
-       if($dropbox) {
-               my @datearr = localtime( time() );
-               my @yesterdayarr =  Add_Delta_Days( $datearr[5] + 1900 , $datearr[4] + 1, $datearr[3] , -1 );
-               unshift @bind, sprintf("%0.4d-%0.2d-%0.2d",@yesterdayarr) ;
+       if($dropbox_branch) {
+               my $calendar = C4::Calendar->new(  branchcode => $dropbox_branch );
+               my $dropboxdate = $calendar->addDate(C4::Dates->new(), -1 );
+               unshift @bind, $dropboxdate->output('iso') ;
                $query .= " ? "
        } else {
                $query .= " now() ";
@@ -1405,18 +1559,21 @@ sub MarkIssueReturned {
 
 =head2 FixOverduesOnReturn
 
-    &FixOverduesOnReturn($brn,$itm, $exemptfine);
+    &FixOverduesOnReturn($brn,$itm, $exemptfine, $dropboxmode);
 
 C<$brn> borrowernumber
 
 C<$itm> itemnumber
 
+C<$exemptfine> BOOL -- remove overdue charge associated with this issue. 
+C<$dropboxmode> BOOL -- remove lastincrement on overdue charge associated with this issue.
+
 internal function, called only by AddReturn
 
 =cut
 
 sub FixOverduesOnReturn {
-    my ( $borrowernumber, $item, $exemptfine ) = @_;
+    my ( $borrowernumber, $item, $exemptfine, $dropbox ) = @_;
     my $dbh = C4::Context->dbh;
 
     # check for overdue fine
@@ -1429,10 +1586,30 @@ sub FixOverduesOnReturn {
     # alter fine to show that the book has been returned
    my $data; 
        if ($data = $sth->fetchrow_hashref) {
-        my $uquery =($exemptfine)? "update accountlines set accounttype='FFOR', amountoutstanding=0":"update accountlines set accounttype='F' ";
+        my $uquery;
+               my @bind = ($borrowernumber,$item ,$data->{'accountno'});
+               if ($exemptfine) {
+                       $uquery = "update accountlines set accounttype='FFOR', amountoutstanding=0";
+                       if (C4::Context->preference("FinesLog")) {
+                       &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
+                       }
+               } elsif ($dropbox && $data->{lastincrement}) {
+                       my $outstanding = $data->{amountoutstanding} - $data->{lastincrement} ;
+                       my $amt = $data->{amount} - $data->{lastincrement} ;
+                       if (C4::Context->preference("FinesLog")) {
+                       &logaction("FINES", 'MODIFY',$borrowernumber,"Dropbox adjustment $amt, item $item");
+                       }
+                        $uquery = "update accountlines set accounttype='F' ";
+                        if($outstanding  >= 0 && $amt >=0) {
+                               $uquery .= ", amount = ? , amountoutstanding=? ";
+                               unshift @bind, ($amt, $outstanding) ;
+                       }
+               } else {
+                       $uquery = "update accountlines set accounttype='F' ";
+               }
                $uquery .= " where (borrowernumber = ?) and (itemnumber = ?) and (accountno = ?)";
         my $usth = $dbh->prepare($uquery);
-        $usth->execute($borrowernumber,$item ,$data->{'accountno'});
+        $usth->execute(@bind);
         $usth->finish();
     }
 
@@ -1456,7 +1633,6 @@ Internal function, called by AddReturn
 
 sub FixAccountForLostAndReturned {
        my ($iteminfo, $borrower) = @_;
-       my %env;
        my $dbh = C4::Context->dbh;
        my $itm = $iteminfo->{'itemnumber'};
        # check for charge made for lost book
@@ -1481,7 +1657,7 @@ sub FixAccountForLostAndReturned {
                $usth->execute($data->{'borrowernumber'},$itm,$acctno);
                $usth->finish;
        #check if any credit is left if so writeoff other accounts
-               my $nextaccntno = getnextacctno(\%env,$data->{'borrowernumber'},$dbh);
+               my $nextaccntno = getnextacctno($data->{'borrowernumber'});
                if ($amountleft < 0){
                $amountleft*=-1;
                }
@@ -1573,6 +1749,28 @@ sub GetItemIssue {
     return ($data);
 }
 
+=head2 GetOpenIssue
+
+$issue = GetOpenIssue( $itemnumber );
+
+Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
+
+C<$itemnumber> is the item's itemnumber
+
+Returns a hashref
+
+=cut
+
+sub GetOpenIssue {
+  my ( $itemnumber ) = @_;
+
+  my $dbh = C4::Context->dbh;  
+  my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
+  $sth->execute( $itemnumber );
+  my $issue = $sth->fetchrow_hashref();
+  return $issue;
+}
+
 =head2 GetItemIssues
 
 $issues = &GetItemIssues($itemnumber, $history);
@@ -1669,6 +1867,40 @@ sub GetBiblioIssues {
     return \@issues;
 }
 
+=head2 GetUpcomingDueIssues
+
+=over 4
+my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
+
+=back
+
+=cut
+
+sub GetUpcomingDueIssues {
+    my $params = shift;
+
+    $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
+    my $dbh = C4::Context->dbh;
+
+    my $statement = <<END_SQL;
+SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due
+FROM issues 
+LEFT JOIN items USING (itemnumber)
+WhERE returndate is NULL
+AND ( TO_DAYS( NOW() )-TO_DAYS( date_due ) ) < ?
+END_SQL
+
+    my @bind_parameters = ( $params->{'days_in_advance'} );
+    
+    my $sth = $dbh->prepare( $statement );
+    $sth->execute( @bind_parameters );
+    my $upcoming_dues = $sth->fetchall_arrayref({});
+    $sth->finish;
+
+    return $upcoming_dues;
+}
+
 =head2 CanBookBeRenewed
 
 ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber);
@@ -1801,7 +2033,7 @@ sub AddRenewal {
     # Update the issues record to have the new due date, and a new count
     # of how many times it has been renewed.
     my $renews = $issuedata->{'renewals'} + 1;
-    $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?
+    $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = CURRENT_DATE
                             WHERE borrowernumber=? 
                             AND itemnumber=?"
     );
@@ -1832,9 +2064,43 @@ sub AddRenewal {
         $sth->finish;
     }
     # Log the renewal
-    UpdateStats( $branch, 'renew', $charge, '', $itemnumber );
+    UpdateStats( $branch, 'renew', $charge, '', $itemnumber, $item->{itype}, $borrowernumber);
 }
 
+
+=head2 ForceRenewal
+
+ForRenewal( $itemnumber, $date, $date_due );
+
+Renews an item for the given date. This function should only be used to update renewals that have occurred in the past.
+
+C<$itemnumber> is the itemnumber of the item being renewed.
+
+C<$date> is the date the renewal took place, in the format YYYY-MM-DD
+
+C<$date_due> is the date the item is now due to be returned, in the format YYYY-MM-DD
+
+=cut
+
+sub ForceRenewal {
+  my ( $itemnumber, $date, $date_due ) = @_;
+  my $dbh = C4::Context->dbh;
+
+  my $sth = $dbh->prepare("SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL");
+  $sth->execute( $itemnumber );
+  my $issue = $sth->fetchrow_hashref();
+  $sth->finish();
+  
+
+  $sth = $dbh->prepare('UPDATE issues SET renewals = ?, lastreneweddate = ?, date_due = ? WHERE itemnumber = ? AND returndate IS NULL');
+  $sth->execute( $issue->{'renewals'} + 1, $date, $date_due, $itemnumber );
+  $sth->finish();
+  
+  my $item = GetBiblioFromItemNumber( $itemnumber );
+  UpdateStats( $issue->{'branchcode'}, 'renew', undef, undef, $itemnumber, $item->{ 'itemtype' }, $issue->{'borrowernumber'} );
+}
+
+
 sub GetRenewCount {
     # check renewal status
     my ($bornum,$itemno)=@_;