Bug 24083: Add support for unseen_renewals
[srvgit] / C4 / Circulation.pm
index 3d40540..6acab27 100644 (file)
@@ -1330,16 +1330,30 @@ sub checkHighHolds {
 
         my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
 
-        my $decreaseLoanHighHoldsDuration = C4::Context->preference('decreaseLoanHighHoldsDuration');
+        my $rule = Koha::CirculationRules->get_effective_rule(
+            {
+                categorycode => $borrower->{categorycode},
+                itemtype     => $item_object->effective_itemtype,
+                branchcode   => $branchcode,
+                rule_name    => 'decreaseloanholds',
+            }
+        );
 
-        my $reduced_datedue = $calendar->addDate( $issuedate, $decreaseLoanHighHoldsDuration );
+        my $duration;
+        if ( defined($rule) && $rule->rule_value ne '' ){
+            # overrides decreaseLoanHighHoldsDuration syspref
+            $duration = $rule->rule_value;
+        } else {
+            $duration = C4::Context->preference('decreaseLoanHighHoldsDuration');
+        }
+        my $reduced_datedue = $calendar->addDate( $issuedate, $duration );
         $reduced_datedue->set_hour($orig_due->hour);
         $reduced_datedue->set_minute($orig_due->minute);
         $reduced_datedue->truncate( to => 'minute' );
 
         if ( DateTime->compare( $reduced_datedue, $orig_due ) == -1 ) {
             $return_data->{exceeded} = 1;
-            $return_data->{duration} = $decreaseLoanHighHoldsDuration;
+            $return_data->{duration} = $duration;
             $return_data->{due_date} = $reduced_datedue;
         }
     }
@@ -1506,6 +1520,17 @@ sub AddIssue {
                 auto_renew      => $auto_renew ? 1 : 0,
             };
 
+            # Get ID of logged in user.  if called from a batch job,
+            # no user session exists and C4::Context->userenv() returns
+            # the scalar '0'. Only do this if the syspref says so
+            if ( C4::Context->preference('RecordStaffUserOnCheckout') ) {
+                my $userenv = C4::Context->userenv();
+                my $usernumber = (ref($userenv) eq 'HASH') ? $userenv->{'number'} : undef;
+                if ($usernumber) {
+                    $issue_attributes->{issuer_id} = $usernumber;
+                }
+            }
+
             # In the case that the borrower has an on-site checkout
             # and SwitchOnSiteCheckouts is enabled this converts it to a regular checkout
             $issue = Koha::Checkouts->find( { itemnumber => $item_object->itemnumber } );
@@ -1520,6 +1545,7 @@ sub AddIssue {
                     }
                 )->store;
             }
