Bug 26265: (QA follow-up) Remove g option from regex, add few dirs
[koha-equinox.git] / Koha / Item.pm
index c760e05..8dffac4 100644 (file)
@@ -38,6 +38,7 @@ use Koha::Checkouts;
 use Koha::CirculationRules;
 use Koha::Item::Transfer::Limits;
 use Koha::Item::Transfers;
+use Koha::ItemTypes;
 use Koha::Patrons;
 use Koha::Plugins;
 use Koha::Libraries;
@@ -58,10 +59,21 @@ Koha::Item - Koha Item object class
 
 =head3 store
 
+    $item->store;
+
+$params can take an optional 'skip_modzebra_update' parameter.
+If set, the reindexation process will not happen (ModZebra not called)
+
+NOTE: This is a temporary fix to answer a performance issue when lot of items
+are added (or modified) at the same time.
+The correct way to fix this is to make the ES reindexation process async.
+You should not turn it on if you do not understand what it is doing exactly.
+
 =cut
 
 sub store {
-    my ($self, $params) = @_;
+    my $self = shift;
+    my $params = @_ ? shift : {};
 
     my $log_action = $params->{log_action} // 1;
 
@@ -76,11 +88,6 @@ sub store {
         $self->itype($self->biblio->biblioitem->itemtype);
     }
 
-    if ( $self->itemcallnumber ) { # This could be improved, we should recalculate it only if changed
-        my $cn_sort = GetClassSort($self->cn_source, $self->itemcallnumber, "");
-        $self->cn_sort($cn_sort);
-    }
-
     my $today = dt_from_string;
     unless ( $self->in_storage ) { #AddItem
         unless ( $self->permanent_location ) {
@@ -97,7 +104,15 @@ sub store {
             $self->dateaccessioned($today);
         }
 
-        C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
+        if (   $self->itemcallnumber
+            or $self->cn_source )
+        {
+            my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
+            $self->cn_sort($cn_sort);
+        }
+
+        C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" )
+            unless $params->{skip_modzebra_update};
 
         logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
           if $log_action && C4::Context->preference("CataloguingLog");
@@ -106,37 +121,54 @@ sub store {
 
     } else { # ModItem
 
-        { # Update *_on  fields if needed
-          # Why not for AddItem as well?
-            my @fields = qw( itemlost withdrawn damaged );
-
-            # Only retrieve the item if we need to set an "on" date field
-            if ( $self->itemlost || $self->withdrawn || $self->damaged ) {
-                my $pre_mod_item = $self->get_from_storage;
-                for my $field (@fields) {
-                    if (    $self->$field
-                        and not $pre_mod_item->$field )
-                    {
-                        my $field_on = "${field}_on";
-                        $self->$field_on(
-                          DateTime::Format::MySQL->format_datetime( dt_from_string() )
-                        );
-                    }
-                }
-            }
+        my %updated_columns = $self->_result->get_dirty_columns;
+        return $self->SUPER::store unless %updated_columns;
 
-            # If the field is defined but empty, we are removing and,
-            # and thus need to clear out the 'on' field as well
-            for my $field (@fields) {
-                if ( defined( $self->$field ) && !$self->$field ) {
-                    my $field_on = "${field}_on";
-                    $self->$field_on(undef);
-                }
+        # Retrieve the item for comparison if we need to
+        my $pre_mod_item = (
+                 exists $updated_columns{itemlost}
+              or exists $updated_columns{withdrawn}
+              or exists $updated_columns{damaged}
+        ) ? $self->get_from_storage : undef;
+
+        # Update *_on  fields if needed
+        # FIXME: Why not for AddItem as well?
+        my @fields = qw( itemlost withdrawn damaged );
+        for my $field (@fields) {
+
+            # If the field is defined but empty or 0, we are
+            # removing/unsetting and thus need to clear out
+            # the 'on' field
+            if (   exists $updated_columns{$field}
+                && defined( $self->$field )
+                && !$self->$field )
+            {
+                my $field_on = "${field}_on";
+                $self->$field_on(undef);
+            }
+            # If the field has changed otherwise, we much update
+            # the 'on' field
+            elsif (exists $updated_columns{$field}
+                && $updated_columns{$field}
+                && !$pre_mod_item->$field )
+            {
+                my $field_on = "${field}_on";
+                $self->$field_on(
+                    DateTime::Format::MySQL->format_datetime(
+                        dt_from_string()
+                    )
+                );
             }
         }
 
-        my %updated_columns = $self->_result->get_dirty_columns;
-        return $self->SUPER::store unless %updated_columns;
+        if (   exists $updated_columns{itemcallnumber}
+            or exists $updated_columns{cn_source} )
+        {
+            my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
+            $self->cn_sort($cn_sort);
+        }
+
+
         if (    exists $updated_columns{location}
             and $self->location ne 'CART'
             and $self->location ne 'PROC'
@@ -145,9 +177,18 @@ sub store {
             $self->permanent_location( $self->location );
         }
 
-        $self->timestamp(undef) if $self->timestamp; # Maybe move this to Koha::Object->store?
+        # If item was lost and has now been found,
+        # reverse any list item charges if necessary.
+        if (    exists $updated_columns{itemlost}
+            and $updated_columns{itemlost} <= 0
+            and $pre_mod_item->itemlost > 0 )
+        {
+            $self->_set_found_trigger($pre_mod_item);
+            $self->paidfor('');
+        }
 
-        C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
+        C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" )
+            unless $params->{skip_modzebra_update};
 
         $self->_after_item_action_hooks({ action => 'modify' });
 
@@ -167,12 +208,14 @@ sub store {
 =cut
 
 sub delete {
-    my ( $self ) = @_;
+    my $self = shift;
+    my $params = @_ ? shift : {};
 
     # FIXME check the item has no current issues
     # i.e. raise the appropriate exception
 
-    C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" );
+    C4::Biblio::ModZebra( $self->biblionumber, "specialUpdate", "biblioserver" )
+        unless $params->{skip_modzebra_update};
 
     $self->_after_item_action_hooks({ action => 'delete' });
 
@@ -187,14 +230,15 @@ sub delete {
 =cut
 
 sub safe_delete {
-    my ($self) = @_;
+    my $self = shift;
+    my $params = @_ ? shift : {};
 
     my $safe_to_delete = $self->safe_to_delete;
     return $safe_to_delete unless $safe_to_delete eq '1';
 
     $self->move_to_deleted;
 
-    return $self->delete;
+    return $self->delete($params);
 }
 
 =head3 safe_to_delete
@@ -209,6 +253,8 @@ returns 1 if the item is safe to delete,
 
 "linked_analytics" if the item has linked analytic records.
 
+"last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
+
 =cut
 
 sub safe_to_delete {
@@ -229,6 +275,14 @@ sub safe_to_delete {
     return "linked_analytics"
       if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
 
+    return "last_item_for_hold"
+      if $self->biblio->items->count == 1
+      && $self->biblio->holds->search(
+          {
+              itemnumber => undef,
+          }
+        )->count;
+
     return 1;
 }
 
@@ -497,7 +551,7 @@ sub can_be_transferred {
 
 =head3 pickup_locations
 
-@pickup_locations = $item->pickup_locations( {patron => $patron } )
+$pickup_locations = $item->pickup_locations( {patron => $patron } )
 
 Returns possible pickup locations for this item, according to patron's home library (if patron is defined and holds are allowed only from hold groups)
 and if item can be transferred to each pickup location.
@@ -516,8 +570,8 @@ sub pickup_locations {
 
     my @libs;
     if(defined $patron) {
-        return @libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
-        return @libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
+        return \@libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
+        return \@libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
     }
 
     if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
@@ -546,7 +600,7 @@ sub pickup_locations {
         }
     }
 
-    return wantarray ? @pickup_locations : \@pickup_locations;
+    return \@pickup_locations;
 }
 
 =head3 article_request_type
@@ -602,16 +656,6 @@ sub current_holds {
     return Koha::Holds->_new_from_dbic($hold_rs);
 }
 
-=head3 holds
-
-=cut
-
-sub holds {
-    my ( $self ) = @_;
-    my $hold_rs = $self->_result->reserves->search;
-    return Koha::Holds->_new_from_dbic($hold_rs);
-}
-
 =head3 stockrotationitem
 
   my $sritem = Koha::Item->stockrotationitem;
@@ -684,7 +728,8 @@ sub as_marc_field {
         next if !$tagfield; # TODO: Should we raise an exception instead?
                             # Feels like safe fallback is better
 
-        push @subfields, $tagsubfield => $self->$item_field;
+        push @subfields, $tagsubfield => $self->$item_field
+            if defined $self->$item_field and $item_field ne '';
     }
 
     my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
@@ -700,6 +745,158 @@ sub as_marc_field {
     return $field;
 }
 
+=head3 renewal_branchcode
+
+Returns the branchcode to be recorded in statistics renewal of the item
+
+=cut
+
+sub renewal_branchcode {
+
+    my ($self, $params ) = @_;
+
+    my $interface = C4::Context->interface;
+    my $branchcode;
+    if ( $interface eq 'opac' ){
+        my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
+        if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
+            $branchcode = 'OPACRenew';
+        }
+        elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
+            $branchcode = $self->homebranch;
+        }
+        elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
+            $branchcode = $self->checkout->patron->branchcode;
+        }
+        elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
+            $branchcode = $self->checkout->branchcode;
+        }
+        else {
+            $branchcode = "";
+        }
+    } else {
+        $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
+            ? C4::Context->userenv->{branch} : $params->{branch};
+    }
+    return $branchcode;
+}
+
+=head3 _set_found_trigger
+
+    $self->_set_found_trigger
+
+Finds the most recent lost item charge for this item and refunds the patron
+appropriately, taking into account any payments or writeoffs already applied
+against the charge.
+
+Internal function, not exported, called only by Koha::Item->store.
+
+=cut
+
+sub _set_found_trigger {
+    my ( $self, $pre_mod_item ) = @_;
+
+    ## If item was lost, it has now been found, reverse any list item charges if necessary.
+    my $no_refund_after_days =
+      C4::Context->preference('NoRefundOnLostReturnedItemsAge');
+    if ($no_refund_after_days) {
+        my $today = dt_from_string();
+        my $lost_age_in_days =
+          dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
+          ->in_units('days');
+
+        return $self unless $lost_age_in_days < $no_refund_after_days;
+    }
+
+    return $self
+      unless Koha::CirculationRules->get_lostreturn_policy(
+        {
+            item          => $self,
+            return_branch => C4::Context->userenv
+            ? C4::Context->userenv->{'branch'}
+            : undef,
+        }
+      );
+
+    # check for charge made for lost book
+    my $accountlines = Koha::Account::Lines->search(
+        {
+            itemnumber      => $self->itemnumber,
+            debit_type_code => 'LOST',
+            status          => [ undef, { '<>' => 'FOUND' } ]
+        },
+        {
+            order_by => { -desc => [ 'date', 'accountlines_id' ] }
+        }
+    );
+
+    return $self unless $accountlines->count > 0;
+
+    my $accountline     = $accountlines->next;
+    my $total_to_refund = 0;
+
+    return $self unless $accountline->borrowernumber;
+
+    my $patron = Koha::Patrons->find( $accountline->borrowernumber );
+    return $self
+      unless $patron;  # Patron has been deleted, nobody to credit the return to
+                       # FIXME Should not we notify this somewhere
+
+    my $account = $patron->account;
+
+    # Use cases
+    if ( $accountline->amount > $accountline->amountoutstanding ) {
+
+    # some amount has been cancelled. collect the offsets that are not writeoffs
+    # this works because the only way to subtract from this kind of a debt is
+    # using the UI buttons 'Pay' and 'Write off'
+        my $credits_offsets = Koha::Account::Offsets->search(
+            {
+                debit_id  => $accountline->id,
+                credit_id => { '!=' => undef },     # it is not the debit itself
+                type      => { '!=' => 'Writeoff' },
+                amount => { '<' => 0 }    # credits are negative on the DB
+            }
+        );
+
+        $total_to_refund = ( $credits_offsets->count > 0 )
+          ? $credits_offsets->total * -1    # credits are negative on the DB
+          : 0;
+    }
+
+    my $credit_total = $accountline->amountoutstanding + $total_to_refund;
+
+    my $credit;
+    if ( $credit_total > 0 ) {
+        my $branchcode =
+          C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
+        $credit = $account->add_credit(
+            {
+                amount      => $credit_total,
+                description => 'Item found ' . $self->itemnumber,
+                type        => 'LOST_FOUND',
+                interface   => C4::Context->interface,
+                library_id  => $branchcode,
+                item_id     => $self->itemnumber,
+                issue_id    => $accountline->issue_id
+            }
+        );
+
+        $credit->apply( { debits => [$accountline] } );
+        $self->{_refunded} = 1;
+    }
+
+    # Update the account status
+    $accountline->status('FOUND');
+    $accountline->store();
+
+    if ( defined $account and C4::Context->preference('AccountAutoReconcile') ) {
+        $account->reconcile_balance;
+    }
+
+    return $self;
+}
+
 =head3 to_api_mapping
 
 This method returns the mapping for representing a Koha::Item object
@@ -757,6 +954,19 @@ sub to_api_mapping {
     };
 }
 
+=head3 itemtype
+
+    my $itemtype = $item->itemtype;
+
+    Returns Koha object for effective itemtype
+
+=cut
+
+sub itemtype {
+    my ( $self ) = @_;
+    return Koha::ItemTypes->find( $self->effective_itemtype );
+}
+
 =head2 Internal methods
 
 =head3 _after_item_action_hooks
@@ -770,24 +980,14 @@ sub _after_item_action_hooks {
 
     my $action = $params->{action};
 
-    if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
-
-        my @plugins = Koha::Plugins->new->GetPlugins({
-            method => 'after_item_action',
-        });
-
-        if (@plugins) {
-
-            foreach my $plugin ( @plugins ) {
-                try {
-                    $plugin->after_item_action({ action => $action, item => $self, item_id => $self->itemnumber });
-                }
-                catch {
-                    warn "$_";
-                };
-            }
+    Koha::Plugins->call(
+        'after_item_action',
+        {
+            action  => $action,
+            item    => $self,
+            item_id => $self->itemnumber,
         }
-    }
+    );
 }
 
 =head3 _type