Bug 26515: Don't need to call safe_to_delete
[koha.git] / Koha / Item.pm
blob5114a87db34df6d965f97fc4075278691b4e7444
1 package Koha::Item;
3 # Copyright ByWater Solutions 2014
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20 use Modern::Perl;
22 use Carp;
23 use List::MoreUtils qw(any);
24 use Data::Dumper;
25 use Try::Tiny;
27 use Koha::Database;
28 use Koha::DateUtils qw( dt_from_string );
30 use C4::Context;
31 use C4::Circulation;
32 use C4::Reserves;
33 use C4::ClassSource; # FIXME We would like to avoid that
34 use C4::Log qw( logaction );
36 use Koha::Checkouts;
37 use Koha::CirculationRules;
38 use Koha::CoverImages;
39 use Koha::SearchEngine::Indexer;
40 use Koha::Item::Transfer::Limits;
41 use Koha::Item::Transfers;
42 use Koha::ItemTypes;
43 use Koha::Patrons;
44 use Koha::Plugins;
45 use Koha::Libraries;
46 use Koha::StockRotationItem;
47 use Koha::StockRotationRotas;
49 use base qw(Koha::Object);
51 =head1 NAME
53 Koha::Item - Koha Item object class
55 =head1 API
57 =head2 Class methods
59 =cut
61 =head3 store
63 $item->store;
65 $params can take an optional 'skip_record_index' parameter.
66 If set, the reindexation process will not happen (index_records not called)
68 NOTE: This is a temporary fix to answer a performance issue when lot of items
69 are added (or modified) at the same time.
70 The correct way to fix this is to make the ES reindexation process async.
71 You should not turn it on if you do not understand what it is doing exactly.
73 =cut
75 sub store {
76 my $self = shift;
77 my $params = @_ ? shift : {};
79 my $log_action = $params->{log_action} // 1;
81 # We do not want to oblige callers to pass this value
82 # Dev conveniences vs performance?
83 unless ( $self->biblioitemnumber ) {
84 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
87 # See related changes from C4::Items::AddItem
88 unless ( $self->itype ) {
89 $self->itype($self->biblio->biblioitem->itemtype);
92 my $today = dt_from_string;
93 my $plugin_action = 'create';
95 unless ( $self->in_storage ) { #AddItem
96 unless ( $self->permanent_location ) {
97 $self->permanent_location($self->location);
99 unless ( $self->replacementpricedate ) {
100 $self->replacementpricedate($today);
102 unless ( $self->datelastseen ) {
103 $self->datelastseen($today);
106 unless ( $self->dateaccessioned ) {
107 $self->dateaccessioned($today);
110 if ( $self->itemcallnumber
111 or $self->cn_source )
113 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
114 $self->cn_sort($cn_sort);
117 logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
118 if $log_action && C4::Context->preference("CataloguingLog");
120 } else { # ModItem
122 $plugin_action = 'modify';
124 my %updated_columns = $self->_result->get_dirty_columns;
125 return $self->SUPER::store unless %updated_columns;
127 # Retrieve the item for comparison if we need to
128 my $pre_mod_item = (
129 exists $updated_columns{itemlost}
130 or exists $updated_columns{withdrawn}
131 or exists $updated_columns{damaged}
132 ) ? $self->get_from_storage : undef;
134 # Update *_on fields if needed
135 # FIXME: Why not for AddItem as well?
136 my @fields = qw( itemlost withdrawn damaged );
137 for my $field (@fields) {
139 # If the field is defined but empty or 0, we are
140 # removing/unsetting and thus need to clear out
141 # the 'on' field
142 if ( exists $updated_columns{$field}
143 && defined( $self->$field )
144 && !$self->$field )
146 my $field_on = "${field}_on";
147 $self->$field_on(undef);
149 # If the field has changed otherwise, we much update
150 # the 'on' field
151 elsif (exists $updated_columns{$field}
152 && $updated_columns{$field}
153 && !$pre_mod_item->$field )
155 my $field_on = "${field}_on";
156 $self->$field_on(
157 DateTime::Format::MySQL->format_datetime(
158 dt_from_string()
164 if ( exists $updated_columns{itemcallnumber}
165 or exists $updated_columns{cn_source} )
167 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
168 $self->cn_sort($cn_sort);
172 if ( exists $updated_columns{location}
173 and $self->location ne 'CART'
174 and $self->location ne 'PROC'
175 and not exists $updated_columns{permanent_location} )
177 $self->permanent_location( $self->location );
180 # If item was lost and has now been found,
181 # reverse any list item charges if necessary.
182 if ( exists $updated_columns{itemlost}
183 and $updated_columns{itemlost} <= 0
184 and $pre_mod_item->itemlost > 0 )
186 $self->_set_found_trigger($pre_mod_item);
189 logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
190 if $log_action && C4::Context->preference("CataloguingLog");
193 unless ( $self->dateaccessioned ) {
194 $self->dateaccessioned($today);
197 my $result = $self->SUPER::store;
198 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
199 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
200 unless $params->{skip_record_index};
201 $self->get_from_storage->_after_item_action_hooks({ action => $plugin_action });
203 return $result;
206 =head3 delete
208 =cut
210 sub delete {
211 my $self = shift;
212 my $params = @_ ? shift : {};
214 # FIXME check the item has no current issues
215 # i.e. raise the appropriate exception
217 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
218 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
219 unless $params->{skip_record_index};
221 $self->_after_item_action_hooks({ action => 'delete' });
223 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
224 if C4::Context->preference("CataloguingLog");
226 return $self->SUPER::delete;
229 =head3 safe_delete
231 =cut
233 sub safe_delete {
234 my $self = shift;
235 my $params = @_ ? shift : {};
237 my $safe_to_delete = $self->safe_to_delete;
238 return $safe_to_delete unless $safe_to_delete eq '1';
240 $self->move_to_deleted;
242 return $self->delete($params);
245 =head3 safe_to_delete
247 returns 1 if the item is safe to delete,
249 "book_on_loan" if the item is checked out,
251 "not_same_branch" if the item is blocked by independent branches,
253 "book_reserved" if the there are holds aganst the item, or
255 "linked_analytics" if the item has linked analytic records.
257 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
259 =cut
261 sub safe_to_delete {
262 my ($self) = @_;
264 return "book_on_loan" if $self->checkout;
266 return "not_same_branch"
267 if defined C4::Context->userenv
268 and !C4::Context->IsSuperLibrarian()
269 and C4::Context->preference("IndependentBranches")
270 and ( C4::Context->userenv->{branch} ne $self->homebranch );
272 # check it doesn't have a waiting reserve
273 return "book_reserved"
274 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
276 return "linked_analytics"
277 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
279 return "last_item_for_hold"
280 if $self->biblio->items->count == 1
281 && $self->biblio->holds->search(
283 itemnumber => undef,
285 )->count;
287 return 1;
290 =head3 move_to_deleted
292 my $is_moved = $item->move_to_deleted;
294 Move an item to the deleteditems table.
295 This can be done before deleting an item, to make sure the data are not completely deleted.
297 =cut
299 sub move_to_deleted {
300 my ($self) = @_;
301 my $item_infos = $self->unblessed;
302 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
303 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
307 =head3 effective_itemtype
309 Returns the itemtype for the item based on whether item level itemtypes are set or not.
311 =cut
313 sub effective_itemtype {
314 my ( $self ) = @_;
316 return $self->_result()->effective_itemtype();
319 =head3 home_branch
321 =cut
323 sub home_branch {
324 my ($self) = @_;
326 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
328 return $self->{_home_branch};
331 =head3 holding_branch
333 =cut
335 sub holding_branch {
336 my ($self) = @_;
338 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
340 return $self->{_holding_branch};
343 =head3 biblio
345 my $biblio = $item->biblio;
347 Return the bibliographic record of this item
349 =cut
351 sub biblio {
352 my ( $self ) = @_;
353 my $biblio_rs = $self->_result->biblio;
354 return Koha::Biblio->_new_from_dbic( $biblio_rs );
357 =head3 biblioitem
359 my $biblioitem = $item->biblioitem;
361 Return the biblioitem record of this item
363 =cut
365 sub biblioitem {
366 my ( $self ) = @_;
367 my $biblioitem_rs = $self->_result->biblioitem;
368 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
371 =head3 checkout
373 my $checkout = $item->checkout;
375 Return the checkout for this item
377 =cut
379 sub checkout {
380 my ( $self ) = @_;
381 my $checkout_rs = $self->_result->issue;
382 return unless $checkout_rs;
383 return Koha::Checkout->_new_from_dbic( $checkout_rs );
386 =head3 holds
388 my $holds = $item->holds();
389 my $holds = $item->holds($params);
390 my $holds = $item->holds({ found => 'W'});
392 Return holds attached to an item, optionally accept a hashref of params to pass to search
394 =cut
396 sub holds {
397 my ( $self,$params ) = @_;
398 my $holds_rs = $self->_result->reserves->search($params);
399 return Koha::Holds->_new_from_dbic( $holds_rs );
402 =head3 get_transfer
404 my $transfer = $item->get_transfer;
406 Return the transfer if the item is in transit or undef
408 =cut
410 sub get_transfer {
411 my ( $self ) = @_;
412 my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
413 return unless $transfer_rs;
414 return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
417 =head3 last_returned_by
419 Gets and sets the last borrower to return an item.
421 Accepts and returns Koha::Patron objects
423 $item->last_returned_by( $borrowernumber );
425 $last_returned_by = $item->last_returned_by();
427 =cut
429 sub last_returned_by {
430 my ( $self, $borrower ) = @_;
432 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
434 if ($borrower) {
435 return $items_last_returned_by_rs->update_or_create(
436 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
438 else {
439 unless ( $self->{_last_returned_by} ) {
440 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
441 if ($result) {
442 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
446 return $self->{_last_returned_by};
450 =head3 can_article_request
452 my $bool = $item->can_article_request( $borrower )
454 Returns true if item can be specifically requested
456 $borrower must be a Koha::Patron object
458 =cut
460 sub can_article_request {
461 my ( $self, $borrower ) = @_;
463 my $rule = $self->article_request_type($borrower);
465 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
466 return q{};
469 =head3 hidden_in_opac
471 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
473 Returns true if item fields match the hidding criteria defined in $rules.
474 Returns false otherwise.
476 Takes HASHref that can have the following parameters:
477 OPTIONAL PARAMETERS:
478 $rules : { <field> => [ value_1, ... ], ... }
480 Note: $rules inherits its structure from the parsed YAML from reading
481 the I<OpacHiddenItems> system preference.
483 =cut
485 sub hidden_in_opac {
486 my ( $self, $params ) = @_;
488 my $rules = $params->{rules} // {};
490 return 1
491 if C4::Context->preference('hidelostitems') and
492 $self->itemlost > 0;
494 my $hidden_in_opac = 0;
496 foreach my $field ( keys %{$rules} ) {
498 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
499 $hidden_in_opac = 1;
500 last;
504 return $hidden_in_opac;
507 =head3 can_be_transferred
509 $item->can_be_transferred({ to => $to_library, from => $from_library })
510 Checks if an item can be transferred to given library.
512 This feature is controlled by two system preferences:
513 UseBranchTransferLimits to enable / disable the feature
514 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
515 for setting the limitations
517 Takes HASHref that can have the following parameters:
518 MANDATORY PARAMETERS:
519 $to : Koha::Library
520 OPTIONAL PARAMETERS:
521 $from : Koha::Library # if not given, item holdingbranch
522 # will be used instead
524 Returns 1 if item can be transferred to $to_library, otherwise 0.
526 To find out whether at least one item of a Koha::Biblio can be transferred, please
527 see Koha::Biblio->can_be_transferred() instead of using this method for
528 multiple items of the same biblio.
530 =cut
532 sub can_be_transferred {
533 my ($self, $params) = @_;
535 my $to = $params->{to};
536 my $from = $params->{from};
538 $to = $to->branchcode;
539 $from = defined $from ? $from->branchcode : $self->holdingbranch;
541 return 1 if $from eq $to; # Transfer to current branch is allowed
542 return 1 unless C4::Context->preference('UseBranchTransferLimits');
544 my $limittype = C4::Context->preference('BranchTransferLimitsType');
545 return Koha::Item::Transfer::Limits->search({
546 toBranch => $to,
547 fromBranch => $from,
548 $limittype => $limittype eq 'itemtype'
549 ? $self->effective_itemtype : $self->ccode
550 })->count ? 0 : 1;
553 =head3 pickup_locations
555 $pickup_locations = $item->pickup_locations( {patron => $patron } )
557 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)
558 and if item can be transferred to each pickup location.
560 =cut
562 sub pickup_locations {
563 my ($self, $params) = @_;
565 my $patron = $params->{patron};
567 my $circ_control_branch =
568 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
569 my $branchitemrule =
570 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
572 my @libs;
573 if(defined $patron) {
574 return \@libs if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
575 return \@libs if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
578 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
579 @libs = $self->home_branch->get_hold_libraries;
580 push @libs, $self->home_branch unless scalar(@libs) > 0;
581 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
582 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
583 @libs = $plib->get_hold_libraries;
584 push @libs, $self->home_branch unless scalar(@libs) > 0;
585 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
586 push @libs, $self->home_branch;
587 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
588 push @libs, $self->holding_branch;
589 } else {
590 @libs = Koha::Libraries->search({
591 pickup_location => 1
592 }, {
593 order_by => ['branchname']
594 })->as_list;
597 my @pickup_locations;
598 foreach my $library (@libs) {
599 if ($library->pickup_location && $self->can_be_transferred({ to => $library })) {
600 push @pickup_locations, $library;
604 return \@pickup_locations;
607 =head3 article_request_type
609 my $type = $item->article_request_type( $borrower )
611 returns 'yes', 'no', 'bib_only', or 'item_only'
613 $borrower must be a Koha::Patron object
615 =cut
617 sub article_request_type {
618 my ( $self, $borrower ) = @_;
620 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
621 my $branchcode =
622 $branch_control eq 'homebranch' ? $self->homebranch
623 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
624 : undef;
625 my $borrowertype = $borrower->categorycode;
626 my $itemtype = $self->effective_itemtype();
627 my $rule = Koha::CirculationRules->get_effective_rule(
629 rule_name => 'article_requests',
630 categorycode => $borrowertype,
631 itemtype => $itemtype,
632 branchcode => $branchcode
636 return q{} unless $rule;
637 return $rule->rule_value || q{}
640 =head3 current_holds
642 =cut
644 sub current_holds {
645 my ( $self ) = @_;
646 my $attributes = { order_by => 'priority' };
647 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
648 my $params = {
649 itemnumber => $self->itemnumber,
650 suspend => 0,
651 -or => [
652 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
653 waitingdate => { '!=' => undef },
656 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
657 return Koha::Holds->_new_from_dbic($hold_rs);
660 =head3 stockrotationitem
662 my $sritem = Koha::Item->stockrotationitem;
664 Returns the stock rotation item associated with the current item.
666 =cut
668 sub stockrotationitem {
669 my ( $self ) = @_;
670 my $rs = $self->_result->stockrotationitem;
671 return 0 if !$rs;
672 return Koha::StockRotationItem->_new_from_dbic( $rs );
675 =head3 add_to_rota
677 my $item = $item->add_to_rota($rota_id);
679 Add this item to the rota identified by $ROTA_ID, which means associating it
680 with the first stage of that rota. Should this item already be associated
681 with a rota, then we will move it to the new rota.
683 =cut
685 sub add_to_rota {
686 my ( $self, $rota_id ) = @_;
687 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
688 return $self;
691 =head3 has_pending_hold
693 my $is_pending_hold = $item->has_pending_hold();
695 This method checks the tmp_holdsqueue to see if this item has been selected for a hold, but not filled yet and returns true or false
697 =cut
699 sub has_pending_hold {
700 my ( $self ) = @_;
701 my $pending_hold = $self->_result->tmp_holdsqueues;
702 return $pending_hold->count ? 1: 0;
705 =head3 as_marc_field
707 my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
708 my $field = $item->as_marc_field({ [ mss => $mss ] });
710 This method returns a MARC::Field object representing the Koha::Item object
711 with the current mappings configuration.
713 =cut
715 sub as_marc_field {
716 my ( $self, $params ) = @_;
718 my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
719 my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
721 my @subfields;
723 my @columns = $self->_result->result_source->columns;
725 foreach my $item_field ( @columns ) {
726 my $mapping = $mss->{ "items.$item_field"}[0];
727 my $tagfield = $mapping->{tagfield};
728 my $tagsubfield = $mapping->{tagsubfield};
729 next if !$tagfield; # TODO: Should we raise an exception instead?
730 # Feels like safe fallback is better
732 push @subfields, $tagsubfield => $self->$item_field
733 if defined $self->$item_field and $item_field ne '';
736 my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
737 push( @subfields, @{$unlinked_item_subfields} )
738 if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
740 my $field;
742 $field = MARC::Field->new(
743 "$item_tag", ' ', ' ', @subfields
744 ) if @subfields;
746 return $field;
749 =head3 renewal_branchcode
751 Returns the branchcode to be recorded in statistics renewal of the item
753 =cut
755 sub renewal_branchcode {
757 my ($self, $params ) = @_;
759 my $interface = C4::Context->interface;
760 my $branchcode;
761 if ( $interface eq 'opac' ){
762 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
763 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
764 $branchcode = 'OPACRenew';
766 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
767 $branchcode = $self->homebranch;
769 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
770 $branchcode = $self->checkout->patron->branchcode;
772 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
773 $branchcode = $self->checkout->branchcode;
775 else {
776 $branchcode = "";
778 } else {
779 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
780 ? C4::Context->userenv->{branch} : $params->{branch};
782 return $branchcode;
785 =head3 cover_images
787 Return the cover images associated with this item.
789 =cut
791 sub cover_images {
792 my ( $self ) = @_;
794 my $cover_image_rs = $self->_result->cover_images;
795 return unless $cover_image_rs;
796 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
799 =head3 _set_found_trigger
801 $self->_set_found_trigger
803 Finds the most recent lost item charge for this item and refunds the patron
804 appropriately, taking into account any payments or writeoffs already applied
805 against the charge.
807 Internal function, not exported, called only by Koha::Item->store.
809 =cut
811 sub _set_found_trigger {
812 my ( $self, $pre_mod_item ) = @_;
814 ## If item was lost, it has now been found, reverse any list item charges if necessary.
815 my $no_refund_after_days =
816 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
817 if ($no_refund_after_days) {
818 my $today = dt_from_string();
819 my $lost_age_in_days =
820 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
821 ->in_units('days');
823 return $self unless $lost_age_in_days < $no_refund_after_days;
826 return $self
827 unless Koha::CirculationRules->get_lostreturn_policy(
829 item => $self,
830 return_branch => C4::Context->userenv
831 ? C4::Context->userenv->{'branch'}
832 : undef,
836 # check for charge made for lost book
837 my $accountlines = Koha::Account::Lines->search(
839 itemnumber => $self->itemnumber,
840 debit_type_code => 'LOST',
841 status => [ undef, { '<>' => 'FOUND' } ]
844 order_by => { -desc => [ 'date', 'accountlines_id' ] }
848 return $self unless $accountlines->count > 0;
850 my $accountline = $accountlines->next;
851 my $total_to_refund = 0;
853 return $self unless $accountline->borrowernumber;
855 my $patron = Koha::Patrons->find( $accountline->borrowernumber );
856 return $self
857 unless $patron; # Patron has been deleted, nobody to credit the return to
858 # FIXME Should not we notify this somewhere
860 my $account = $patron->account;
862 # Use cases
863 if ( $accountline->amount > $accountline->amountoutstanding ) {
865 # some amount has been cancelled. collect the offsets that are not writeoffs
866 # this works because the only way to subtract from this kind of a debt is
867 # using the UI buttons 'Pay' and 'Write off'
868 my $credits_offsets = Koha::Account::Offsets->search(
870 debit_id => $accountline->id,
871 credit_id => { '!=' => undef }, # it is not the debit itself
872 type => { '!=' => 'Writeoff' },
873 amount => { '<' => 0 } # credits are negative on the DB
877 $total_to_refund = ( $credits_offsets->count > 0 )
878 ? $credits_offsets->total * -1 # credits are negative on the DB
879 : 0;
882 my $credit_total = $accountline->amountoutstanding + $total_to_refund;
884 my $credit;
885 if ( $credit_total > 0 ) {
886 my $branchcode =
887 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
888 $credit = $account->add_credit(
890 amount => $credit_total,
891 description => 'Item found ' . $self->itemnumber,
892 type => 'LOST_FOUND',
893 interface => C4::Context->interface,
894 library_id => $branchcode,
895 item_id => $self->itemnumber,
896 issue_id => $accountline->issue_id
900 $credit->apply( { debits => [$accountline] } );
901 $self->{_refunded} = 1;
904 # Update the account status
905 $accountline->status('FOUND');
906 $accountline->store();
908 if ( defined $account and C4::Context->preference('AccountAutoReconcile') ) {
909 $account->reconcile_balance;
912 return $self;
915 =head3 to_api_mapping
917 This method returns the mapping for representing a Koha::Item object
918 on the API.
920 =cut
922 sub to_api_mapping {
923 return {
924 itemnumber => 'item_id',
925 biblionumber => 'biblio_id',
926 biblioitemnumber => undef,
927 barcode => 'external_id',
928 dateaccessioned => 'acquisition_date',
929 booksellerid => 'acquisition_source',
930 homebranch => 'home_library_id',
931 price => 'purchase_price',
932 replacementprice => 'replacement_price',
933 replacementpricedate => 'replacement_price_date',
934 datelastborrowed => 'last_checkout_date',
935 datelastseen => 'last_seen_date',
936 stack => undef,
937 notforloan => 'not_for_loan_status',
938 damaged => 'damaged_status',
939 damaged_on => 'damaged_date',
940 itemlost => 'lost_status',
941 itemlost_on => 'lost_date',
942 withdrawn => 'withdrawn',
943 withdrawn_on => 'withdrawn_date',
944 itemcallnumber => 'callnumber',
945 coded_location_qualifier => 'coded_location_qualifier',
946 issues => 'checkouts_count',
947 renewals => 'renewals_count',
948 reserves => 'holds_count',
949 restricted => 'restricted_status',
950 itemnotes => 'public_notes',
951 itemnotes_nonpublic => 'internal_notes',
952 holdingbranch => 'holding_library_id',
953 timestamp => 'timestamp',
954 location => 'location',
955 permanent_location => 'permanent_location',
956 onloan => 'checked_out_date',
957 cn_source => 'call_number_source',
958 cn_sort => 'call_number_sort',
959 ccode => 'collection_code',
960 materials => 'materials_notes',
961 uri => 'uri',
962 itype => 'item_type',
963 more_subfields_xml => 'extended_subfields',
964 enumchron => 'serial_issue_number',
965 copynumber => 'copy_number',
966 stocknumber => 'inventory_number',
967 new_status => 'new_status'
971 =head3 itemtype
973 my $itemtype = $item->itemtype;
975 Returns Koha object for effective itemtype
977 =cut
979 sub itemtype {
980 my ( $self ) = @_;
981 return Koha::ItemTypes->find( $self->effective_itemtype );
984 =head2 Internal methods
986 =head3 _after_item_action_hooks
988 Helper method that takes care of calling all plugin hooks
990 =cut
992 sub _after_item_action_hooks {
993 my ( $self, $params ) = @_;
995 my $action = $params->{action};
997 Koha::Plugins->call(
998 'after_item_action',
1000 action => $action,
1001 item => $self,
1002 item_id => $self->itemnumber,
1007 =head3 _type
1009 =cut
1011 sub _type {
1012 return 'Item';
1015 =head1 AUTHOR
1017 Kyle M Hall <kyle@bywatersolutions.com>
1019 =cut