+            $issue->discard_changes;
             if ( $item_object->location && $item_object->location eq 'CART'
                 && ( !$item_object->permanent_location || $item_object->permanent_location ne 'CART' ) ) {
             ## Item was moved to cart via UpdateItemLocationOnCheckin, anything issued should be taken off the cart.
@@ -1541,51 +1567,31 @@ sub AddIssue {
             $item_object->datelastseen( dt_from_string()->ymd() );
             $item_object->store({log_action => 0});
 
-            # If the item was lost, it has now been found, restore/charge the overdue if necessary
+            # If the item was lost, it has now been found, charge the overdue if necessary
             if ($was_lost) {
-                my $lostreturn_policy =
-                  Koha::CirculationRules->get_lostreturn_policy(
-                    {
-                        return_branch => C4::Context->userenv->{branch},
-                        item          => $item_object
-                    }
-                  );
-
-                if ($lostreturn_policy) {
-
-                    if ( $lostreturn_policy eq 'charge' ) {
-                        $actualissue //= Koha::Old::Checkouts->search(
-                            { itemnumber => $item_unblessed->{itemnumber} },
-                            {
-                                order_by => { '-desc' => 'returndate' },
-                                rows     => 1
-                            }
-                        )->single;
-                        unless ( exists( $borrower->{branchcode} ) ) {
-                            my $patron = $actualissue->patron;
-                            $borrower = $patron->unblessed;
+                if ( $item_object->{_charge} ) {
+                    $actualissue //= Koha::Old::Checkouts->search(
+                        { itemnumber => $item_unblessed->{itemnumber} },
+                        {
+                            order_by => { '-desc' => 'returndate' },
+                            rows     => 1
                         }
-                        _CalculateAndUpdateFine(
-                            {
-                                issue       => $actualissue,
-                                item        => $item_unblessed,
-                                borrower    => $borrower,
-                                return_date => $issuedate
-                            }
-                        );
-                        _FixOverduesOnReturn( $borrower->{borrowernumber},
-                            $item_object->itemnumber, undef, 'RENEWED' );
-                    }
-                    elsif ( $lostreturn_policy eq 'restore' ) {
-                        _RestoreOverdueForLostAndFound(
-                            $item_object->itemnumber );
-                    }
-
-                    if ( C4::Context->preference('AccountAutoReconcile') ) {
-                        $account->reconcile_balance;
+                    )->single;
+                    unless ( exists( $borrower->{branchcode} ) ) {
+                        my $patron = $actualissue->patron;
+                        $borrower = $patron->unblessed;
                     }
+                    _CalculateAndUpdateFine(
+                        {
+                            issue       => $actualissue,
+                            item        => $item_unblessed,
+                            borrower    => $borrower,
+                            return_date => $issuedate
+                        }
+                    );
+                    _FixOverduesOnReturn( $borrower->{borrowernumber},
+                        $item_object->itemnumber, undef, 'RENEWED' );
                 }
-
             }
 
             # If it costs to borrow this book, charge it to the patron's account.
@@ -2094,46 +2100,32 @@ sub AddReturn {
     my $updated_item = ModDateLastSeen( $item->itemnumber, $leave_item_lost, { skip_record_index => 1 } ); # will unset itemlost if needed
 
     # fix up the accounts.....
-    if ( $item_was_lost ) {
+    if ($item_was_lost) {
         $messages->{'WasLost'} = 1;
         unless ( C4::Context->preference("BlockReturnOfLostItems") ) {
             $messages->{'LostItemFeeRefunded'} = $updated_item->{_refunded};
-
-            my $lostreturn_policy =
-              Koha::CirculationRules->get_lostreturn_policy(
-                {
-                    return_branch => C4::Context->userenv->{branch},
-                    item          => $updated_item
+            $messages->{'LostItemFeeRestored'} = $updated_item->{_restored};
+
+            if ( $updated_item->{_charge} ) {
+                $issue //= Koha::Old::Checkouts->search(
+                    { itemnumber => $item->itemnumber },
+                    { order_by   => { '-desc' => 'returndate' }, rows => 1 } )
+                  ->single;
+                unless ( exists( $patron_unblessed->{branchcode} ) ) {
+                    my $patron = $issue->patron;
+                    $patron_unblessed = $patron->unblessed;
                 }
-              );
-
-            if ($lostreturn_policy) {
-
-                if ( $lostreturn_policy eq 'charge' ) {
-                    $issue //= Koha::Old::Checkouts->search(
-                        { itemnumber => $item->itemnumber },
-                        { order_by   => { '-desc' => 'returndate' }, rows => 1 }
-                    )->single;
-                    unless (exists($patron_unblessed->{branchcode})) {
-                        my $patron = $issue->patron;
-                        $patron_unblessed = $patron->unblessed;
+                _CalculateAndUpdateFine(
+                    {
+                        issue       => $issue,
+                        item        => $item->unblessed,
+                        borrower    => $patron_unblessed,
+                        return_date => $return_date
                     }
-                    _CalculateAndUpdateFine(
-                        {
-                            issue       => $issue,
-                            item        => $item->unblessed,
-                            borrower    => $patron_unblessed,
-                            return_date => $return_date
-                        }
-                    );
-                    _FixOverduesOnReturn( $patron_unblessed->{borrowernumber},
-                        $item->itemnumber, undef, 'RETURNED' );
-                    $messages->{'LostItemFeeCharged'} = 1;
-                }
-                elsif ( $lostreturn_policy eq 'restore' ) {
-                    _RestoreOverdueForLostAndFound( $item->itemnumber );
-                    $messages->{'LostItemFeeRestored'} = 1;
-                }
+                );
+                _FixOverduesOnReturn( $patron_unblessed->{borrowernumber},
+                    $item->itemnumber, undef, 'RETURNED' );
+                $messages->{'LostItemFeeCharged'} = 1;
             }
         }
     }
@@ -2593,66 +2585,6 @@ sub _FixOverduesOnReturn {
     return $result;
 }
 
-=head2 _RestoreOverdueForLostAndFound
-
-   &_RestoreOverdueForLostAndFound( $itemnumber );
-
-C<$itemnumber> itemnumber
-
-Internal function
-
-=cut
-
-sub _RestoreOverdueForLostAndFound {
-    my ( $item ) = @_;
-
-    unless( $item ) {
-        warn "_RestoreOverdueForLostAndFound() not supplied valid itemnumber";
-        return;
-    }
-
-    my $schema = Koha::Database->schema;
-
-    my $result = $schema->txn_do(
-        sub {
-            # check for lost overdue fine
-            my $accountlines = Koha::Account::Lines->search(
-                {
-                    itemnumber      => $item,
-                    debit_type_code => 'OVERDUE',
-                    status          => 'LOST'
-                },
-                {
-                    order_by => { '-desc' => 'date' },
-                    rows     => 1
-                }
-            );
-            return 0 unless $accountlines->count; # no warning, there's just nothing to fix
-
-            # Update status of fine
-            my $overdue = $accountlines->next;
-            $overdue->status('RETURNED')->store();
-
-            # Find related forgive credit
-            my $refunds = $overdue->credits(
-                {
-                    credit_type_code => 'FORGIVEN',
-                    itemnumber       => $item,
-                    status           => [ { '!=' => 'VOID' }, undef ]
-                },
-                { order_by => { '-desc' => 'date' }, rows => 1 }
-            );
-            return 0 unless $refunds->count; # no warning, there's just nothing to fix
-
-            # Revert the forgive credit
-            my $refund = $refunds->next;
-            return $refund->void();
-        }
-    );
-
-    return $result;
-}
-
 =head2 _GetCircControlBranch
 
    my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
@@ -2841,6 +2773,7 @@ sub CanBookBeRenewed {
                     'no_auto_renewal_after_hard_limit',
                     'lengthunit',
                     'norenewalbefore',
+                    'unseen_renewals_allowed'
                 ]
             }
         );
@@ -3019,12 +2952,124 @@ sub CanBookBeRenewed {
 
     return ( 0, "auto_renew" ) if $auto_renew eq "ok" && !$override_limit; # 0 if auto-renewal should not succeed
 
+    return ( 1, undef ) if $override_limit;
+
+    my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
+    my $issuing_rule = Koha::CirculationRules->get_effective_rules(
+        {
+            categorycode => $patron->categorycode,
+            itemtype     => $item->effective_itemtype,
+            branchcode   => $branchcode,
+            rules => [
+                'renewalsallowed',
+                'no_auto_renewal_after',
+                'no_auto_renewal_after_hard_limit',
+                'lengthunit',
+                'norenewalbefore',
+                'unseen_renewals_allowed'
+            ]
+        }
+    );
+
+    return ( 0, "too_many" )
+      if not $issuing_rule->{renewalsallowed} or $issuing_rule->{renewalsallowed} <= $issue->renewals;
+
+    return ( 0, "too_unseen" )
+      if C4::Context->preference('UnseenRenewals') &&
+        $issuing_rule->{unseen_renewals_allowed} &&
+        $issuing_rule->{unseen_renewals_allowed} <= $issue->unseen_renewals;
+
+    my $overduesblockrenewing = C4::Context->preference('OverduesBlockRenewing');
+    my $restrictionblockrenewing = C4::Context->preference('RestrictionBlockRenewing');
+    $patron         = Koha::Patrons->find($borrowernumber); # FIXME Is this really useful?
+    my $restricted  = $patron->is_debarred;
+    my $hasoverdues = $patron->has_overdues;
+
+    if ( $restricted and $restrictionblockrenewing ) {
+        return ( 0, 'restriction');
+    } elsif ( ($hasoverdues and $overduesblockrenewing eq 'block') || ($issue->is_overdue and $overduesblockrenewing eq 'blockitem') ) {
+        return ( 0, 'overdue');
+    }
+
+    if ( $issue->auto_renew ) {
+
+        if ( $patron->category->effective_BlockExpiredPatronOpacActions and $patron->is_expired ) {
+            return ( 0, 'auto_account_expired' );
+        }
+
+        if ( defined $issuing_rule->{no_auto_renewal_after}
+                and $issuing_rule->{no_auto_renewal_after} ne "" ) {
+            # Get issue_date and add no_auto_renewal_after
+            # If this is greater than today, it's too late for renewal.
+            my $maximum_renewal_date = dt_from_string($issue->issuedate, 'sql');
+            $maximum_renewal_date->add(
+                $issuing_rule->{lengthunit} => $issuing_rule->{no_auto_renewal_after}
+            );
+            my $now = dt_from_string;
+            if ( $now >= $maximum_renewal_date ) {
+                return ( 0, "auto_too_late" );
+            }
+        }
+        if ( defined $issuing_rule->{no_auto_renewal_after_hard_limit}
+                      and $issuing_rule->{no_auto_renewal_after_hard_limit} ne "" ) {
+            # If no_auto_renewal_after_hard_limit is >= today, it's also too late for renewal
+            if ( dt_from_string >= dt_from_string( $issuing_rule->{no_auto_renewal_after_hard_limit} ) ) {
+                return ( 0, "auto_too_late" );
+            }
+        }
+
+        if ( C4::Context->preference('OPACFineNoRenewalsBlockAutoRenew') ) {
+            my $fine_no_renewals = C4::Context->preference("OPACFineNoRenewals");
+            my $amountoutstanding =
+              C4::Context->preference("OPACFineNoRenewalsIncludeCredit")
+              ? $patron->account->balance
+              : $patron->account->outstanding_debits->total_outstanding;
+            if ( $amountoutstanding and $amountoutstanding > $fine_no_renewals ) {
+                return ( 0, "auto_too_much_oweing" );
+            }
+        }
+    }
+
+    if ( defined $issuing_rule->{norenewalbefore}
+        and $issuing_rule->{norenewalbefore} ne "" )
+    {
+
+        # Calculate soonest renewal by subtracting 'No renewal before' from due date
+        my $soonestrenewal = dt_from_string( $issue->date_due, 'sql' )->subtract(
+            $issuing_rule->{lengthunit} => $issuing_rule->{norenewalbefore} );
+
+        # Depending on syspref reset the exact time, only check the date
+        if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
+            and $issuing_rule->{lengthunit} eq 'days' )
+        {
+            $soonestrenewal->truncate( to => 'day' );
+        }
+
+        if ( $soonestrenewal > DateTime->now( time_zone => C4::Context->tz() ) )
+        {
+            return ( 0, "auto_too_soon" ) if $issue->auto_renew;
+            return ( 0, "too_soon" );
+        }
+        elsif ( $issue->auto_renew ) {
+            return ( 0, "auto_renew" );
+        }
+    }
+
+    # Fallback for automatic renewals:
+    # If norenewalbefore is undef, don't renew before due date.
+    if ( $issue->auto_renew ) {
+        my $now = dt_from_string;
+        return ( 0, "auto_renew" )
+          if $now >= dt_from_string( $issue->date_due, 'sql' );
+        return ( 0, "auto_too_soon" );
+    }
+
     return ( 1, undef );
 }
 
 =head2 AddRenewal
 
-  &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
+  &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate], [$seen]);
 
 Renews a loan.
 
@@ -3049,6 +3094,10 @@ syspref)
 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
 from the book's item type.
 
