use Koha::Checkouts;
use Koha::CirculationRules;
use Koha::CoverImages;
+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::Recalls;
use Koha::Result::Boolean;
use Koha::SearchEngine::Indexer;
use Koha::StockRotationItem;
$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
&& !$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 );
$error = "book_on_loan" if $self->checkout;
- $error = "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
- $error = "book_reserved"
- if $self->holds->search( { found => [ 'W', 'T' ] } )->count;
+ $error //= "book_reserved"
+ if $self->holds->filter_by_found->count;
- $error = "linked_analytics"
+ $error //= "linked_analytics"
if C4::Items::GetAnalyticsCount( $self->itemnumber ) > 0;
- $error = "last_item_for_hold"
+ $error //= "last_item_for_hold"
if $self->biblio->items->count == 1
&& $self->biblio->holds->search(
{
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
sub get_transfer {
my ($self) = @_;
- my $transfer_rs = $self->_result->branchtransfers->search(
- {
- datearrived => undef,
- datecancelled => undef
- },
- {
- order_by =>
- [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
- rows => 1
- }
- )->first;
- return unless $transfer_rs;
- return Koha::Item::Transfer->_new_from_dbic($transfer_rs);
+
+ return $self->get_transfers->search( {}, { rows => 1 } )->next;
}
=head3 get_transfers
sub get_transfers {
my ($self) = @_;
- my $transfer_rs = $self->_result->branchtransfers->search(
- {
- datearrived => undef,
- datecancelled => undef
- },
- {
- order_by =>
- [ { -desc => 'datesent' }, { -asc => 'daterequested' } ],
- }
- );
- return Koha::Item::Transfers->_new_from_dbic($transfer_rs);
+
+ 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
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 $field = $item->as_marc_field;
my $kohafield = $subfield->{kohafield};
my $tagsubfield = $subfield->{tagsubfield};
my $value;
- if ( defined $kohafield ) {
+ 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
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'
+
+ # 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(
{
'credit_id' => { '!=' => undef },
{ join => 'credit' }
);
- $total_to_refund = ( $credit_offsets->count > 0 )
- ? $credit_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 =
}
}
- # 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'
- },
- {
- order_by => { '-desc' => 'date' },
- rows => 1
- }
- )->single;
-
- if ( $lost_overdue ) {
-
- my $patron = $lost_overdue->patron;
- if ($patron) {
- my $account = $patron->account;
+ # 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 ) {
- # Update status of fine
- $lost_overdue->status('FOUND')->store();
+ my $patron = $lost_overdue->patron;
+ if ($patron) {
+ my $account = $patron->account;
- # Find related forgive credit
- my $refund = $lost_overdue->credits(
+ # 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(
{
- 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 }
- }
- );
- }
+ type => 'info',
+ message => 'lost_restored',
+ payload => { refund_id => $refund->id }
+ }
+ );
+ }
- # Reconcile balances if required
- if ( C4::Context->preference('AccountAutoReconcile') ) {
- $account->reconcile_balance;
- }
+ # Reconcile balances if required
+ if ( C4::Context->preference('AccountAutoReconcile') ) {
+ $account->reconcile_balance;
}
}
- } elsif ( $lostreturn_policy eq 'charge' ) {
+
+ } elsif ( $lostreturn_policy eq 'charge' && ( $lost_overdue || $lost_charge ) ) {
$self->add_message(
{
type => 'info',
}
}
+ 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 ) {
+
+ my $patron = $processing_charge->patron;
+ if ( $patron ) {
+
+ my $account = $patron->account;
+
+ # Credit outstanding amount
+ my $credit_total = $processing_charge->amountoutstanding;
+
+ # 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'
+
+ # 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 = $processing_charge->debit_offsets(
+ {
+ 'credit_id' => { '!=' => undef },
+ 'credit.credit_type_code' => { '!=' => 'Writeoff' }
+ },
+ { join => 'credit' }
+ );
+
+ 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;
+ 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;
+ }
+ }
+ }
+ }
+
return $self;
}
];
}
+=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
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 );
}
=cut
sub add_to_bundle {
- my ( $self, $bundle_item ) = @_;
+ 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;
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 } );
}
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 fro DBIx::Error
+ # 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' ) {
- warn $_->{msg};
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
$_->rethrow();
}
else {
- $_;
+ $_->rethrow();
}
};
}
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