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>.
22 use List::MoreUtils qw( any );
25 use Koha::DateUtils qw( dt_from_string output_pref );
28 use C4::Circulation qw( barcodedecode GetBranchItemRule );
30 use C4::ClassSource qw( GetClassSort );
31 use C4::Log qw( logaction );
33 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
35 use Koha::CirculationRules;
36 use Koha::CoverImages;
37 use Koha::SearchEngine::Indexer;
38 use Koha::Exceptions::Item::Transfer;
39 use Koha::Item::Transfer::Limits;
40 use Koha::Item::Transfers;
41 use Koha::Item::Attributes;
46 use Koha::StockRotationItem;
47 use Koha::StockRotationRotas;
48 use Koha::TrackedLinks;
49 use Koha::Result::Boolean;
51 use base qw(Koha::Object);
55 Koha::Item - Koha Item object class
67 $params can take an optional 'skip_record_index' parameter.
68 If set, the reindexation process will not happen (index_records not called)
70 NOTE: This is a temporary fix to answer a performance issue when lot of items
71 are added (or modified) at the same time.
72 The correct way to fix this is to make the ES reindexation process async.
73 You should not turn it on if you do not understand what it is doing exactly.
79 my $params = @_ ? shift : {};
81 my $log_action = $params->{log_action} // 1;
83 # We do not want to oblige callers to pass this value
84 # Dev conveniences vs performance?
85 unless ( $self->biblioitemnumber ) {
86 $self->biblioitemnumber( $self->biblio->biblioitem->biblioitemnumber );
89 # See related changes from C4::Items::AddItem
90 unless ( $self->itype ) {
91 $self->itype($self->biblio->biblioitem->itemtype);
94 $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
96 my $today = dt_from_string;
97 my $action = 'create';
99 unless ( $self->in_storage ) { #AddItem
101 unless ( $self->permanent_location ) {
102 $self->permanent_location($self->location);
105 my $default_location = C4::Context->preference('NewItemsDefaultLocation');
106 unless ( $self->location || !$default_location ) {
107 $self->permanent_location( $self->location || $default_location )
108 unless $self->permanent_location;
109 $self->location($default_location);
112 unless ( $self->replacementpricedate ) {
113 $self->replacementpricedate($today);
115 unless ( $self->datelastseen ) {
116 $self->datelastseen($today);
119 unless ( $self->dateaccessioned ) {
120 $self->dateaccessioned($today);
123 if ( $self->itemcallnumber
124 or $self->cn_source )
126 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
127 $self->cn_sort($cn_sort);
134 my %updated_columns = $self->_result->get_dirty_columns;
135 return $self->SUPER::store unless %updated_columns;
137 # Retrieve the item for comparison if we need to
139 exists $updated_columns{itemlost}
140 or exists $updated_columns{withdrawn}
141 or exists $updated_columns{damaged}
142 ) ? $self->get_from_storage : undef;
144 # Update *_on fields if needed
145 # FIXME: Why not for AddItem as well?
146 my @fields = qw( itemlost withdrawn damaged );
147 for my $field (@fields) {
149 # If the field is defined but empty or 0, we are
150 # removing/unsetting and thus need to clear out
152 if ( exists $updated_columns{$field}
153 && defined( $self->$field )
156 my $field_on = "${field}_on";
157 $self->$field_on(undef);
159 # If the field has changed otherwise, we much update
161 elsif (exists $updated_columns{$field}
162 && $updated_columns{$field}
163 && !$pre_mod_item->$field )
165 my $field_on = "${field}_on";
167 DateTime::Format::MySQL->format_datetime(
174 if ( exists $updated_columns{itemcallnumber}
175 or exists $updated_columns{cn_source} )
177 my $cn_sort = GetClassSort( $self->cn_source, $self->itemcallnumber, "" );
178 $self->cn_sort($cn_sort);
182 if ( exists $updated_columns{location}
183 and $self->location ne 'CART'
184 and $self->location ne 'PROC'
185 and not exists $updated_columns{permanent_location} )
187 $self->permanent_location( $self->location );
190 # If item was lost and has now been found,
191 # reverse any list item charges if necessary.
192 if ( exists $updated_columns{itemlost}
193 and $updated_columns{itemlost} <= 0
194 and $pre_mod_item->itemlost > 0 )
196 $self->_set_found_trigger($pre_mod_item);
201 unless ( $self->dateaccessioned ) {
202 $self->dateaccessioned($today);
205 my $result = $self->SUPER::store;
206 if ( $log_action && C4::Context->preference("CataloguingLog") ) {
208 ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
209 : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
211 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
212 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
213 unless $params->{skip_record_index};
214 $self->get_from_storage->_after_item_action_hooks({ action => $action });
216 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
218 biblio_ids => [ $self->biblionumber ]
220 ) unless $params->{skip_holds_queue};
231 my $params = @_ ? shift : {};
233 # FIXME check the item has no current issues
234 # i.e. raise the appropriate exception
236 my $result = $self->SUPER::delete;
238 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
239 $indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
240 unless $params->{skip_record_index};
242 $self->_after_item_action_hooks({ action => 'delete' });
244 logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
245 if C4::Context->preference("CataloguingLog");
247 Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
249 biblio_ids => [ $self->biblionumber ]
262 my $params = @_ ? shift : {};
264 my $safe_to_delete = $self->safe_to_delete;
265 return $safe_to_delete unless $safe_to_delete;
267 $self->move_to_deleted;
269 return $self->delete($params);
272 =head3 safe_to_delete
274 returns 1 if the item is safe to delete,
276 "book_on_loan" if the item is checked out,
278 "not_same_branch" if the item is blocked by independent branches,
280 "book_reserved" if the there are holds aganst the item, or
282 "linked_analytics" if the item has linked analytic records.
284 "last_item_for_hold" if the item is the last one on a record on which a biblio-level hold is placed
293 $error = "book_on_loan" if $self->checkout;
295 $error = "not_same_branch"
296 if defined C4::Context->userenv
297 and !C4::Context->IsSuperLibrarian()
298 and C4::Context->preference("IndependentBranches")
299 and ( C4::Context->userenv->{branch} ne $self->homebranch );
301 # check it doesn't have a waiting reserve
302 $error = "book_reserved"
303 if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
305 $error = "linked_analytics"
306 if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
308 $error = "last_item_for_hold"
309 if $self->biblio->items->count == 1
310 && $self->biblio->holds->search(
317 return Koha::Result::Boolean->new(0)->add_message({ message => $error });
320 return Koha::Result::Boolean->new(1);
323 =head3 move_to_deleted
325 my $is_moved = $item->move_to_deleted;
327 Move an item to the deleteditems table.
328 This can be done before deleting an item, to make sure the data are not completely deleted.
332 sub move_to_deleted {
334 my $item_infos = $self->unblessed;
335 delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
336 return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
340 =head3 effective_itemtype
342 Returns the itemtype for the item based on whether item level itemtypes are set or not.
346 sub effective_itemtype {
349 return $self->_result()->effective_itemtype();
359 $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
361 return $self->{_home_branch};
364 =head3 holding_branch
371 $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
373 return $self->{_holding_branch};
378 my $biblio = $item->biblio;
380 Return the bibliographic record of this item
386 my $biblio_rs = $self->_result->biblio;
387 return Koha::Biblio->_new_from_dbic( $biblio_rs );
392 my $biblioitem = $item->biblioitem;
394 Return the biblioitem record of this item
400 my $biblioitem_rs = $self->_result->biblioitem;
401 return Koha::Biblioitem->_new_from_dbic( $biblioitem_rs );
406 my $checkout = $item->checkout;
408 Return the checkout for this item
414 my $checkout_rs = $self->_result->issue;
415 return unless $checkout_rs;
416 return Koha::Checkout->_new_from_dbic( $checkout_rs );
421 my $holds = $item->holds();
422 my $holds = $item->holds($params);
423 my $holds = $item->holds({ found => 'W'});
425 Return holds attached to an item, optionally accept a hashref of params to pass to search
430 my ( $self,$params ) = @_;
431 my $holds_rs = $self->_result->reserves->search($params);
432 return Koha::Holds->_new_from_dbic( $holds_rs );
435 =head3 request_transfer
437 my $transfer = $item->request_transfer(
441 [ ignore_limits => 0, enqueue => 1, replace => 1 ]
445 Add a transfer request for this item to the given branch for the given reason.
447 An exception will be thrown if the BranchTransferLimits would prevent the requested
448 transfer, unless 'ignore_limits' is passed to override the limits.
450 An exception will be thrown if an active transfer (i.e pending arrival date) is found;
451 The caller should catch such cases and retry the transfer request as appropriate passing
452 an appropriate override.
455 * enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
456 * replace - Used to replace the existing transfer request with your own.
460 sub request_transfer {
461 my ( $self, $params ) = @_;
463 # check for mandatory params
464 my @mandatory = ( 'to', 'reason' );
465 for my $param (@mandatory) {
466 unless ( defined( $params->{$param} ) ) {
467 Koha::Exceptions::MissingParameter->throw(
468 error => "The $param parameter is mandatory" );
472 Koha::Exceptions::Item::Transfer::Limit->throw()
473 unless ( $params->{ignore_limits}
474 || $self->can_be_transferred( { to => $params->{to} } ) );
476 my $request = $self->get_transfer;
477 Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
478 if ( $request && !$params->{enqueue} && !$params->{replace} );
480 $request->cancel( { reason => $params->{reason}, force => 1 } )
481 if ( defined($request) && $params->{replace} );
483 my $transfer = Koha::Item::Transfer->new(
485 itemnumber => $self->itemnumber,
486 daterequested => dt_from_string,
487 frombranch => $self->holdingbranch,
488 tobranch => $params->{to}->branchcode,
489 reason => $params->{reason},
490 comments => $params->{comment}
499 my $transfer = $item->get_transfer;
501 Return the active transfer request or undef
503 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
504 whereby the most recently sent, but not received, transfer will be returned
505 if it exists, otherwise the oldest unsatisfied transfer will be returned.
507 This allows for transfers to queue, which is the case for stock rotation and
508 rotating collections where a manual transfer may need to take precedence but
509 we still expect the item to end up at a final location eventually.
515 my $transfer_rs = $self->_result->branchtransfers->search(
517 datearrived => undef,
518 datecancelled => undef
522 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
526 return unless $transfer_rs;
527 return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
532 my $transfer = $item->get_transfers;
534 Return the list of outstanding transfers (i.e requested but not yet cancelled
537 Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
538 whereby the most recently sent, but not received, transfer will be returned
539 first if it exists, otherwise requests are in oldest to newest request order.
541 This allows for transfers to queue, which is the case for stock rotation and
542 rotating collections where a manual transfer may need to take precedence but
543 we still expect the item to end up at a final location eventually.
549 my $transfer_rs = $self->_result->branchtransfers->search(
551 datearrived => undef,
552 datecancelled => undef
556 [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
559 return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
562 =head3 last_returned_by
564 Gets and sets the last borrower to return an item.
566 Accepts and returns Koha::Patron objects
568 $item->last_returned_by( $borrowernumber );
570 $last_returned_by = $item->last_returned_by();
574 sub last_returned_by {
575 my ( $self, $borrower ) = @_;
577 my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower');
580 return $items_last_returned_by_rs->update_or_create(
581 { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } );
584 unless ( $self->{_last_returned_by} ) {
585 my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } );
587 $self->{_last_returned_by} = Koha::Patrons->find( $result->get_column('borrowernumber') );
591 return $self->{_last_returned_by};
595 =head3 can_article_request
597 my $bool = $item->can_article_request( $borrower )
599 Returns true if item can be specifically requested
601 $borrower must be a Koha::Patron object
605 sub can_article_request {
606 my ( $self, $borrower ) = @_;
608 my $rule = $self->article_request_type($borrower);
610 return 1 if $rule && $rule ne 'no' && $rule ne 'bib_only';
614 =head3 hidden_in_opac
616 my $bool = $item->hidden_in_opac({ [ rules => $rules ] })
618 Returns true if item fields match the hidding criteria defined in $rules.
619 Returns false otherwise.
621 Takes HASHref that can have the following parameters:
623 $rules : { <field> => [ value_1, ... ], ... }
625 Note: $rules inherits its structure from the parsed YAML from reading
626 the I<OpacHiddenItems> system preference.
631 my ( $self, $params ) = @_;
633 my $rules = $params->{rules} // {};
636 if C4::Context->preference('hidelostitems') and
639 my $hidden_in_opac = 0;
641 foreach my $field ( keys %{$rules} ) {
643 if ( any { $self->$field eq $_ } @{ $rules->{$field} } ) {
649 return $hidden_in_opac;
652 =head3 can_be_transferred
654 $item->can_be_transferred({ to => $to_library, from => $from_library })
655 Checks if an item can be transferred to given library.
657 This feature is controlled by two system preferences:
658 UseBranchTransferLimits to enable / disable the feature
659 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
660 for setting the limitations
662 Takes HASHref that can have the following parameters:
663 MANDATORY PARAMETERS:
666 $from : Koha::Library # if not given, item holdingbranch
667 # will be used instead
669 Returns 1 if item can be transferred to $to_library, otherwise 0.
671 To find out whether at least one item of a Koha::Biblio can be transferred, please
672 see Koha::Biblio->can_be_transferred() instead of using this method for
673 multiple items of the same biblio.
677 sub can_be_transferred {
678 my ($self, $params) = @_;
680 my $to = $params->{to};
681 my $from = $params->{from};
683 $to = $to->branchcode;
684 $from = defined $from ? $from->branchcode : $self->holdingbranch;
686 return 1 if $from eq $to; # Transfer to current branch is allowed
687 return 1 unless C4::Context->preference('UseBranchTransferLimits');
689 my $limittype = C4::Context->preference('BranchTransferLimitsType');
690 return Koha::Item::Transfer::Limits->search({
693 $limittype => $limittype eq 'itemtype'
694 ? $self->effective_itemtype : $self->ccode
699 =head3 pickup_locations
701 $pickup_locations = $item->pickup_locations( {patron => $patron } )
703 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)
704 and if item can be transferred to each pickup location.
708 sub pickup_locations {
709 my ($self, $params) = @_;
711 my $patron = $params->{patron};
713 my $circ_control_branch =
714 C4::Reserves::GetReservesControlBranch( $self->unblessed(), $patron->unblessed );
716 C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
718 if(defined $patron) {
719 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
720 return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
723 my $pickup_libraries = Koha::Libraries->search();
724 if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup') {
725 $pickup_libraries = $self->home_branch->get_hold_libraries;
726 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup') {
727 my $plib = Koha::Libraries->find({ branchcode => $patron->branchcode});
728 $pickup_libraries = $plib->get_hold_libraries;
729 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'homebranch') {
730 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->homebranch });
731 } elsif ($branchitemrule->{hold_fulfillment_policy} eq 'holdingbranch') {
732 $pickup_libraries = Koha::Libraries->search({ branchcode => $self->holdingbranch });
735 return $pickup_libraries->search(
740 order_by => ['branchname']
742 ) unless C4::Context->preference('UseBranchTransferLimits');
744 my $limittype = C4::Context->preference('BranchTransferLimitsType');
745 my ($ccode, $itype) = (undef, undef);
746 if( $limittype eq 'ccode' ){
747 $ccode = $self->ccode;
749 $itype = $self->itype;
751 my $limits = Koha::Item::Transfer::Limits->search(
753 fromBranch => $self->holdingbranch,
757 { columns => ['toBranch'] }
760 return $pickup_libraries->search(
762 pickup_location => 1,
764 '-not_in' => $limits->_resultset->as_query
768 order_by => ['branchname']
773 =head3 article_request_type
775 my $type = $item->article_request_type( $borrower )
777 returns 'yes', 'no', 'bib_only', or 'item_only'
779 $borrower must be a Koha::Patron object
783 sub article_request_type {
784 my ( $self, $borrower ) = @_;
786 my $branch_control = C4::Context->preference('HomeOrHoldingBranch');
788 $branch_control eq 'homebranch' ? $self->homebranch
789 : $branch_control eq 'holdingbranch' ? $self->holdingbranch
791 my $borrowertype = $borrower->categorycode;
792 my $itemtype = $self->effective_itemtype();
793 my $rule = Koha::CirculationRules->get_effective_rule(
795 rule_name => 'article_requests',
796 categorycode => $borrowertype,
797 itemtype => $itemtype,
798 branchcode => $branchcode
802 return q{} unless $rule;
803 return $rule->rule_value || q{}
812 my $attributes = { order_by => 'priority' };
813 my $dtf = Koha::Database->new->schema->storage->datetime_parser;
815 itemnumber => $self->itemnumber,
818 reservedate => { '<=' => $dtf->format_date(dt_from_string) },
819 waitingdate => { '!=' => undef },
822 my $hold_rs = $self->_result->reserves->search( $params, $attributes );
823 return Koha::Holds->_new_from_dbic($hold_rs);
826 =head3 stockrotationitem
828 my $sritem = Koha::Item->stockrotationitem;
830 Returns the stock rotation item associated with the current item.
834 sub stockrotationitem {
836 my $rs = $self->_result->stockrotationitem;
838 return Koha::StockRotationItem->_new_from_dbic( $rs );
843 my $item = $item->add_to_rota($rota_id);
845 Add this item to the rota identified by $ROTA_ID, which means associating it
846 with the first stage of that rota. Should this item already be associated
847 with a rota, then we will move it to the new rota.
852 my ( $self, $rota_id ) = @_;
853 Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
857 =head3 has_pending_hold
859 my $is_pending_hold = $item->has_pending_hold();
861 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
865 sub has_pending_hold {
867 my $pending_hold = $self->_result->tmp_holdsqueues;
868 return $pending_hold->count ? 1: 0;
873 my $field = $item->as_marc_field;
875 This method returns a MARC::Field object representing the Koha::Item object
876 with the current mappings configuration.
883 my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
885 my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
889 my $item_field = $tagslib->{$itemtag};
891 my $more_subfields = $self->additional_attributes->to_hashref;
892 foreach my $subfield (
894 $a->{display_order} <=> $b->{display_order}
895 || $a->{subfield} cmp $b->{subfield}
896 } grep { ref($_) && %$_ } values %$item_field
899 my $kohafield = $subfield->{kohafield};
900 my $tagsubfield = $subfield->{tagsubfield};
902 if ( defined $kohafield ) {
903 next if $kohafield !~ m{^items\.}; # That would be weird!
904 ( my $attribute = $kohafield ) =~ s|^items\.||;
905 $value = $self->$attribute # This call may fail if a kohafield is not a DB column but we don't want to add extra work for that there
906 if defined $self->$attribute and $self->$attribute ne '';
908 $value = $more_subfields->{$tagsubfield}
911 next unless defined $value
914 if ( $subfield->{repeatable} ) {
915 my @values = split '\|', $value;
916 push @subfields, ( $tagsubfield => $_ ) for @values;
919 push @subfields, ( $tagsubfield => $value );
924 return unless @subfields;
926 return MARC::Field->new(
927 "$itemtag", ' ', ' ', @subfields
931 =head3 renewal_branchcode
933 Returns the branchcode to be recorded in statistics renewal of the item
937 sub renewal_branchcode {
939 my ($self, $params ) = @_;
941 my $interface = C4::Context->interface;
943 if ( $interface eq 'opac' ){
944 my $renewal_branchcode = C4::Context->preference('OpacRenewalBranch');
945 if( !defined $renewal_branchcode || $renewal_branchcode eq 'opacrenew' ){
946 $branchcode = 'OPACRenew';
948 elsif ( $renewal_branchcode eq 'itemhomebranch' ) {
949 $branchcode = $self->homebranch;
951 elsif ( $renewal_branchcode eq 'patronhomebranch' ) {
952 $branchcode = $self->checkout->patron->branchcode;
954 elsif ( $renewal_branchcode eq 'checkoutbranch' ) {
955 $branchcode = $self->checkout->branchcode;
961 $branchcode = ( C4::Context->userenv && defined C4::Context->userenv->{branch} )
962 ? C4::Context->userenv->{branch} : $params->{branch};
969 Return the cover images associated with this item.
976 my $cover_image_rs = $self->_result->cover_images;
977 return unless $cover_image_rs;
978 return Koha::CoverImages->_new_from_dbic($cover_image_rs);
981 =head3 columns_to_str
983 my $values = $items->columns_to_str;
985 Return a hashref with the string representation of the different attribute of the item.
987 This is meant to be used for display purpose only.
994 my $frameworkcode = $self->biblio->frameworkcode;
995 my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
996 my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
998 my $columns_info = $self->_result->result_source->columns_info;
1000 my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
1002 for my $column ( keys %$columns_info ) {
1004 next if $column eq 'more_subfields_xml';
1006 my $value = $self->$column;
1007 # Maybe we need to deal with datetime columns here, but so far we have damaged_on, itemlost_on and withdrawn_on, and they are not linked with kohafield
1009 if ( not defined $value or $value eq "" ) {
1010 $values->{$column} = $value;
1015 exists $mss->{"items.$column"}
1016 ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
1019 $values->{$column} =
1021 ? $subfield->{authorised_value}
1022 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1023 $subfield->{tagsubfield}, $value, '', $tagslib )
1029 $self->more_subfields_xml
1030 ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
1035 my ( $field ) = $marc_more->fields;
1036 for my $sf ( $field->subfields ) {
1037 my $subfield_code = $sf->[0];
1038 my $value = $sf->[1];
1039 my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
1040 next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
1042 $subfield->{authorised_value}
1043 ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
1044 $subfield->{tagsubfield}, $value, '', $tagslib )
1047 push @{$more_values->{$subfield_code}}, $value;
1050 while ( my ( $k, $v ) = each %$more_values ) {
1051 $values->{$k} = join ' | ', @$v;
1058 =head3 additional_attributes
1060 my $attributes = $item->additional_attributes;
1061 $attributes->{k} = 'new k';
1062 $item->update({ more_subfields => $attributes->to_marcxml });
1064 Returns a Koha::Item::Attributes object that represents the non-mapped
1065 attributes for this item.
1069 sub additional_attributes {
1072 return Koha::Item::Attributes->new_from_marcxml(
1073 $self->more_subfields_xml,
1077 =head3 _set_found_trigger
1079 $self->_set_found_trigger
1081 Finds the most recent lost item charge for this item and refunds the patron
1082 appropriately, taking into account any payments or writeoffs already applied
1085 Internal function, not exported, called only by Koha::Item->store.
1089 sub _set_found_trigger {
1090 my ( $self, $pre_mod_item ) = @_;
1092 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1093 my $no_refund_after_days =
1094 C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1095 if ($no_refund_after_days) {
1096 my $today = dt_from_string();
1097 my $lost_age_in_days =
1098 dt_from_string( $pre_mod_item->itemlost_on )->delta_days($today)
1101 return $self unless $lost_age_in_days < $no_refund_after_days;
1104 my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
1107 return_branch => C4::Context->userenv
1108 ? C4::Context->userenv->{'branch'}
1113 if ( $lostreturn_policy ) {
1115 # refund charge made for lost book
1116 my $lost_charge = Koha::Account::Lines->search(
1118 itemnumber => $self->itemnumber,
1119 debit_type_code => 'LOST',
1120 status => [ undef, { '<>' => 'FOUND' } ]
1123 order_by => { -desc => [ 'date', 'accountlines_id' ] },
1128 if ( $lost_charge ) {
1130 my $patron = $lost_charge->patron;
1133 my $account = $patron->account;
1134 my $total_to_refund = 0;
1137 if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
1139 # some amount has been cancelled. collect the offsets that are not writeoffs
1140 # this works because the only way to subtract from this kind of a debt is
1141 # using the UI buttons 'Pay' and 'Write off'
1142 my $credit_offsets = $lost_charge->debit_offsets(
1144 'credit_id' => { '!=' => undef },
1145 'credit.credit_type_code' => { '!=' => 'Writeoff' }
1147 { join => 'credit' }
1150 $total_to_refund = ( $credit_offsets->count > 0 )
1151 ? $credit_offsets->total * -1 # credits are negative on the DB
1155 my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
1158 if ( $credit_total > 0 ) {
1160 C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
1161 $credit = $account->add_credit(
1163 amount => $credit_total,
1164 description => 'Item found ' . $self->itemnumber,
1165 type => 'LOST_FOUND',
1166 interface => C4::Context->interface,
1167 library_id => $branchcode,
1168 item_id => $self->itemnumber,
1169 issue_id => $lost_charge->issue_id
1173 $credit->apply( { debits => [$lost_charge] } );
1177 message => 'lost_refunded',
1178 payload => { credit_id => $credit->id }
1183 # Update the account status
1184 $lost_charge->status('FOUND');
1185 $lost_charge->store();
1187 # Reconcile balances if required
1188 if ( C4::Context->preference('AccountAutoReconcile') ) {
1189 $account->reconcile_balance;
1194 # restore fine for lost book
1195 if ( $lostreturn_policy eq 'restore' ) {
1196 my $lost_overdue = Koha::Account::Lines->search(
1198 itemnumber => $self->itemnumber,
1199 debit_type_code => 'OVERDUE',
1203 order_by => { '-desc' => 'date' },
1208 if ( $lost_overdue ) {
1210 my $patron = $lost_overdue->patron;
1212 my $account = $patron->account;
1214 # Update status of fine
1215 $lost_overdue->status('FOUND')->store();
1217 # Find related forgive credit
1218 my $refund = $lost_overdue->credits(
1220 credit_type_code => 'FORGIVEN',
1221 itemnumber => $self->itemnumber,
1222 status => [ { '!=' => 'VOID' }, undef ]
1224 { order_by => { '-desc' => 'date' }, rows => 1 }
1228 # Revert the forgive credit
1229 $refund->void({ interface => 'trigger' });
1233 message => 'lost_restored',
1234 payload => { refund_id => $refund->id }
1239 # Reconcile balances if required
1240 if ( C4::Context->preference('AccountAutoReconcile') ) {
1241 $account->reconcile_balance;
1245 } elsif ( $lostreturn_policy eq 'charge' ) {
1249 message => 'lost_charge',
1258 =head3 public_read_list
1260 This method returns the list of publicly readable database fields for both API and UI output purposes
1264 sub public_read_list {
1266 'itemnumber', 'biblionumber', 'homebranch',
1267 'holdingbranch', 'location', 'collectioncode',
1268 'itemcallnumber', 'copynumber', 'enumchron',
1269 'barcode', 'dateaccessioned', 'itemnotes',
1270 'onloan', 'uri', 'itype',
1271 'notforloan', 'damaged', 'itemlost',
1272 'withdrawn', 'restricted'
1276 =head3 to_api_mapping
1278 This method returns the mapping for representing a Koha::Item object
1283 sub to_api_mapping {
1285 itemnumber => 'item_id',
1286 biblionumber => 'biblio_id',
1287 biblioitemnumber => undef,
1288 barcode => 'external_id',
1289 dateaccessioned => 'acquisition_date',
1290 booksellerid => 'acquisition_source',
1291 homebranch => 'home_library_id',
1292 price => 'purchase_price',
1293 replacementprice => 'replacement_price',
1294 replacementpricedate => 'replacement_price_date',
1295 datelastborrowed => 'last_checkout_date',
1296 datelastseen => 'last_seen_date',
1298 notforloan => 'not_for_loan_status',
1299 damaged => 'damaged_status',
1300 damaged_on => 'damaged_date',
1301 itemlost => 'lost_status',
1302 itemlost_on => 'lost_date',
1303 withdrawn => 'withdrawn',
1304 withdrawn_on => 'withdrawn_date',
1305 itemcallnumber => 'callnumber',
1306 coded_location_qualifier => 'coded_location_qualifier',
1307 issues => 'checkouts_count',
1308 renewals => 'renewals_count',
1309 reserves => 'holds_count',
1310 restricted => 'restricted_status',
1311 itemnotes => 'public_notes',
1312 itemnotes_nonpublic => 'internal_notes',
1313 holdingbranch => 'holding_library_id',
1314 timestamp => 'timestamp',
1315 location => 'location',
1316 permanent_location => 'permanent_location',
1317 onloan => 'checked_out_date',
1318 cn_source => 'call_number_source',
1319 cn_sort => 'call_number_sort',
1320 ccode => 'collection_code',
1321 materials => 'materials_notes',
1323 itype => 'item_type_id',
1324 more_subfields_xml => 'extended_subfields',
1325 enumchron => 'serial_issue_number',
1326 copynumber => 'copy_number',
1327 stocknumber => 'inventory_number',
1328 new_status => 'new_status'
1334 my $itemtype = $item->itemtype;
1336 Returns Koha object for effective itemtype
1342 return Koha::ItemTypes->find( $self->effective_itemtype );
1347 my $orders = $item->orders();
1349 Returns a Koha::Acquisition::Orders object
1356 my $orders = $self->_result->item_orders;
1357 return Koha::Acquisition::Orders->_new_from_dbic($orders);
1360 =head3 tracked_links
1362 my $tracked_links = $item->tracked_links();
1364 Returns a Koha::TrackedLinks object
1371 my $tracked_links = $self->_result->linktrackers;
1372 return Koha::TrackedLinks->_new_from_dbic($tracked_links);
1375 =head3 move_to_biblio
1377 $item->move_to_biblio($to_biblio[, $params]);
1379 Move the item to another biblio and update any references in other tables.
1381 The final optional parameter, C<$params>, is expected to contain the
1382 'skip_record_index' key, which is relayed down to Koha::Item->store.
1383 There it prevents calling index_records, which takes most of the
1384 time in batch adds/deletes. The caller must take care of calling
1385 index_records separately.
1388 skip_record_index => 1|0
1390 Returns undef if the move failed or the biblionumber of the destination record otherwise
1394 sub move_to_biblio {
1395 my ( $self, $to_biblio, $params ) = @_;
1399 return if $self->biblionumber == $to_biblio->biblionumber;
1401 my $from_biblionumber = $self->biblionumber;
1402 my $to_biblionumber = $to_biblio->biblionumber;
1404 # Own biblionumber and biblioitemnumber
1406 biblionumber => $to_biblionumber,
1407 biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
1408 })->store({ skip_record_index => $params->{skip_record_index} });
1410 unless ($params->{skip_record_index}) {
1411 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1412 $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
1415 # Acquisition orders
1416 $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1419 $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1421 # hold_fill_target (there's no Koha object available yet)
1422 my $hold_fill_target = $self->_result->hold_fill_target;
1423 if ($hold_fill_target) {
1424 $hold_fill_target->update({ biblionumber => $to_biblionumber });
1427 # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
1428 # and can't even fake one since the significant columns are nullable.
1429 my $storage = $self->_result->result_source->storage;
1432 my ($storage, $dbh, @cols) = @_;
1434 $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
1439 $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
1441 return $to_biblionumber;
1444 =head2 Internal methods
1446 =head3 _after_item_action_hooks
1448 Helper method that takes care of calling all plugin hooks
1452 sub _after_item_action_hooks {
1453 my ( $self, $params ) = @_;
1455 my $action = $params->{action};
1457 Koha::Plugins->call(
1458 'after_item_action',
1462 item_id => $self->itemnumber,
1469 my $recall = $item->recall;
1471 Return the relevant recall for this item
1477 my @recalls = Koha::Recalls->search(
1479 biblio_id => $self->biblionumber,
1482 { order_by => { -asc => 'created_date' } }
1484 foreach my $recall (@recalls) {
1485 if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
1489 # no item-level recall to return, so return earliest biblio-level
1490 # FIXME: eventually this will be based on priority
1494 =head3 can_be_recalled
1496 if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
1498 Does item-level checks and returns if items can be recalled by this borrower
1502 sub can_be_recalled {
1503 my ( $self, $params ) = @_;
1505 return 0 if !( C4::Context->preference('UseRecalls') );
1507 # check if this item is not for loan, withdrawn or lost
1508 return 0 if ( $self->notforloan != 0 );
1509 return 0 if ( $self->itemlost != 0 );
1510 return 0 if ( $self->withdrawn != 0 );
1512 # check if this item is not checked out - if not checked out, can't be recalled
1513 return 0 if ( !defined( $self->checkout ) );
1515 my $patron = $params->{patron};
1517 my $branchcode = C4::Context->userenv->{'branch'};
1519 $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
1522 # Check the circulation rule for each relevant itemtype for this item
1523 my $rule = Koha::CirculationRules->get_effective_rules({
1524 branchcode => $branchcode,
1525 categorycode => $patron ? $patron->categorycode : undef,
1526 itemtype => $self->effective_itemtype,
1529 'recalls_per_record',
1534 # check recalls allowed has been set and is not zero
1535 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1538 # check borrower has not reached open recalls allowed limit
1539 return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
1541 # check borrower has not reach open recalls allowed per record limit
1542 return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
1544 # check if this patron has already recalled this item
1545 return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
1547 # check if this patron has already checked out this item
1548 return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1550 # check if this patron has already reserved this item
1551 return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
1554 # check item availability
1555 # items are unavailable for recall if they are lost, withdrawn or notforloan
1556 my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
1558 # if there are no available items at all, no recall can be placed
1559 return 0 if ( scalar @items == 0 );
1561 my $checked_out_count = 0;
1563 if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
1566 # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
1567 return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
1569 # can't recall if no items have been checked out
1570 return 0 if ( $checked_out_count == 0 );
1576 =head3 can_be_waiting_recall
1578 if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
1580 Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
1581 At this point the item has already been recalled. We are now at the checkin and set waiting stage.
1585 sub can_be_waiting_recall {
1588 return 0 if !( C4::Context->preference('UseRecalls') );
1590 # check if this item is not for loan, withdrawn or lost
1591 return 0 if ( $self->notforloan != 0 );
1592 return 0 if ( $self->itemlost != 0 );
1593 return 0 if ( $self->withdrawn != 0 );
1595 my $branchcode = $self->holdingbranch;
1596 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
1597 $branchcode = C4::Context->userenv->{'branch'};
1599 $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
1602 # Check the circulation rule for each relevant itemtype for this item
1603 my $rule = Koha::CirculationRules->get_effective_rules({
1604 branchcode => $branchcode,
1605 categorycode => undef,
1606 itemtype => $self->effective_itemtype,
1612 # check recalls allowed has been set and is not zero
1613 return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
1619 =head3 check_recalls
1621 my $recall = $item->check_recalls;
1623 Get the most relevant recall for this item.
1630 my @recalls = Koha::Recalls->search(
1631 { biblio_id => $self->biblionumber,
1632 item_id => [ $self->itemnumber, undef ]
1634 { order_by => { -asc => 'created_date' } }
1635 )->filter_by_current->as_list;
1638 # iterate through relevant recalls to find the best one.
1639 # if we come across a waiting recall, use this one.
1640 # if we have iterated through all recalls and not found a waiting recall, use the first recall in the array, which should be the oldest recall.
1641 foreach my $r ( @recalls ) {
1642 if ( $r->waiting ) {
1647 unless ( defined $recall ) {
1648 $recall = $recalls[0];
1664 Kyle M Hall <kyle@bywatersolutions.com>