+C<$seen> is a boolean flag indicating if the item was seen or not during the renewal. This
+informs the incrementing of the unseen_renewals column. If this flag is not supplied, we
+fallback to a true value
+
 =cut
 
 sub AddRenewal {
@@ -3058,6 +3107,10 @@ sub AddRenewal {
     my $datedue         = shift;
     my $lastreneweddate = shift || dt_from_string();
     my $skipfinecalc    = shift;
+    my $seen            = shift;
+
+    # Fallback on a 'seen' renewal
+    $seen = defined $seen && $seen == 0 ? 0 : 1;
 
     my $item_object   = Koha::Items->find($itemnumber) or return;
     my $biblio = $item_object->biblio;
@@ -3110,15 +3163,35 @@ sub AddRenewal {
             }
         );
 
+        # Increment the unseen renewals, if appropriate
+        # We only do so if the syspref is enabled and
+        # a maximum value has been set in the circ rules
+        my $unseen_renewals = $issue->unseen_renewals;
+        if (C4::Context->preference('UnseenRenewals')) {
+            my $rule = Koha::CirculationRules->get_effective_rule(
+                {   categorycode => $patron->categorycode,
+                    itemtype     => $item_object->effective_itemtype,
+                    branchcode   => $circ_library->branchcode,
+                    rule_name    => 'unseen_renewals_allowed'
+                }
+            );
+            if (!$seen && $rule && $rule->rule_value) {
+                $unseen_renewals++;
+            } else {
+                # If the renewal is seen, unseen should revert to 0
+                $unseen_renewals = 0;
+            }
+        }
+
         # Update the issues record to have the new due date, and a new count
         # of how many times it has been renewed.
         my $renews = ( $issue->renewals || 0 ) + 1;
-        my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
+        my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, unseen_renewals = ?, lastreneweddate = ?
                                 WHERE borrowernumber=?
                                 AND itemnumber=?"
         );
 
