use Modern::Perl;
-use Carp;
-use List::MoreUtils qw(any);
-use Data::Dumper;
-use Try::Tiny;
+use List::MoreUtils qw( any );
+use Try::Tiny qw( catch try );
use Koha::Database;
-use Koha::DateUtils qw( dt_from_string );
+use Koha::DateUtils qw( dt_from_string output_pref );
use C4::Context;
-use C4::Circulation;
+use C4::Circulation qw( barcodedecode GetBranchItemRule );
use C4::Reserves;
-use C4::ClassSource; # FIXME We would like to avoid that
+use C4::ClassSource qw( GetClassSort );
use C4::Log qw( logaction );
+use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
+use Koha::Biblio::ItemGroups;
use Koha::Checkouts;
use Koha::CirculationRules;
use Koha::CoverImages;
-use Koha::SearchEngine::Indexer;
+use Koha::Exceptions::Checkin;
+use Koha::Exceptions::Item::Bundle;
+use Koha::Exceptions::Item::Transfer;
+use Koha::Item::Attributes;
+use Koha::Exceptions::Item::Bundle;
use Koha::Item::Transfer::Limits;
use Koha::Item::Transfers;
use Koha::ItemTypes;
+use Koha::Libraries;
use Koha::Patrons;
use Koha::Plugins;
-use Koha::Libraries;
+use Koha::Recalls;
+use Koha::Result::Boolean;
+use Koha::SearchEngine::Indexer;
use Koha::StockRotationItem;
use Koha::StockRotationRotas;
+use Koha::TrackedLinks;
use base qw(Koha::Object);
$params can take an optional 'skip_record_index' parameter.
If set, the reindexation process will not happen (index_records 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
$self->itype($self->biblio->biblioitem->itemtype);
}
- my $today = dt_from_string;
- my $plugin_action = 'create';
+ $self->barcode( C4::Circulation::barcodedecode( $self->barcode ) );
+
+ my $today = dt_from_string;
+ my $action = 'create';
unless ( $self->in_storage ) { #AddItem
+
unless ( $self->permanent_location ) {
$self->permanent_location($self->location);
}
+
+ my $default_location = C4::Context->preference('NewItemsDefaultLocation');
+ unless ( $self->location || !$default_location ) {
+ $self->permanent_location( $self->location || $default_location )
+ unless $self->permanent_location;
+ $self->location($default_location);
+ }
+
unless ( $self->replacementpricedate ) {
$self->replacementpricedate($today);
}
$self->cn_sort($cn_sort);
}
- logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
- if $log_action && C4::Context->preference("CataloguingLog");
-
} else { # ModItem
- $plugin_action = 'modify';
+ $action = 'modify';
my %updated_columns = $self->_result->get_dirty_columns;
return $self->SUPER::store unless %updated_columns;
&& !$pre_mod_item->$field )
{
my $field_on = "${field}_on";
- $self->$field_on(
- DateTime::Format::MySQL->format_datetime(
- dt_from_string()
- )
- );
+ $self->$field_on(dt_from_string);
}
}
if ( exists $updated_columns{location}
- and $self->location ne 'CART'
- and $self->location ne 'PROC'
+ and ( !defined($self->location) or $self->location !~ /^(CART|PROC)$/ )
and not exists $updated_columns{permanent_location} )
{
$self->permanent_location( $self->location );
$self->_set_found_trigger($pre_mod_item);
}
- logaction( "CATALOGUING", "MODIFY", $self->itemnumber, "item " . Dumper($self->unblessed) )
- if $log_action && C4::Context->preference("CataloguingLog");
- }
-
- unless ( $self->dateaccessioned ) {
- $self->dateaccessioned($today);
}
my $result = $self->SUPER::store;
+ if ( $log_action && C4::Context->preference("CataloguingLog") ) {
+ $action eq 'create'
+ ? logaction( "CATALOGUING", "ADD", $self->itemnumber, "item" )
+ : logaction( "CATALOGUING", "MODIFY", $self->itemnumber, $self );
+ }
my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
$indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
unless $params->{skip_record_index};
- $self->get_from_storage->_after_item_action_hooks({ action => $plugin_action });
+ $self->get_from_storage->_after_item_action_hooks({ action => $action });
+
+ Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
+ {
+ biblio_ids => [ $self->biblionumber ]
+ }
+ ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
return $result;
}
# FIXME check the item has no current issues
# i.e. raise the appropriate exception
+ # Get the item group so we can delete it later if it has no items left
+ my $item_group = C4::Context->preference('EnableItemGroups') ? $self->item_group : undef;
+
my $result = $self->SUPER::delete;
+ # Delete the item gorup if it has no items left
+ $item_group->delete if ( $item_group && $item_group->items->count == 0 );
+
my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
$indexer->index_records( $self->biblionumber, "specialUpdate", "biblioserver" )
unless $params->{skip_record_index};
logaction( "CATALOGUING", "DELETE", $self->itemnumber, "item" )
if C4::Context->preference("CataloguingLog");
+ Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
+ {
+ biblio_ids => [ $self->biblionumber ]
+ }
+ ) unless $params->{skip_holds_queue} or !C4::Context->preference('RealTimeHoldsQueue');
+
return $result;
}
my $params = @_ ? shift : {};
my $safe_to_delete = $self->safe_to_delete;
- return $safe_to_delete unless $safe_to_delete eq '1';
+ return $safe_to_delete unless $safe_to_delete;
$self->move_to_deleted;
sub safe_to_delete {
my ($self) = @_;
- return "book_on_loan" if $self->checkout;
+ my $error;
+
+ $error = "book_on_loan" if $self->checkout;
- return "not_same_branch"
+ $error //= "not_same_branch"
if defined C4::Context->userenv
- and !C4::Context->IsSuperLibrarian()
- and C4::Context->preference("IndependentBranches")
- and ( C4::Context->userenv->{branch} ne $self->homebranch );
+ and defined C4::Context->userenv->{number}
+ and !Koha::Patrons->find( C4::Context->userenv->{number} )->can_edit_items_from( $self->homebranch );
# check it doesn't have a waiting reserve
- return "book_reserved"
- if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
+ $error //= "book_reserved"
+ if $self->holds->filter_by_found->count;
- return "linked_analytics"
+ $error //= "linked_analytics"
if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
- return "last_item_for_hold"
+ $error //= "last_item_for_hold"
if $self->biblio->items->count == 1
&& $self->biblio->holds->search(
{
}
)->count;
- return 1;
+ if ( $error ) {
+ return Koha::Result::Boolean->new(0)->add_message({ message => $error });
+ }
+
+ return Koha::Result::Boolean->new(1);
}
=head3 move_to_deleted
my ($self) = @_;
my $item_infos = $self->unblessed;
delete $item_infos->{timestamp}; #This ensures the timestamp date in deleteditems will be set to the current timestamp
+ $item_infos->{deleted_on} = dt_from_string;
return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos);
}
sub home_branch {
my ($self) = @_;
- $self->{_home_branch} ||= Koha::Libraries->find( $self->homebranch() );
+ my $hb_rs = $self->_result->homebranch;
- return $self->{_home_branch};
+ return Koha::Library->_new_from_dbic( $hb_rs );
}
=head3 holding_branch
sub holding_branch {
my ($self) = @_;
- $self->{_holding_branch} ||= Koha::Libraries->find( $self->holdingbranch() );
+ my $hb_rs = $self->_result->holdingbranch;
- return $self->{_holding_branch};
+ return Koha::Library->_new_from_dbic( $hb_rs );
}
=head3 biblio
return Koha::Checkout->_new_from_dbic( $checkout_rs );
}
+=head3 item_group
+
+my $item_group = $item->item_group;
+
+Return the item group for this item
+
+=cut
+
+sub item_group {
+ my ( $self ) = @_;
+
+ my $item_group_item = $self->_result->item_group_item;
+ return unless $item_group_item;
+
+ my $item_group_rs = $item_group_item->item_group;
+ return unless $item_group_rs;
+
+ my $item_group = Koha::Biblio::ItemGroup->_new_from_dbic( $item_group_rs );
+ return $item_group;
+}
+
+=head3 return_claims
+
+ my $return_claims = $item->return_claims;
+
+Return any return_claims associated with this item
+
+=cut
+
+sub return_claims {
+ my ( $self, $params, $attrs ) = @_;
+ my $claims_rs = $self->_result->return_claims->search($params, $attrs);
+ return Koha::Checkouts::ReturnClaims->_new_from_dbic( $claims_rs );
+}
+
+=head3 return_claim
+
+ my $return_claim = $item->return_claim;
+
+Returns the most recent unresolved return_claims associated with this item
+
+=cut
+
+sub return_claim {
+ my ($self) = @_;
+ my $claims_rs =
+ $self->_result->return_claims->search( { resolution => undef },
+ { order_by => { '-desc' => 'created_on' }, rows => 1 } )->single;
+ return unless $claims_rs;
+ return Koha::Checkouts::ReturnClaim->_new_from_dbic($claims_rs);
+}
+
=head3 holds
my $holds = $item->holds();
return Koha::Holds->_new_from_dbic( $holds_rs );
}
+=head3 request_transfer
+
+ my $transfer = $item->request_transfer(
+ {
+ to => $to_library,
+ reason => $reason,
+ [ ignore_limits => 0, enqueue => 1, replace => 1 ]
+ }
+ );
+
+Add a transfer request for this item to the given branch for the given reason.
+
+An exception will be thrown if the BranchTransferLimits would prevent the requested
+transfer, unless 'ignore_limits' is passed to override the limits.
+
+An exception will be thrown if an active transfer (i.e pending arrival date) is found;
+The caller should catch such cases and retry the transfer request as appropriate passing
+an appropriate override.
+
+Overrides
+* enqueue - Used to queue up the transfer when the existing transfer is found to be in transit.
+* replace - Used to replace the existing transfer request with your own.
+
+=cut
+
+sub request_transfer {
+ my ( $self, $params ) = @_;
+
+ # check for mandatory params
+ my @mandatory = ( 'to', 'reason' );
+ for my $param (@mandatory) {
+ unless ( defined( $params->{$param} ) ) {
+ Koha::Exceptions::MissingParameter->throw(
+ error => "The $param parameter is mandatory" );
+ }
+ }
+
+ Koha::Exceptions::Item::Transfer::Limit->throw()
+ unless ( $params->{ignore_limits}
+ || $self->can_be_transferred( { to => $params->{to} } ) );
+
+ my $request = $self->get_transfer;
+ Koha::Exceptions::Item::Transfer::InQueue->throw( transfer => $request )
+ if ( $request && !$params->{enqueue} && !$params->{replace} );
+
+ $request->cancel( { reason => $params->{reason}, force => 1 } )
+ if ( defined($request) && $params->{replace} );
+
+ my $transfer = Koha::Item::Transfer->new(
+ {
+ itemnumber => $self->itemnumber,
+ daterequested => dt_from_string,
+ frombranch => $self->holdingbranch,
+ tobranch => $params->{to}->branchcode,
+ reason => $params->{reason},
+ comments => $params->{comment}
+ }
+ )->store();
+
+ return $transfer;
+}
+
=head3 get_transfer
-my $transfer = $item->get_transfer;
+ my $transfer = $item->get_transfer;
-Return the transfer if the item is in transit or undef
+Return the active transfer request or undef
+
+Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
+whereby the most recently sent, but not received, transfer will be returned
+if it exists, otherwise the oldest unsatisfied transfer will be returned.
+
+This allows for transfers to queue, which is the case for stock rotation and
+rotating collections where a manual transfer may need to take precedence but
+we still expect the item to end up at a final location eventually.
=cut
sub get_transfer {
- my ( $self ) = @_;
- my $transfer_rs = $self->_result->branchtransfers->search({ datearrived => undef })->first;
- return unless $transfer_rs;
- return Koha::Item::Transfer->_new_from_dbic( $transfer_rs );
+ my ($self) = @_;
+
+ return $self->get_transfers->search( {}, { rows => 1 } )->next;
+}
+
+=head3 get_transfers
+
+ my $transfer = $item->get_transfers;
+
+Return the list of outstanding transfers (i.e requested but not yet cancelled
+or received).
+
+Note: Transfers are retrieved in a Modified FIFO (First In First Out) order
+whereby the most recently sent, but not received, transfer will be returned
+first if it exists, otherwise requests are in oldest to newest request order.
+
+This allows for transfers to queue, which is the case for stock rotation and
+rotating collections where a manual transfer may need to take precedence but
+we still expect the item to end up at a final location eventually.
+
+=cut
+
+sub get_transfers {
+ my ($self) = @_;
+
+ my $transfer_rs = $self->_result->branchtransfers;
+
+ return Koha::Item::Transfers
+ ->_new_from_dbic($transfer_rs)
+ ->filter_by_current
+ ->search( {}, { order_by => [ { -desc => 'datesent' }, { -asc => 'daterequested' } ], } );
}
=head3 last_returned_by
C4::Circulation::GetBranchItemRule( $circ_control_branch, $self->itype );
if(defined $patron) {
- return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} == 3 && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
- return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} == 1 && $self->home_branch->branchcode ne $patron->branchcode;
+ return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_local_hold_group' && !$self->home_branch->validate_hold_sibling( {branchcode => $patron->branchcode} );
+ return Koha::Libraries->new()->empty if $branchitemrule->{holdallowed} eq 'from_home_library' && $self->home_branch->branchcode ne $patron->branchcode;
}
my $pickup_libraries = Koha::Libraries->search();
return $pending_hold->count ? 1: 0;
}
+=head3 has_pending_recall {
+
+ my $has_pending_recall
+
+Return if whether has pending recall of not.
+
+=cut
+
+sub has_pending_recall {
+ my ( $self ) = @_;
+
+ # FIXME Must be moved to $self->recalls
+ return Koha::Recalls->search(
+ {
+ item_id => $self->itemnumber,
+ status => 'waiting',
+ }
+ )->count;
+}
+
=head3 as_marc_field
- my $mss = C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
- my $field = $item->as_marc_field({ [ mss => $mss ] });
+ my $field = $item->as_marc_field;
This method returns a MARC::Field object representing the Koha::Item object
with the current mappings configuration.
=cut
sub as_marc_field {
- my ( $self, $params ) = @_;
+ my ( $self ) = @_;
- my $mss = $params->{mss} // C4::Biblio::GetMarcSubfieldStructure( '', { unsafe => 1 } );
- my $item_tag = $mss->{'items.itemnumber'}[0]->{tagfield};
+ my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
- my @subfields;
+ my $tagslib = C4::Biblio::GetMarcStructure( 1, $self->biblio->frameworkcode, { unsafe => 1 });
- my @columns = $self->_result->result_source->columns;
+ my @subfields;
- foreach my $item_field ( @columns ) {
- my $mapping = $mss->{ "items.$item_field"}[0];
- my $tagfield = $mapping->{tagfield};
- my $tagsubfield = $mapping->{tagsubfield};
- next if !$tagfield; # TODO: Should we raise an exception instead?
- # Feels like safe fallback is better
+ my $item_field = $tagslib->{$itemtag};
+
+ my $more_subfields = $self->additional_attributes->to_hashref;
+ foreach my $subfield (
+ sort {
+ $a->{display_order} <=> $b->{display_order}
+ || $a->{subfield} cmp $b->{subfield}
+ } grep { ref($_) && %$_ } values %$item_field
+ ){
+
+ my $kohafield = $subfield->{kohafield};
+ my $tagsubfield = $subfield->{tagsubfield};
+ my $value;
+ if ( defined $kohafield && $kohafield ne '' ) {
+ next if $kohafield !~ m{^items\.}; # That would be weird!
+ ( my $attribute = $kohafield ) =~ s|^items\.||;
+ $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
+ if defined $self->$attribute and $self->$attribute ne '';
+ } else {
+ $value = $more_subfields->{$tagsubfield}
+ }
- push @subfields, $tagsubfield => $self->$item_field
- if defined $self->$item_field and $item_field ne '';
- }
+ next unless defined $value
+ and $value ne q{};
- my $unlinked_item_subfields = C4::Items::_parse_unlinked_item_subfields_from_xml($self->more_subfields_xml);
- push( @subfields, @{$unlinked_item_subfields} )
- if defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1;
+ if ( $subfield->{repeatable} ) {
+ my @values = split '\|', $value;
+ push @subfields, ( $tagsubfield => $_ ) for @values;
+ }
+ else {
+ push @subfields, ( $tagsubfield => $value );
+ }
- my $field;
+ }
- $field = MARC::Field->new(
- "$item_tag", ' ', ' ', @subfields
- ) if @subfields;
+ return unless @subfields;
- return $field;
+ return MARC::Field->new(
+ "$itemtag", ' ', ' ', @subfields
+ );
}
=head3 renewal_branchcode
return Koha::CoverImages->_new_from_dbic($cover_image_rs);
}
+=head3 columns_to_str
+
+ my $values = $items->columns_to_str;
+
+Return a hashref with the string representation of the different attribute of the item.
+
+This is meant to be used for display purpose only.
+
+=cut
+
+sub columns_to_str {
+ my ( $self ) = @_;
+
+ my $frameworkcode = $self->biblio->frameworkcode;
+ my $tagslib = C4::Biblio::GetMarcStructure(1, $frameworkcode);
+ my ( $itemtagfield, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
+
+ my $columns_info = $self->_result->result_source->columns_info;
+
+ my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
+ my $values = {};
+ for my $column ( keys %$columns_info ) {
+
+ next if $column eq 'more_subfields_xml';
+
+ my $value = $self->$column;
+ # 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
+
+ if ( not defined $value or $value eq "" ) {
+ $values->{$column} = $value;
+ next;
+ }
+
+ my $subfield =
+ exists $mss->{"items.$column"}
+ ? @{ $mss->{"items.$column"} }[0] # Should we deal with several subfields??
+ : undef;
+
+ $values->{$column} =
+ $subfield
+ ? $subfield->{authorised_value}
+ ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
+ $subfield->{tagsubfield}, $value, '', $tagslib )
+ : $value
+ : $value;
+ }
+
+ my $marc_more=
+ $self->more_subfields_xml
+ ? MARC::Record->new_from_xml( $self->more_subfields_xml, 'UTF-8' )
+ : undef;
+
+ my $more_values;
+ if ( $marc_more ) {
+ my ( $field ) = $marc_more->fields;
+ for my $sf ( $field->subfields ) {
+ my $subfield_code = $sf->[0];
+ my $value = $sf->[1];
+ my $subfield = $tagslib->{$itemtagfield}->{$subfield_code};
+ next unless $subfield; # We have the value but it's not mapped, data lose! No regression however.
+ $value =
+ $subfield->{authorised_value}
+ ? C4::Biblio::GetAuthorisedValueDesc( $itemtagfield,
+ $subfield->{tagsubfield}, $value, '', $tagslib )
+ : $value;
+
+ push @{$more_values->{$subfield_code}}, $value;
+ }
+
+ while ( my ( $k, $v ) = each %$more_values ) {
+ $values->{$k} = join ' | ', @$v;
+ }
+ }
+
+ return $values;
+}
+
+=head3 additional_attributes
+
+ my $attributes = $item->additional_attributes;
+ $attributes->{k} = 'new k';
+ $item->update({ more_subfields => $attributes->to_marcxml });
+
+Returns a Koha::Item::Attributes object that represents the non-mapped
+attributes for this item.
+
+=cut
+
+sub additional_attributes {
+ my ($self) = @_;
+
+ return Koha::Item::Attributes->new_from_marcxml(
+ $self->more_subfields_xml,
+ );
+}
+
=head3 _set_found_trigger
$self->_set_found_trigger
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.
+ # Reverse any lost item charges if necessary.
my $no_refund_after_days =
C4::Context->preference('NoRefundOnLostReturnedItemsAge');
if ($no_refund_after_days) {
return $self unless $lost_age_in_days < $no_refund_after_days;
}
- my $lostreturn_policy = Koha::CirculationRules->get_lostreturn_policy(
+ my $lost_proc_return_policy = Koha::CirculationRules->get_lostreturn_policy(
{
item => $self,
return_branch => C4::Context->userenv
: undef,
}
);
+ my $lostreturn_policy = $lost_proc_return_policy->{lostreturn};
if ( $lostreturn_policy ) {
if ( $patron ) {
my $account = $patron->account;
- my $total_to_refund = 0;
- # Use cases
- if ( $lost_charge->amount > $lost_charge->amountoutstanding ) {
+ # Credit outstanding amount
+ my $credit_total = $lost_charge->amountoutstanding;
+ # Use cases
+ if (
+ $lost_charge->amount > $lost_charge->amountoutstanding &&
+ $lostreturn_policy ne "refund_unpaid"
+ ) {
# 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(
+
+ # We don't credit any payments if return policy is
+ # "refund_unpaid"
+ #
+ # In that case only unpaid/outstanding amount
+ # will be credited which settles the debt without
+ # creating extra credits
+
+ my $credit_offsets = $lost_charge->debit_offsets(
{
- debit_id => $lost_charge->id,
- credit_id => { '!=' => undef }, # it is not the debit itself
- type => { '!=' => 'Writeoff' },
- amount => { '<' => 0 } # credits are negative on the DB
- }
+ 'credit_id' => { '!=' => undef },
+ 'credit.credit_type_code' => { '!=' => 'Writeoff' }
+ },
+ { join => 'credit' }
);
- $total_to_refund = ( $credits_offsets->count > 0 )
- ? $credits_offsets->total * -1 # credits are negative on the DB
- : 0;
+ my $total_to_refund = ( $credit_offsets->count > 0 ) ?
+ # credits are negative on the DB
+ $credit_offsets->total * -1 :
+ 0;
+ # Credit the outstanding amount, then add what has been
+ # paid to create a net credit for this amount
+ $credit_total += $total_to_refund;
}
- my $credit_total = $lost_charge->amountoutstanding + $total_to_refund;
-
my $credit;
if ( $credit_total > 0 ) {
my $branchcode =
);
$credit->apply( { debits => [$lost_charge] } );
- $self->{_refunded} = 1;
+ $self->add_message(
+ {
+ type => 'info',
+ message => 'lost_refunded',
+ payload => { credit_id => $credit->id }
+ }
+ );
}
# Update the account status
}
}
- # restore fine for lost book
- if ( $lostreturn_policy eq 'restore' ) {
- my $lost_overdue = Koha::Account::Lines->search(
- {
- itemnumber => $self->itemnumber,
- debit_type_code => 'OVERDUE',
- status => 'LOST'
- },
+ # possibly restore fine for lost book
+ my $lost_overdue = Koha::Account::Lines->search(
+ {
+ itemnumber => $self->itemnumber,
+ debit_type_code => 'OVERDUE',
+ status => 'LOST'
+ },
+ {
+ order_by => { '-desc' => 'date' },
+ rows => 1
+ }
+ )->single;
+ if ( $lostreturn_policy eq 'restore' && $lost_overdue ) {
+
+ my $patron = $lost_overdue->patron;
+ if ($patron) {
+ my $account = $patron->account;
+
+ # Update status of fine
+ $lost_overdue->status('FOUND')->store();
+
+ # Find related forgive credit
+ my $refund = $lost_overdue->credits(
+ {
+ credit_type_code => 'FORGIVEN',
+ itemnumber => $self->itemnumber,
+ status => [ { '!=' => 'VOID' }, undef ]
+ },
+ { order_by => { '-desc' => 'date' }, rows => 1 }
+ )->single;
+
+ if ( $refund ) {
+ # Revert the forgive credit
+ $refund->void({ interface => 'trigger' });
+ $self->add_message(
+ {
+ type => 'info',
+ message => 'lost_restored',
+ payload => { refund_id => $refund->id }
+ }
+ );
+ }
+
+ # Reconcile balances if required
+ if ( C4::Context->preference('AccountAutoReconcile') ) {
+ $account->reconcile_balance;
+ }
+ }
+
+ } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
+ $self->add_message(
{
- order_by => { '-desc' => 'date' },
- rows => 1
+ type => 'info',
+ message => 'lost_charge',
}
- )->single;
+ );
+ }
+ }
+
+ my $processingreturn_policy = $lost_proc_return_policy->{processingreturn};
+
+ if ( $processingreturn_policy ) {
+
+ # refund processing charge made for lost book
+ my $processing_charge = Koha::Account::Lines->search(
+ {
+ itemnumber => $self->itemnumber,
+ debit_type_code => 'PROCESSING',
+ status => [ undef, { '<>' => 'FOUND' } ]
+ },
+ {
+ order_by => { -desc => [ 'date', 'accountlines_id' ] },
+ rows => 1
+ }
+ )->single;
+
+ if ( $processing_charge ) {
- if ( $lost_overdue ) {
+ my $patron = $processing_charge->patron;
+ if ( $patron ) {
+
+ my $account = $patron->account;
+
+ # Credit outstanding amount
+ my $credit_total = $processing_charge->amountoutstanding;
- my $patron = $lost_overdue->patron;
- if ($patron) {
- my $account = $patron->account;
+ # Use cases
+ if (
+ $processing_charge->amount > $processing_charge->amountoutstanding &&
+ $processingreturn_policy ne "refund_unpaid"
+ ) {
+ # 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'
- # Update status of fine
- $lost_overdue->status('FOUND')->store();
+ # We don't credit any payments if return policy is
+ # "refund_unpaid"
+ #
+ # In that case only unpaid/outstanding amount
+ # will be credited which settles the debt without
+ # creating extra credits
- # Find related forgive credit
- my $refund = $lost_overdue->credits(
+ my $credit_offsets = $processing_charge->debit_offsets(
{
- credit_type_code => 'FORGIVEN',
- itemnumber => $self->itemnumber,
- status => [ { '!=' => 'VOID' }, undef ]
+ 'credit_id' => { '!=' => undef },
+ 'credit.credit_type_code' => { '!=' => 'Writeoff' }
},
- { order_by => { '-desc' => 'date' }, rows => 1 }
- )->single;
+ { join => 'credit' }
+ );
- if ( $refund ) {
- # Revert the forgive credit
- $refund->void();
- $self->{_restored} = 1;
- }
+ my $total_to_refund = ( $credit_offsets->count > 0 ) ?
+ # credits are negative on the DB
+ $credit_offsets->total * -1 :
+ 0;
+ # Credit the outstanding amount, then add what has been
+ # paid to create a net credit for this amount
+ $credit_total += $total_to_refund;
+ }
- # Reconcile balances if required
- if ( C4::Context->preference('AccountAutoReconcile') ) {
- $account->reconcile_balance;
- }
+ 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 => 'PROCESSING_FOUND',
+ interface => C4::Context->interface,
+ library_id => $branchcode,
+ item_id => $self->itemnumber,
+ issue_id => $processing_charge->issue_id
+ }
+ );
+
+ $credit->apply( { debits => [$processing_charge] } );
+ $self->add_message(
+ {
+ type => 'info',
+ message => 'processing_refunded',
+ payload => { credit_id => $credit->id }
+ }
+ );
+ }
+
+ # Update the account status
+ $processing_charge->status('FOUND');
+ $processing_charge->store();
+
+ # Reconcile balances if required
+ if ( C4::Context->preference('AccountAutoReconcile') ) {
+ $account->reconcile_balance;
}
}
- } elsif ( $lostreturn_policy eq 'charge' ) {
- $self->{_charge} = 1;
}
}
return $self;
}
+=head3 public_read_list
+
+This method returns the list of publicly readable database fields for both API and UI output purposes
+
+=cut
+
+sub public_read_list {
+ return [
+ 'itemnumber', 'biblionumber', 'homebranch',
+ 'holdingbranch', 'location', 'collectioncode',
+ 'itemcallnumber', 'copynumber', 'enumchron',
+ 'barcode', 'dateaccessioned', 'itemnotes',
+ 'onloan', 'uri', 'itype',
+ 'notforloan', 'damaged', 'itemlost',
+ 'withdrawn', 'restricted'
+ ];
+}
+
+=head3 to_api
+
+Overloaded to_api method to ensure item-level itypes is adhered to.
+
+=cut
+
+sub to_api {
+ my ($self, $params) = @_;
+
+ my $response = $self->SUPER::to_api($params);
+ my $overrides = {};
+
+ $overrides->{effective_item_type_id} = $self->effective_itemtype;
+ $overrides->{effective_not_for_loan_status} = $self->notforloan ? $self->notforloan : $self->itemtype->notforloan;
+
+ return { %$response, %$overrides };
+}
+
=head3 to_api_mapping
This method returns the mapping for representing a Koha::Item object
ccode => 'collection_code',
materials => 'materials_notes',
uri => 'uri',
- itype => 'item_type',
+ itype => 'item_type_id',
more_subfields_xml => 'extended_subfields',
enumchron => 'serial_issue_number',
copynumber => 'copy_number',
stocknumber => 'inventory_number',
- new_status => 'new_status'
+ new_status => 'new_status',
+ deleted_on => undef,
};
}
sub itemtype {
my ( $self ) = @_;
+
return Koha::ItemTypes->find( $self->effective_itemtype );
}
+=head3 orders
+
+ my $orders = $item->orders();
+
+Returns a Koha::Acquisition::Orders object
+
+=cut
+
+sub orders {
+ my ( $self ) = @_;
+
+ my $orders = $self->_result->item_orders;
+ return Koha::Acquisition::Orders->_new_from_dbic($orders);
+}
+
+=head3 tracked_links
+
+ my $tracked_links = $item->tracked_links();
+
+Returns a Koha::TrackedLinks object
+
+=cut
+
+sub tracked_links {
+ my ( $self ) = @_;
+
+ my $tracked_links = $self->_result->linktrackers;
+ return Koha::TrackedLinks->_new_from_dbic($tracked_links);
+}
+
+=head3 move_to_biblio
+
+ $item->move_to_biblio($to_biblio[, $params]);
+
+Move the item to another biblio and update any references in other tables.
+
+The final optional parameter, C<$params>, is expected to contain the
+'skip_record_index' key, which is relayed down to Koha::Item->store.
+There it prevents calling index_records, which takes most of the
+time in batch adds/deletes. The caller must take care of calling
+index_records separately.
+
+$params:
+ skip_record_index => 1|0
+
+Returns undef if the move failed or the biblionumber of the destination record otherwise
+
+=cut
+
+sub move_to_biblio {
+ my ( $self, $to_biblio, $params ) = @_;
+
+ $params //= {};
+
+ return if $self->biblionumber == $to_biblio->biblionumber;
+
+ my $from_biblionumber = $self->biblionumber;
+ my $to_biblionumber = $to_biblio->biblionumber;
+
+ # Own biblionumber and biblioitemnumber
+ $self->set({
+ biblionumber => $to_biblionumber,
+ biblioitemnumber => $to_biblio->biblioitem->biblioitemnumber
+ })->store({ skip_record_index => $params->{skip_record_index} });
+
+ unless ($params->{skip_record_index}) {
+ my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
+ $indexer->index_records( $from_biblionumber, "specialUpdate", "biblioserver" );
+ }
+
+ # Acquisition orders
+ $self->orders->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
+
+ # Holds
+ $self->holds->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
+
+ # hold_fill_target (there's no Koha object available yet)
+ my $hold_fill_target = $self->_result->hold_fill_target;
+ if ($hold_fill_target) {
+ $hold_fill_target->update({ biblionumber => $to_biblionumber });
+ }
+
+ # tmp_holdsqueues - Can't update with DBIx since the table is missing a primary key
+ # and can't even fake one since the significant columns are nullable.
+ my $storage = $self->_result->result_source->storage;
+ $storage->dbh_do(
+ sub {
+ my ($storage, $dbh, @cols) = @_;
+
+ $dbh->do("UPDATE tmp_holdsqueue SET biblionumber=? WHERE itemnumber=?", undef, $to_biblionumber, $self->itemnumber);
+ }
+ );
+
+ # tracked_links
+ $self->tracked_links->update({ biblionumber => $to_biblionumber }, { no_triggers => 1 });
+
+ return $to_biblionumber;
+}
+
+=head3 bundle_items
+
+ my $bundle_items = $item->bundle_items;
+
+Returns the items associated with this bundle
+
+=cut
+
+sub bundle_items {
+ my ($self) = @_;
+
+ if ( !$self->{_bundle_items_cached} ) {
+ my $bundle_items = Koha::Items->search(
+ { 'item_bundles_item.host' => $self->itemnumber },
+ { join => 'item_bundles_item' } );
+ $self->{_bundle_items} = $bundle_items;
+ $self->{_bundle_items_cached} = 1;
+ }
+
+ return $self->{_bundle_items};
+}
+
+=head3 is_bundle
+
+ my $is_bundle = $item->is_bundle;
+
+Returns whether the item is a bundle or not
+
+=cut
+
+sub is_bundle {
+ my ($self) = @_;
+ return $self->bundle_items->count ? 1 : 0;
+}
+
+=head3 bundle_host
+
+ my $bundle = $item->bundle_host;
+
+Returns the bundle item this item is attached to
+
+=cut
+
+sub bundle_host {
+ my ($self) = @_;
+
+ my $bundle_items_rs = $self->_result->item_bundles_item;
+ return unless $bundle_items_rs;
+ return Koha::Item->_new_from_dbic($bundle_items_rs->host);
+}
+
+=head3 in_bundle
+
+ my $in_bundle = $item->in_bundle;
+
+Returns whether this item is currently in a bundle
+
+=cut
+
+sub in_bundle {
+ my ($self) = @_;
+ return $self->bundle_host ? 1 : 0;
+}
+
+=head3 add_to_bundle
+
+ my $link = $item->add_to_bundle($bundle_item);
+
+Adds the bundle_item passed to this item
+
+=cut
+
+sub add_to_bundle {
+ my ( $self, $bundle_item, $options ) = @_;
+
+ $options //= {};
+
+ Koha::Exceptions::Item::Bundle::IsBundle->throw()
+ if ( $self->itemnumber eq $bundle_item->itemnumber
+ || $bundle_item->is_bundle
+ || $self->in_bundle );
+
+ my $schema = Koha::Database->new->schema;
+
+ my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
+
+ try {
+ $schema->txn_do(
+ sub {
+ my $checkout = $bundle_item->checkout;
+ if ($checkout) {
+ unless ($options->{force_checkin}) {
+ Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
+ }
+
+ my $branchcode = C4::Context->userenv->{'branch'};
+ my ($success) = C4::Circulation::AddReturn($bundle_item->barcode, $branchcode);
+ unless ($success) {
+ Koha::Exceptions::Checkin::FailedCheckin->throw();
+ }
+ }
+
+ my $holds = $bundle_item->current_holds;
+ if ($holds->count) {
+ unless ($options->{ignore_holds}) {
+ Koha::Exceptions::Item::Bundle::ItemHasHolds->throw();
+ }
+ }
+
+ $self->_result->add_to_item_bundles_hosts(
+ { item => $bundle_item->itemnumber } );
+
+ $bundle_item->notforloan($BundleNotLoanValue)->store();
+ }
+ );
+ }
+ catch {
+
+ # FIXME: See if we can move the below copy/paste from Koha::Object::store into it's own class and catch at a lower level in the Schema instantiation, take inspiration from DBIx::Error
+ if ( ref($_) eq 'DBIx::Class::Exception' ) {
+ if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
+ # FK constraints
+ # FIXME: MySQL error, if we support more DB engines we should implement this for each
+ if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
+ Koha::Exceptions::Object::FKConstraint->throw(
+ error => 'Broken FK constraint',
+ broken_fk => $+{column}
+ );
+ }
+ }
+ elsif (
+ $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
+ {
+ Koha::Exceptions::Object::DuplicateID->throw(
+ error => 'Duplicate ID',
+ duplicate_id => $+{key}
+ );
+ }
+ elsif ( $_->{msg} =~
+/Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
+ )
+ { # The optional \W in the regex might be a quote or backtick
+ my $type = $+{type};
+ my $value = $+{value};
+ my $property = $+{property};
+ $property =~ s/['`]//g;
+ Koha::Exceptions::Object::BadValue->throw(
+ type => $type,
+ value => $value,
+ property => $property =~ /(\w+\.\w+)$/
+ ? $1
+ : $property
+ , # results in table.column without quotes or backtics
+ );
+ }
+
+ # Catch-all for foreign key breakages. It will help find other use cases
+ $_->rethrow();
+ }
+ else {
+ $_->rethrow();
+ }
+ };
+}
+
+=head3 remove_from_bundle
+
+Remove this item from any bundle it may have been attached to.
+
+=cut
+
+sub remove_from_bundle {
+ my ($self) = @_;
+
+ my $bundle_item_rs = $self->_result->item_bundles_item;
+ if ( $bundle_item_rs ) {
+ $bundle_item_rs->delete;
+ $self->notforloan(0)->store();
+ return 1;
+ }
+ return 0;
+}
+
=head2 Internal methods
=head3 _after_item_action_hooks
);
}
+=head3 recall
+
+ my $recall = $item->recall;
+
+Return the relevant recall for this item
+
+=cut
+
+sub recall {
+ my ( $self ) = @_;
+ my @recalls = Koha::Recalls->search(
+ {
+ biblio_id => $self->biblionumber,
+ completed => 0,
+ },
+ { order_by => { -asc => 'created_date' } }
+ )->as_list;
+ foreach my $recall (@recalls) {
+ if ( $recall->item_level and $recall->item_id == $self->itemnumber ){
+ return $recall;
+ }
+ }
+ # no item-level recall to return, so return earliest biblio-level
+ # FIXME: eventually this will be based on priority
+ return $recalls[0];
+}
+
+=head3 can_be_recalled
+
+ if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
+
+Does item-level checks and returns if items can be recalled by this borrower
+
+=cut
+
+sub can_be_recalled {
+ my ( $self, $params ) = @_;
+
+ return 0 if !( C4::Context->preference('UseRecalls') );
+
+ # check if this item is not for loan, withdrawn or lost
+ return 0 if ( $self->notforloan != 0 );
+ return 0 if ( $self->itemlost != 0 );
+ return 0 if ( $self->withdrawn != 0 );
+
+ # check if this item is not checked out - if not checked out, can't be recalled
+ return 0 if ( !defined( $self->checkout ) );
+
+ my $patron = $params->{patron};
+
+ my $branchcode = C4::Context->userenv->{'branch'};
+ if ( $patron ) {
+ $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
+ }
+
+ # Check the circulation rule for each relevant itemtype for this item
+ my $rule = Koha::CirculationRules->get_effective_rules({
+ branchcode => $branchcode,
+ categorycode => $patron ? $patron->categorycode : undef,
+ itemtype => $self->effective_itemtype,
+ rules => [
+ 'recalls_allowed',
+ 'recalls_per_record',
+ 'on_shelf_recalls',
+ ],
+ });
+
+ # check recalls allowed has been set and is not zero
+ return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
+
+ if ( $patron ) {
+ # check borrower has not reached open recalls allowed limit
+ return 0 if ( $patron->recalls->filter_by_current->count >= $rule->{recalls_allowed} );
+
+ # check borrower has not reach open recalls allowed per record limit
+ return 0 if ( $patron->recalls->filter_by_current->search({ biblio_id => $self->biblionumber })->count >= $rule->{recalls_per_record} );
+
+ # check if this patron has already recalled this item
+ return 0 if ( Koha::Recalls->search({ item_id => $self->itemnumber, patron_id => $patron->borrowernumber })->filter_by_current->count > 0 );
+
+ # check if this patron has already checked out this item
+ return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
+
+ # check if this patron has already reserved this item
+ return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
+ }
+
+ # check item availability
+ # items are unavailable for recall if they are lost, withdrawn or notforloan
+ my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 })->as_list;
+
+ # if there are no available items at all, no recall can be placed
+ return 0 if ( scalar @items == 0 );
+
+ my $checked_out_count = 0;
+ foreach (@items) {
+ if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
+ }
+
+ # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
+ return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
+
+ # can't recall if no items have been checked out
+ return 0 if ( $checked_out_count == 0 );
+
+ # can recall
+ return 1;
+}
+
+=head3 can_be_waiting_recall
+
+ if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
+
+Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
+At this point the item has already been recalled. We are now at the checkin and set waiting stage.
+
+=cut
+
+sub can_be_waiting_recall {
+ my ( $self ) = @_;
+
+ return 0 if !( C4::Context->preference('UseRecalls') );
+
+ # check if this item is not for loan, withdrawn or lost
+ return 0 if ( $self->notforloan != 0 );
+ return 0 if ( $self->itemlost != 0 );
+ return 0 if ( $self->withdrawn != 0 );
+
+ my $branchcode = $self->holdingbranch;
+ if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
+ $branchcode = C4::Context->userenv->{'branch'};
+ } else {
+ $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
+ }
+
+ # Check the circulation rule for each relevant itemtype for this item
+ my $rule = Koha::CirculationRules->get_effective_rules({
+ branchcode => $branchcode,
+ categorycode => undef,
+ itemtype => $self->effective_itemtype,
+ rules => [
+ 'recalls_allowed',
+ ],
+ });
+
+ # check recalls allowed has been set and is not zero
+ return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
+
+ # can recall
+ return 1;
+}
+
+=head3 check_recalls
+
+ my $recall = $item->check_recalls;
+
+Get the most relevant recall for this item.
+
+=cut
+
+sub check_recalls {
+ my ( $self ) = @_;
+
+ my @recalls = Koha::Recalls->search(
+ { biblio_id => $self->biblionumber,
+ item_id => [ $self->itemnumber, undef ]
+ },
+ { order_by => { -asc => 'created_date' } }
+ )->filter_by_current->as_list;
+
+ my $recall;
+ # iterate through relevant recalls to find the best one.
+ # if we come across a waiting recall, use this one.
+ # 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.
+ foreach my $r ( @recalls ) {
+ if ( $r->waiting ) {
+ $recall = $r;
+ last;
+ }
+ }
+ unless ( defined $recall ) {
+ $recall = $recalls[0];
+ }
+
+ return $recall;
+}
+
+=head3 is_notforloan
+
+ my $is_notforloan = $item->is_notforloan;
+
+Determine whether or not this item is "notforloan" based on
+the item's notforloan status or its item type
+
+=cut
+
+sub is_notforloan {
+ my ( $self ) = @_;
+ my $is_notforloan = 0;
+
+ if ( $self->notforloan ){
+ $is_notforloan = 1;
+ }
+ else {
+ my $itemtype = $self->itemtype;
+ if ($itemtype){
+ if ( $itemtype->notforloan ){
+ $is_notforloan = 1;
+ }
+ }
+ }
+
+ return $is_notforloan;
+}
+
+=head3 is_denied_renewal
+
+ my $is_denied_renewal = $item->is_denied_renewal;
+
+Determine whether or not this item can be renewed based on the
+rules set in the ItemsDeniedRenewal system preference.
+
+=cut
+
+sub is_denied_renewal {
+ my ( $self ) = @_;
+
+ my $denyingrules = Koha::Config::SysPrefs->find('ItemsDeniedRenewal')->get_yaml_pref_hash();
+ return 0 unless $denyingrules;
+ foreach my $field (keys %$denyingrules) {
+ my $val = $self->$field;
+ if( !defined $val) {
+ if ( any { !defined $_ } @{$denyingrules->{$field}} ){
+ return 1;
+ }
+ } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
+ # If the results matches the values in the syspref
+ # We return true if match found
+ return 1;
+ }
+ }
+ return 0;
+}
+
+=head3 strings_map
+
+Returns a map of column name to string representations including the string,
+the mapping type and the mapping category where appropriate.
+
+Currently handles authorised value mappings, library, callnumber and itemtype
+expansions.
+
+Accepts a param hashref where the 'public' key denotes whether we want the public
+or staff client strings.
+
+=cut
+
+sub strings_map {
+ my ( $self, $params ) = @_;
+
+ my $columns_info = $self->_result->result_source->columns_info;
+ my $frameworkcode = $self->biblio->frameworkcode;
+ my $tagslib = C4::Biblio::GetMarcStructure( 1, $frameworkcode );
+ my $mss = C4::Biblio::GetMarcSubfieldStructure( $frameworkcode, { unsafe => 1 } );
+
+ my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
+
+ # Hardcoded known 'authorised_value' values mapped to API codes
+ my $code_to_type = {
+ branches => 'library',
+ cn_source => 'call_number_source',
+ itemtypes => 'item_type',
+ };
+
+ # Handle not null and default values for integers and dates
+ my $strings = {};
+
+ foreach my $col ( keys %{$columns_info} ) {
+
+ # By now, we are done with known columns, now check the framework for mappings
+ my $field = $self->_result->result_source->name . '.' . $col;
+
+ # Check there's an entry in the MARC subfield structure for the field
+ if ( exists $mss->{$field}
+ && scalar @{ $mss->{$field} } > 0
+ && $mss->{$field}[0]->{authorised_value} )
+ {
+ my $subfield = $mss->{$field}[0];
+ my $code = $subfield->{authorised_value};
+
+ my $str = C4::Biblio::GetAuthorisedValueDesc( $itemtagfield, $subfield->{tagsubfield}, $self->$col, '', $tagslib, undef, $params->{public} );
+ my $type = exists $code_to_type->{$code} ? $code_to_type->{$code} : 'av';
+ $strings->{$col} = {
+ str => $str,
+ type => $type,
+ ( $type eq 'av' ? ( category => $code ) : () ),
+ };
+ }
+ }
+
+ return $strings;
+}
+
=head3 _type
=cut