-        $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
+        $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $unseen_renewals, $lastreneweddate, $borrowernumber, $itemnumber );
 
         # Update the renewal count on the item, and tell zebra to reindex
         $renews = ( $item_object->renewals || 0 ) + 1;
@@ -3205,8 +3278,11 @@ sub GetRenewCount {
     my ( $bornum, $itemno ) = @_;
     my $dbh           = C4::Context->dbh;
     my $renewcount    = 0;
+    my $unseencount    = 0;
     my $renewsallowed = 0;
+    my $unseenallowed = 0;
     my $renewsleft    = 0;
+    my $unseenleft    = 0;
 
     my $patron = Koha::Patrons->find( $bornum );
     my $item   = Koha::Items->find($itemno);
@@ -3225,22 +3301,34 @@ sub GetRenewCount {
     $sth->execute( $bornum, $itemno );
     my $data = $sth->fetchrow_hashref;
     $renewcount = $data->{'renewals'} if $data->{'renewals'};
+    $unseencount = $data->{'unseen_renewals'} if $data->{'unseen_renewals'};
     # $item and $borrower should be calculated
     my $branchcode = _GetCircControlBranch($item->unblessed, $patron->unblessed);
 
-    my $rule = Koha::CirculationRules->get_effective_rule(
+    my $rules = Koha::CirculationRules->get_effective_rules(
         {
             categorycode => $patron->categorycode,
             itemtype     => $item->effective_itemtype,
             branchcode   => $branchcode,
-            rule_name    => 'renewalsallowed',
+            rules        => [ 'renewalsallowed', 'unseen_renewals_allowed' ]
         }
     );
-
-    $renewsallowed = $rule ? $rule->rule_value : 0;
+    $renewsallowed = $rules ? $rules->{renewalsallowed} : 0;
+    $unseenallowed = $rules->{unseen_renewals_allowed} ?
+        $rules->{unseen_renewals_allowed} :
+        0;
     $renewsleft    = $renewsallowed - $renewcount;
+    $unseenleft    = $unseenallowed - $unseencount;
     if($renewsleft < 0){ $renewsleft = 0; }
-    return ( $renewcount, $renewsallowed, $renewsleft );
+    if($unseenleft < 0){ $unseenleft = 0; }
+    return (
+        $renewcount,
+        $renewsallowed,
+        $renewsleft,
+        $unseencount,
+        $unseenallowed,
+        $unseenleft
+    );
 }
 
 =head2 GetSoonestRenewDate