Koha-clms/Koha/MarcOrder.pm

1256 lines
48 KiB
Perl

package Koha::MarcOrder;
# Copyright 2023, PTFS-Europe Ltd
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use Modern::Perl;
use Try::Tiny qw( catch try );
use Net::FTP;
use base qw(Koha::Object);
use C4::Matcher;
use C4::ImportBatch qw(
RecordsFromMARCXMLFile
RecordsFromISO2709File
RecordsFromMarcPlugin
BatchStageMarcRecords
BatchFindDuplicates
SetImportBatchMatcher
SetImportBatchOverlayAction
SetImportBatchNoMatchAction
SetImportBatchItemAction
SetImportBatchStatus
GetImportBatchOverlayAction
GetImportBatchNoMatchAction
GetImportBatchItemAction
GetImportBatch
GetImportBatchRangeDesc
GetNumberOfNonZ3950ImportBatches
);
use C4::Search qw( FindDuplicate );
use C4::Acquisition qw( NewBasket );
use C4::Biblio qw(
AddBiblio
GetMarcFromKohaField
TransformHtmlToXml
GetMarcQuantity
GetMarcPrice
);
use C4::Items qw( AddItemFromMarc );
use C4::Budgets qw( GetBudgetByCode );
use Koha::Database;
use Koha::ImportBatchProfiles;
use Koha::ImportBatches;
use Koha::Import::Records;
use Koha::Acquisition::Currencies;
use Koha::Acquisition::Booksellers;
use Koha::Acquisition::Baskets;
use Koha::Plugins;
=head1 NAME
Koha::MarcOrder - Koha Marc Order Object class
=head1 API
=head2 Class methods
=cut
=head3 create_order_lines_from_file
my $result = Koha::MarcOrder->create_order_lines_from_file($args);
Controller for file staging, basket creation and order line creation when using the cronjob in marc_ordering_process.pl
=cut
sub create_order_lines_from_file {
my ( $self, $args ) = @_;
my $filename = $args->{filename};
my $filepath = $args->{filepath};
my $profile = $args->{profile};
my $agent = $args->{agent};
my $success;
my $error;
my $vendor_id = $profile->vendor_id;
my $budget_id = $profile->budget_id;
my $vendor_record = Koha::Acquisition::Booksellers->find( { id => $vendor_id } );
my $basket_id = _create_basket_for_file(
{
filename => $filename,
vendor_id => $vendor_id
}
);
my $format = index( $filename, '.mrc' ) != -1 ? 'ISO2709' : 'MARCXML';
my $params = {
record_type => $profile->record_type,
encoding => $profile->encoding,
format => $format,
filepath => $filepath,
filename => $filename,
comments => undef,
parse_items => $profile->parse_items,
matcher_id => $profile->matcher_id,
overlay_action => $profile->overlay_action,
nomatch_action => $profile->nomatch_action,
item_action => $profile->item_action,
};
try {
my $import_batch_id = _stage_file($params);
my $import_records = Koha::Import::Records->search(
{
import_batch_id => $import_batch_id,
}
);
while ( my $import_record = $import_records->next ) {
my $result = add_biblio_from_import_record(
{
import_batch_id => $import_batch_id,
import_record => $import_record,
matcher_id => $params->{matcher_id},
overlay_action => $params->{overlay_action},
agent => $agent,
}
);
warn "Duplicates found in $result->{duplicates_in_batch}, record was skipped."
if $result->{duplicates_in_batch};
next if $result->{skip};
my $order_line_details = add_items_from_import_record(
{
record_result => $result->{record_result},
basket_id => $basket_id,
vendor => $vendor_record,
budget_id => $budget_id,
agent => $agent,
}
);
}
SetImportBatchStatus( $import_batch_id, 'imported' )
if Koha::Import::Records->search( { import_batch_id => $import_batch_id, status => 'imported' } )->count ==
Koha::Import::Records->search( { import_batch_id => $import_batch_id } )->count;
$success = 1;
} catch {
$success = 0;
$error = $_;
};
return $success ? { success => 1, error => '' } : { success => 0, error => $error };
}
=head3 import_record_and_create_order_lines
my $result = Koha::MarcOrder->import_record_and_create_order_lines($args);
Controller for record import and order line creation when using the interface in addorderiso2709.pl
=cut
sub import_record_and_create_order_lines {
my ( $self, $args ) = @_;
my $import_batch_id = $args->{import_batch_id};
my $import_record_id_selected = $args->{import_record_id_selected} || ();
my $matcher_id = $args->{matcher_id};
my $overlay_action = $args->{overlay_action};
my $import_record = $args->{import_record};
my $client_item_fields = $args->{client_item_fields};
my $agent = $args->{agent};
my $basket_id = $args->{basket_id};
my $budget_id = $args->{budget_id};
my $vendor = $args->{vendor};
my $result = add_biblio_from_import_record(
{
import_batch_id => $import_batch_id,
import_record => $import_record,
matcher_id => $matcher_id,
overlay_action => $overlay_action,
agent => $agent,
import_record_id_selected => $import_record_id_selected,
}
);
return {
duplicates_in_batch => $result->{duplicates_in_batch},
skip => $result->{skip}
} if $result->{skip};
my $itemnumbers = add_items_from_import_record(
{
record_result => $result->{record_result},
basket_id => $basket_id,
vendor => $vendor,
budget_id => $budget_id,
agent => $agent,
client_item_fields => $client_item_fields
}
);
return {
duplicates_in_batch => 0,
skip => 0
};
}
=head3 _create_basket_for_file
my $basket_id = _create_basket_for_file({
filename => $filename,
vendor_id => $vendor_id
});
Creates a basket ready to receive order lines based on the imported file
=cut
sub _create_basket_for_file {
my ($args) = @_;
my $filename = $args->{filename};
my $vendor_id = $args->{vendor_id};
# aqbasketname.basketname has a max length of 50 characters so long file names will need to be truncated
my $basketname = length($filename) > 50 ? substr( $filename, 0, 50 ) : $filename;
my $basketno = NewBasket(
$vendor_id, 0, $basketname, q{},
q{}, q{}, q{}, q{}, 0, undef
);
return $basketno;
}
=head3 _stage_file
$file->_stage_file($params)
Stages a file directly using parameters from a MARC ordering account
=cut
sub _stage_file {
my ($args) = @_;
my $result = Koha::ImportBatch->new_from_file($args);
return $result->{report}->{import_batch_id};
}
=head3 _get_syspref_mappings
my $syspref_info = _get_syspref_mappings( $marcrecord, $syspref_name );
Fetches data from a MARC record based on the mappings in the syspref MarcFieldsToOrder or MarcItemFieldsToOrder using the fields selected in $fields (array).
=cut
sub _get_syspref_mappings {
my ( $record, $syspref_to_read ) = @_;
my $syspref = C4::Context->yaml_preference($syspref_to_read);
my @result;
my @tags_list;
# Check tags in syspref definition
for my $field_name ( keys %$syspref ) {
my @fields = split /\|/, $syspref->{$field_name};
for my $field (@fields) {
my ( $f, $sf ) = split /\$/, $field;
next unless $f and $sf;
push @tags_list, $f;
}
}
@tags_list = List::MoreUtils::uniq(@tags_list);
my $tags_count = _verify_number_of_fields( \@tags_list, $record );
# Return if the number of these fields in the record is not the same.
die "Invalid number of fields detected on field $tags_count->{key}, please check this file" if $tags_count->{error};
# Gather the fields
my $fields_hash;
foreach my $tag (@tags_list) {
my @tmp_fields;
foreach my $field ( $record->field($tag) ) {
push @tmp_fields, $field;
}
$fields_hash->{$tag} = \@tmp_fields;
}
if ( $tags_count->{count} ) {
for ( my $i = 0 ; $i < $tags_count->{count} ; $i++ ) {
my $r;
for my $field_name ( keys %$syspref ) {
my @fields = split /\|/, $syspref->{$field_name};
for my $field (@fields) {
my ( $f, $sf ) = split /\$/, $field;
next unless $f and $sf;
my $v = $fields_hash->{$f}[$i] ? $fields_hash->{$f}[$i]->subfield($sf) : undef;
$r->{$field_name} = $v if ( defined $v );
last if $syspref->{$field};
}
}
push @result, $r;
}
}
return \@result if $syspref_to_read eq 'MarcItemFieldsToOrder';
return $result[0] if $syspref_to_read eq 'MarcFieldsToOrder';
}
=head3 _verify_number_of_fields
my $tags_count = _verify_number_of_fields(\@tags_list, $record);
Verifies that the number of fields in the record is consistent for each field
=cut
sub _verify_number_of_fields {
my ( $tags_list, $record ) = @_;
my $tag_fields_count;
for my $tag (@$tags_list) {
my @fields = $record->field($tag);
$tag_fields_count->{$tag} = scalar @fields;
}
my $tags_count;
foreach my $key ( keys %$tag_fields_count ) {
if ( $tag_fields_count->{$key} > 0 ) { # Having 0 of a field is ok
$tags_count //= $tag_fields_count->{$key}; # Start with the count from the first occurrence
return { error => 1, key => $key }
if $tag_fields_count->{$key} !=
$tags_count; # All counts of various fields should be equal if they exist
}
}
return { error => 0, count => $tags_count };
}
=head3 add_biblio_from_import_record
my ($record_results, $duplicates_in_batch) = add_biblio_from_import_record({
import_record => $import_record,
matcher_id => $matcher_id,
overlay_action => $overlay_action,
import_record_id_selected => $import_record_id_selected,
agent => $agent,
import_batch_id => $import_batch_id
});
Takes a set of import records and adds biblio records based on the file content.
Params matcher_id and overlay_action are taken from the MARC ordering account.
Returns the new or matched biblionumber and the MARC record for each import record.
=cut
sub add_biblio_from_import_record {
my ($args) = @_;
my $import_batch_id = $args->{import_batch_id};
my $import_record_id_selected = $args->{import_record_id_selected} || ();
my $matcher_id = $args->{matcher_id};
my $overlay_action = $args->{overlay_action};
my $import_record = $args->{import_record};
my $agent = $args->{agent} || "";
my $duplicates_in_batch;
my $duplicates_found = 0;
if ( $agent eq 'client' ) {
return {
record_result => 0,
duplicates_in_batch => 0,
skip => 1
} if not grep { $_ eq $import_record->import_record_id } @{$import_record_id_selected};
}
my $marcrecord = $import_record->get_marc_record || die "Couldn't translate MARC information";
my $matches = $import_record->get_import_record_matches( { chosen => 1 } );
my $match = $matches->count ? $matches->next : undef;
my $biblionumber = $match ? $match->candidate_match_id : 0;
my $biblio = $biblionumber ? Koha::Biblios->find($biblionumber) : undef;
if ($biblio) {
$import_record->status('imported')->store;
if ( $overlay_action eq 'replace' ) {
$import_record->replace( { biblio => $biblio } );
}
} else {
if ($matcher_id) {
if ( $matcher_id eq '_TITLE_AUTHOR_' ) {
my @matches = FindDuplicate($marcrecord);
$duplicates_found = 1 if @matches;
} else {
my $matcher = C4::Matcher->fetch($matcher_id);
my @matches = $matcher->get_matches( $marcrecord, my $max_matches = 1 );
$duplicates_found = 1 if @matches;
}
return {
record_result => 0,
duplicates_in_batch => $import_batch_id,
skip => 1
} if $duplicates_found;
}
# add the biblio if no matches were found
if ( !$duplicates_found ) {
( $biblionumber, undef ) = AddBiblio( $marcrecord, '' );
$import_record->status('imported')->store;
}
}
$import_record->import_biblio->matched_biblionumber($biblionumber)->store;
my $record_result = {
biblionumber => $biblionumber,
marcrecord => $marcrecord,
import_record_id => $import_record->import_record_id,
};
return {
record_result => $record_result,
duplicates_in_batch => $duplicates_in_batch,
skip => 0
};
}
=head3 add_items_from_import_record
my $order_line_details = add_items_from_import_record({
record_result => $record_result,
basket_id => $basket_id,
vendor => $vendor,
budget_id => $budget_id,
agent => $agent,
client_item_fields => $client_item_fields
});
Adds items to biblio records based on mappings in MarcItemFieldsToOrder.
Returns an array of order line details based on newly added items.
If being called from addorderiso2709.pl then client_item_fields is a hash of all the UI form inputs needed by the script.
=cut
sub add_items_from_import_record {
my ($args) = @_;
my $record_result = $args->{record_result};
my $basket_id = $args->{basket_id};
my $budget_id = $args->{budget_id};
my $vendor = $args->{vendor};
my $agent = $args->{agent};
my $client_item_fields = $args->{client_item_fields} || undef;
my $active_currency = Koha::Acquisition::Currencies->get_active;
my $biblionumber = $record_result->{biblionumber};
my $marcrecord = $record_result->{marcrecord};
if ( $agent eq 'cron' ) {
my $marc_fields_to_order = _get_syspref_mappings( $marcrecord, 'MarcFieldsToOrder' );
my $marc_item_fields_to_order = _get_syspref_mappings( $marcrecord, 'MarcItemFieldsToOrder' );
my $item_fields = _create_item_fields_from_syspref(
{
marc_fields_to_order => $marc_fields_to_order,
marc_item_fields_to_order => $marc_item_fields_to_order,
budget_id => $budget_id
}
);
my $order_line_fields = parse_input_into_order_line_fields(
{
agent => $agent,
biblionumber => $biblionumber,
budget_id => $budget_id,
basket_id => $basket_id,
fields => $item_fields,
marcrecord => $marcrecord,
}
);
my $itemnumbers = create_items_and_generate_order_hash(
{
fields => $order_line_fields,
vendor => $vendor,
agent => $agent,
active_currency => $active_currency,
}
);
return $itemnumbers;
}
if ( $agent eq 'client' ) {
my $order_line_fields = parse_input_into_order_line_fields(
{
agent => $agent,
biblionumber => $biblionumber,
budget_id => $budget_id,
basket_id => $basket_id,
fields => $client_item_fields,
marcrecord => $marcrecord,
}
);
my $order_line_details = create_items_and_generate_order_hash(
{
fields => $order_line_fields,
vendor => $vendor,
agent => $agent,
active_currency => $active_currency,
}
);
return $order_line_details;
}
}
=head3 match_file_to_account
my $file_match = Koha::MarcOrder->match_file_to_account({
filename => $filename,
filepath => $filepath,
profile => $profile
});
Used by the cronjob to detect whether a file matches the account and should be processed
This method only checks the first record in the file.
=cut
sub match_file_to_account {
my ( $self, $args ) = @_;
my $match = 0;
my $filename = $args->{filename};
my $filepath = $args->{filepath};
my $profile = $args->{profile};
my $format = index( $filename, '.mrc' ) != -1 ? 'ISO2709' : 'MARCXML';
my ( $errors, $marcrecords );
if ( $format eq 'MARCXML' ) {
( $errors, $marcrecords ) = C4::ImportBatch::RecordsFromMARCXMLFile( $filepath, $profile->encoding );
} elsif ( $format eq 'ISO2709' ) {
( $errors, $marcrecords ) = C4::ImportBatch::RecordsFromISO2709File(
$filepath, $profile->record_type,
$profile->encoding
);
}
my $match_record = @{$marcrecords}[0];
my ( $field, $subfield ) = split /\$/, $profile->match_field;
my $field_value = $match_record->subfield( $field, $subfield ) || '';
my $match_value = $profile->match_value || '';
if ( $field_value eq $match_value ) {
$match = 1;
}
return $match;
}
=head3 import_batches_list
Fetches import batches matching the batch to be added to the basket and returns these to the template
Koha::MarcOrder->import_batches_list();
=cut
sub import_batches_list {
my ($self) = @_;
my $batches = GetImportBatchRangeDesc();
my @list = ();
foreach my $batch (@$batches) {
if ( $batch->{'import_status'} =~ /^staged$|^reverted$/ && $batch->{'record_type'} eq 'biblio' ) {
# check if there is at least 1 line still staged
my $import_records_count = Koha::Import::Records->search(
{
import_batch_id => $batch->{'import_batch_id'},
status => $batch->{import_status}
}
)->count;
if ($import_records_count) {
push @list, {
import_batch_id => $batch->{'import_batch_id'},
num_records => $batch->{'num_records'},
num_items => $batch->{'num_items'},
staged_date => $batch->{'upload_timestamp'},
import_status => $batch->{'import_status'},
file_name => $batch->{'file_name'},
comments => $batch->{'comments'},
};
} else {
# if there are no more line to includes, set the status to imported
# FIXME This should be removed in the future.
SetImportBatchStatus( $batch->{'import_batch_id'}, 'imported' );
}
}
}
my $num_batches = GetNumberOfNonZ3950ImportBatches();
return {
list => \@list,
num_results => $num_batches
};
}
=head3 import_biblios_list
For an import batch, this function reads the files and creates all the relevant data pertaining to that file
It then returns this to the template to be shown in the UI
Koha::MarcOrder->import_biblios_list( $cgiparams->{'import_batch_id'} );
=cut
sub import_biblios_list {
my ( $self, $import_batch_id ) = @_;
my $batch = GetImportBatch( $import_batch_id, 'staged' );
return () unless $batch and $batch->{import_status} =~ /^staged$|^reverted$/;
my $import_records = Koha::Import::Records->search(
{
import_batch_id => $import_batch_id,
status => $batch->{import_status}
}
);
my @list = ();
my $item_error = 0;
my $ccodes = {
map { $_->{authorised_value} => $_->{opac_description} }
Koha::AuthorisedValues->get_descriptions_by_koha_field(
{ frameworkcode => '', kohafield => 'items.ccode' }
)
};
my $locations = {
map { $_->{authorised_value} => $_->{opac_description} }
Koha::AuthorisedValues->get_descriptions_by_koha_field(
{ frameworkcode => '', kohafield => 'items.location' }
)
};
my $notforloans = {
map { $_->{authorised_value} => $_->{lib} } Koha::AuthorisedValues->get_descriptions_by_koha_field(
{ frameworkcode => '', kohafield => 'items.notforloan' }
)
};
# location list
my @locations;
foreach ( sort keys %$locations ) {
push @locations, { code => $_, description => "$_ - " . $locations->{$_} };
}
my @ccodes;
foreach ( sort { $ccodes->{$a} cmp $ccodes->{$b} } keys %$ccodes ) {
push @ccodes, { code => $_, description => $ccodes->{$_} };
}
my @notforloans;
foreach ( sort { $notforloans->{$a} cmp $notforloans->{$b} } keys %$notforloans ) {
push @notforloans, { code => $_, description => $notforloans->{$_} };
}
my $biblio_count = 0;
while ( my $import_record = $import_records->next ) {
my $item_id = 1;
$biblio_count++;
my $matches = $import_record->get_import_record_matches( { chosen => 1 } );
my $match = $matches->count ? $matches->next : undef;
my $match_biblio = $match ? Koha::Biblios->find( { biblionumber => $match->candidate_match_id } ) : undef;
my %cellrecord = (
import_record_id => $import_record->import_record_id,
import_biblio => $import_record->import_biblio,
import => 1,
status => $import_record->status,
record_sequence => $import_record->record_sequence,
overlay_status => $import_record->overlay_status,
match_biblionumber => $match ? $match->candidate_match_id : 0,
match_citation => $match_biblio
? ( $match_biblio->title || '' ) . ' ' . ( $match_biblio->author || '' )
: '',
match_score => $match ? $match->score : 0,
);
my $marcrecord = $import_record->get_marc_record || die "couldn't translate MARC information";
my $infos = _get_syspref_mappings( $marcrecord, 'MarcFieldsToOrder' );
my $price =
$infos->{price} ? $infos->{price} : GetMarcPrice( $marcrecord, C4::Context->preference('marcflavour') );
my $replacementprice = $infos->{replacementprice} || undef;
my $quantity = $infos->{quantity} || undef;
my $budget_code = $infos->{budget_code} || undef;
my $discount = $infos->{discount} || undef;
my $sort1 = $infos->{sort1} || undef;
my $sort2 = $infos->{sort2} || undef;
my $budget_id;
if ($budget_code) {
my $biblio_budget = GetBudgetByCode($budget_code);
if ($biblio_budget) {
$budget_id = $biblio_budget->{budget_id};
}
}
# Items
my @itemlist = ();
my $all_items_quantity = 0;
my $alliteminfos = _get_syspref_mappings( $marcrecord, 'MarcItemFieldsToOrder' );
if ( $alliteminfos != -1 ) {
foreach my $iteminfos (@$alliteminfos) {
# Quantity is required, default to one if not supplied
my $quantity = delete $iteminfos->{quantity} || 1;
# Handle incorrectly named original parameters for MarcItemFieldsToOrder
$iteminfos->{location} = delete $iteminfos->{loc} if $iteminfos->{loc};
# Convert budget code to a budget id
my $item_budget_code = delete $iteminfos->{budget_code};
if ($item_budget_code) {
my $item_budget = GetBudgetByCode($item_budget_code);
$iteminfos->{budget_id} = $item_budget->{budget_id} || $budget_id;
}
# Clone the item data for the needed quantity
# Add the incremented item id for each item in that quantity
for ( my $i = 0 ; $i < $quantity ; $i++ ) {
my $itemrecord = {%$iteminfos};
$itemrecord->{item_id} = $item_id++;
$all_items_quantity++;
push @itemlist, $itemrecord;
}
}
$cellrecord{'iteminfos'} = \@itemlist;
} else {
$cellrecord{'item_error'} = 1;
}
push @list, \%cellrecord;
# If MarcItemFieldsToOrder is not set, we use MarcFieldsToOrder to populate the order form.
if ( $alliteminfos || %$alliteminfos == -1 ) {
$cellrecord{price} = $price || '';
$cellrecord{replacementprice} = $replacementprice || '';
$cellrecord{quantity} = $quantity || '';
$cellrecord{budget_id} = $budget_id || '';
} else {
# When using MarcItemFields to order we want the order to have the same quantity as total items
$cellrecord{quantity} = $all_items_quantity;
}
# The fields discount, sort1, and sort2 only exist at the order level, so always use MarcItemFieldsToOrder
$cellrecord{discount} = $discount || '';
$cellrecord{sort1} = $sort1 || '';
$cellrecord{sort2} = $sort2 || '';
}
my $num_records = $batch->{'num_records'};
my $overlay_action = GetImportBatchOverlayAction($import_batch_id);
my $nomatch_action = GetImportBatchNoMatchAction($import_batch_id);
my $item_action = GetImportBatchItemAction($import_batch_id);
my $result = {
import_biblio_list => \@list,
num_results => $num_records,
import_batch_id => $import_batch_id,
overlay_action => $overlay_action,
nomatch_action => $nomatch_action,
item_action => $item_action,
item_error => $item_error,
locationloop => \@locations,
ccodeloop => \@ccodes,
notforloanloop => \@notforloans,
batch => $batch,
};
if ( $batch->{'num_records'} > 0 ) {
if ( $batch->{'import_status'} eq 'staged' or $batch->{'import_status'} eq 'reverted' ) {
$result->{can_commit} = 1;
}
if ( $batch->{'import_status'} eq 'imported' ) {
$result->{can_revert} = 1;
}
}
if ( defined $batch->{'matcher_id'} ) {
my $matcher = C4::Matcher->fetch( $batch->{'matcher_id'} );
if ( defined $matcher ) {
$result->{'current_matcher_id'} = $batch->{'matcher_id'};
$result->{'current_matcher_code'} = $matcher->code();
$result->{'current_matcher_description'} = $matcher->description();
}
}
my @matchers = C4::Matcher::GetMatcherList();
if ( defined $batch->{'matcher_id'} ) {
for ( my $i = 0 ; $i <= $#matchers ; $i++ ) {
if ( $matchers[$i]->{'matcher_id'} == $batch->{'matcher_id'} ) {
$matchers[$i]->{'selected'} = 1;
}
}
}
$result->{available_matchers} = \@matchers;
return $result;
}
=head3 parse_input_into_order_line_fields
This function takes inputs from either the cronjob or UI and then parses that into a single set of order line fields that can be used to create items and order lines
my $order_line_fields = parse_input_into_order_line_fields(
{
agent => $agent,
biblionumber => $biblionumber,
budget_id => $budget_id,
basket_id => $basket_id,
fields => $item_fields,
marcrecord => $marcrecord,
}
);
=cut
sub parse_input_into_order_line_fields {
my ($args) = @_;
my $agent = $args->{agent};
my $client = $agent eq 'client' ? 1 : 0;
my $biblionumber = $args->{biblionumber};
my $budget_id = $args->{budget_id};
my $basket_id = $args->{basket_id};
my $fields = $args->{fields};
my $marcrecord = $args->{marcrecord};
my $quantity = $fields->{quantity} || $fields->{c_quantity} || 1;
my @homebranches = $fields->{homebranches} ? @{ $fields->{homebranches} } : ();
my @holdingbranches = $fields->{holdingbranches} ? @{ $fields->{holdingbranches} } : ();
my @itypes = $fields->{itypes} ? @{ $fields->{itypes} } : ();
my @nonpublic_notes = $fields->{nonpublic_notes} ? @{ $fields->{nonpublic_notes} } : ();
my @public_notes = $fields->{public_notes} ? @{ $fields->{public_notes} } : ();
my @locs = $fields->{locs} ? @{ $fields->{locs} } : ();
my @ccodes = $fields->{ccodes} ? @{ $fields->{ccodes} } : ();
my @notforloans = $fields->{notforloans} ? @{ $fields->{notforloans} } : ();
my @uris = $fields->{uris} ? @{ $fields->{uris} } : ();
my @copynos = $fields->{copynos} ? @{ $fields->{copynos} } : ();
my @itemprices = $fields->{itemprices} ? @{ $fields->{itemprices} } : ();
my @replacementprices = $fields->{replacementprices} ? @{ $fields->{replacementprices} } : ();
my @itemcallnumbers = $fields->{itemcallnumbers} ? @{ $fields->{itemcallnumbers} } : ();
my @coded_location_qualifiers =
$fields->{coded_location_qualifiers} ? @{ $fields->{coded_location_qualifiers} } : ();
my @barcodes = $fields->{barcodes} ? @{ $fields->{barcodes} } : ();
my @enumchrons = $fields->{enumchrons} ? @{ $fields->{enumchrons} } : ();
my @budget_codes = $fields->{budget_codes} ? @{ $fields->{budget_codes} } : ();
my $c_quantity = $fields->{c_quantity};
my $c_budget = GetBudgetByCode( $fields->{c_budget_code} );
my $c_budget_id = $c_budget->{budget_id} || $fields->{c_budget_id};
my $c_discount = $fields->{c_discount};
my $c_sort1 = $fields->{c_sort1};
my $c_sort2 = $fields->{c_sort2};
my $c_replacement_price = $fields->{c_replacement_price};
my $c_price = $fields->{c_price};
# If using the cronjob, we want to default to the account budget if not mapped on the record
if ( !$client && ( !@budget_codes || scalar(@budget_codes) == 0 ) ) {
for ( 1 .. $quantity ) {
if ($c_budget_id) {
push( @budget_codes, $c_budget_id );
} else {
push( @budget_codes, $budget_id );
}
}
}
my $loop_limit = $quantity;
my $order_line_fields = {
biblionumber => $biblionumber,
homebranch => \@homebranches,
holdingbranch => \@holdingbranches,
itemnotes_nonpublic => \@nonpublic_notes,
itemnotes => \@public_notes,
location => \@locs,
ccode => \@ccodes,
itype => \@itypes,
notforloan => \@notforloans,
uri => \@uris,
copynumber => \@copynos,
price => \@itemprices,
replacementprice => \@replacementprices,
itemcallnumber => \@itemcallnumbers,
coded_location_qualifier => \@coded_location_qualifiers,
barcode => \@barcodes,
enumchron => \@enumchrons,
budget_code => \@budget_codes,
loop_limit => $loop_limit,
basket_id => $basket_id,
budget_id => $budget_id,
c_quantity => $c_quantity,
c_budget_id => $c_budget_id,
c_discount => $c_discount,
c_sort1 => $c_sort1,
c_sort2 => $c_sort2,
c_replacement_price => $c_replacement_price,
c_price => $c_price,
marcrecord => $marcrecord,
tags => $fields->{tags},
subfields => $fields->{subfields},
field_values => $fields->{field_values},
serials => $fields->{serials},
order_vendornote => $fields->{order_vendornote},
order_internalnote => $fields->{order_internalnote},
all_currency => $fields->{all_currency},
};
return $order_line_fields;
}
=head3 create_items_and_generate_order_hash
This method is used from both the cronjob and the UI to create items and generate order line details for those new items
my $order_line_details = create_items_and_generate_order_hash(
{
fields => $order_line_fields,
vendor => $vendor,
agent => $agent,
active_currency => $active_currency,
}
);
=cut
sub create_items_and_generate_order_hash {
my ($args) = @_;
my $agent = $args->{agent};
my $fields = $args->{fields};
my $loop_limit = $fields->{loop_limit};
my $basket_id = $fields->{basket_id};
my $budget_id = $fields->{budget_id};
my $vendor = $args->{vendor};
my $active_currency = $args->{active_currency};
my $itemcreation = 0;
my @itemnumbers;
# We directly create the items if MarcItemFieldsToOrder is populated (item fields have values)
if ( C4::Context->preference('AcqCreateItem') ne 'cataloguing' && @{ $fields->{homebranch} } ) {
for ( my $i = 0 ; $i < $loop_limit ; $i++ ) {
$itemcreation = 1;
my $item = Koha::Item->new(
{
biblionumber => $fields->{biblionumber},
homebranch => @{ $fields->{homebranch} }[$i],
holdingbranch => @{ $fields->{holdingbranch} }[$i],
itemnotes_nonpublic => @{ $fields->{itemnotes_nonpublic} }[$i],
itemnotes => @{ $fields->{itemnotes} }[$i],
location => @{ $fields->{location} }[$i],
ccode => @{ $fields->{ccode} }[$i],
itype => @{ $fields->{itype} }[$i],
notforloan => @{ $fields->{notforloan} }[$i],
uri => @{ $fields->{uri} }[$i],
copynumber => @{ $fields->{copynumber} }[$i],
price => @{ $fields->{price} }[$i],
replacementprice => @{ $fields->{replacementprice} }[$i],
itemcallnumber => @{ $fields->{itemcallnumber} }[$i],
coded_location_qualifier => @{ $fields->{coded_location_qualifier} }[$i],
barcode => @{ $fields->{barcode} }[$i],
enumchron => @{ $fields->{enumchron} }[$i],
}
)->store;
push( @itemnumbers, $item->itemnumber );
}
}
if ( $itemcreation == 1 ) {
# Group orderlines from MarcItemFieldsToOrder
my $budget_hash;
my @budget_ids = @{ $fields->{budget_code} };
for ( my $i = 0 ; $i < $loop_limit ; $i++ ) {
$budget_ids[$i] = $budget_id if !$budget_ids[$i]; # Use default budget if no budget provided
$budget_hash->{ $budget_ids[$i] }->{quantity} += 1;
$budget_hash->{ $budget_ids[$i] }->{price} = @{ $fields->{price} }[$i] || $fields->{c_price};
$budget_hash->{ $budget_ids[$i] }->{replacementprice} =
@{ $fields->{replacementprice} }[$i] || $fields->{c_replacement_price};
$budget_hash->{ $budget_ids[$i] }->{itemnumbers} //= [];
push @{ $budget_hash->{ $budget_ids[$i] }->{itemnumbers} },
$itemnumbers[$i];
}
# Create orderlines from MarcItemFieldsToOrder
while ( my ( $budget_id, $infos ) = each %$budget_hash ) {
if ($budget_id) {
my %orderinfo = (
biblionumber => $fields->{biblionumber},
basketno => $basket_id,
quantity => $infos->{quantity},
budget_id => $budget_id,
currency => $active_currency->currency,
);
my $price = $infos->{price};
if ($price) {
$price = _format_price_to_CurrencyFormat_syspref($price);
$price = Koha::Number::Price->new($price)->unformat;
$orderinfo{tax_rate_on_ordering} = $vendor->tax_rate;
$orderinfo{tax_rate_on_receiving} = $vendor->tax_rate;
my $order_discount = $fields->{c_discount} ? $fields->{c_discount} : $vendor->discount;
$orderinfo{discount} = $order_discount;
$orderinfo{rrp} = $price;
$orderinfo{ecost} = $order_discount ? $price * ( 1 - $order_discount / 100 ) : $price;
$orderinfo{listprice} = $orderinfo{rrp} / $active_currency->rate;
$orderinfo{unitprice} = $orderinfo{ecost};
$orderinfo{sort1} = $fields->{c_sort1};
$orderinfo{sort2} = $fields->{c_sort2};
} else {
$orderinfo{listprice} = 0;
}
$orderinfo{replacementprice} = $infos->{replacementprice} || 0;
# Remove uncertainprice flag if we have found a price in the MARC record
$orderinfo{uncertainprice} = 0 if $orderinfo{listprice};
Koha::Plugins->call(
'before_orderline_create',
{
marcrecord => $fields->{marcrecord},
orderline => \%orderinfo,
marcfields => $fields
}
);
my $order = Koha::Acquisition::Order->new( \%orderinfo );
$order->populate_with_prices_for_ordering();
$order->populate_with_prices_for_receiving();
$order->store;
$order->add_item($_) for @{ $budget_hash->{$budget_id}->{itemnumbers} };
}
}
} else {
# Here we are using the 'MARC' for items from the 'Item information' tab for creating the items
# Add an orderline for each MARC record
# Get quantity in the MARC record (1 if none)
my $quantity = GetMarcQuantity( $fields->{marcrecord}, C4::Context->preference('marcflavour') ) || 1;
my %orderinfo = (
biblionumber => $fields->{biblionumber},
basketno => $basket_id,
quantity => $fields->{c_quantity},
branchcode => C4::Context->userenv()->{'branch'},
budget_id => $fields->{c_budget_id},
uncertainprice => 1,
sort1 => $fields->{c_sort1},
sort2 => $fields->{c_sort2},
order_internalnote => $fields->{order_internalnote},
order_vendornote => $fields->{order_vendornote},
currency => $fields->{all_currency},
replacementprice => $fields->{c_replacement_price},
);
# Get the price if there is one.
if ( $fields->{c_price} ) {
$fields->{c_price} = _format_price_to_CurrencyFormat_syspref( $fields->{c_price} );
$fields->{c_price} = Koha::Number::Price->new( $fields->{c_price} )->unformat;
$orderinfo{tax_rate_on_ordering} = $vendor->tax_rate;
$orderinfo{tax_rate_on_receiving} = $vendor->tax_rate;
my $order_discount = $fields->{c_discount} ? $fields->{c_discount} : $vendor->discount;
$orderinfo{discount} = $order_discount;
$orderinfo{rrp} = $fields->{c_price};
$orderinfo{ecost} =
$order_discount ? $fields->{c_price} * ( 1 - $order_discount / 100 ) : $fields->{c_price};
$orderinfo{listprice} = $orderinfo{rrp} / $active_currency->rate;
$orderinfo{unitprice} = $orderinfo{ecost};
} else {
$orderinfo{listprice} = 0;
}
# Remove uncertainprice flag if we have found a price in the MARC record
$orderinfo{uncertainprice} = 0 if $orderinfo{listprice};
Koha::Plugins->call(
'before_orderline_create',
{
marcrecord => $fields->{marcrecord},
orderline => \%orderinfo,
marcfields => $fields
}
);
my $order = Koha::Acquisition::Order->new( \%orderinfo );
$order->populate_with_prices_for_ordering();
$order->populate_with_prices_for_receiving();
$order->store;
my $basket = Koha::Acquisition::Baskets->find($basket_id);
if ( C4::Context->preference('AcqCreateItem') ne 'cataloguing'
&& $basket->effective_create_items eq 'ordering'
&& !$basket->is_standing )
{
my $tags = $fields->{tags} || [];
my $subfields = $fields->{subfields} || [];
my $field_values = $fields->{field_values} || [];
my $serials = $fields->{serials} || [];
my $xml = TransformHtmlToXml( $tags, $subfields, $field_values );
my $record = MARC::Record::new_from_xml( $xml, 'UTF-8' );
for ( my $qtyloop = 1 ; $qtyloop <= $fields->{c_quantity} ; $qtyloop++ ) {
my ( $biblionumber, undef, $itemnumber ) =
AddItemFromMarc( $record, $fields->{biblionumber} );
$order->add_item($itemnumber);
push @itemnumbers, $itemnumber;
}
}
}
return \@itemnumbers;
}
=head3 _format_price_to_CurrencyFormat_syspref
In France, the cents separator is the ',' but sometimes a '.' is used
In this case, the price will be x100 when unformatted
The '.' needs replacing by a ',' to get a proper price calculation
=cut
sub _format_price_to_CurrencyFormat_syspref {
my ($price) = @_;
$price =~ s/\./,/ if C4::Context->preference("CurrencyFormat") eq "FR";
return $price;
}
=head3 _create_item_fields_from_syspref
Takes the two sysprefs and returns the item fields required to process and create orderlines
my $item_fields = _create_item_fields_from_syspref(
{
marc_fields_to_order => $marc_fields_to_order,
marc_item_fields_to_order => $marc_item_fields_to_order
}
);
=cut
sub _create_item_fields_from_syspref {
my ($args) = @_;
my $marc_item_fields_to_order = $args->{marc_item_fields_to_order};
my $marc_fields_to_order = $args->{marc_fields_to_order};
my $budget_id = $args->{budget_id};
my @homebranches = ();
my @holdingbranches = ();
my @itypes = ();
my @nonpublic_notes = ();
my @public_notes = ();
my @locs = ();
my @ccodes = ();
my @notforloans = ();
my @uris = ();
my @copynos = ();
my @budget_codes = ();
my @itemprices = ();
my @replacementprices = ();
my @itemcallnumbers = ();
my @coded_location_qualifiers = ();
my @barcodes = ();
my @enumchrons = ();
foreach my $infoset (@$marc_item_fields_to_order) {
my $quantity = $infoset->{quantity} || 1;
for ( my $i = 0 ; $i < $quantity ; $i++ ) {
my $budget_code;
if ( !$infoset->{budget_code} ) {
if ( $marc_fields_to_order->{budget_code} ) {
$budget_code = $marc_fields_to_order->{budget_code};
}
} else {
$budget_code = $infoset->{budget_code};
}
my $item_budget_id;
my $item_budget = GetBudgetByCode($budget_code);
if ($item_budget) {
$item_budget_id = $item_budget->{budget_id};
} else {
$item_budget_id = $budget_id;
}
push @homebranches, $infoset->{homebranch};
push @holdingbranches, $infoset->{holdingbranch};
push @itypes, $infoset->{itype};
push @nonpublic_notes, $infoset->{nonpublic_note};
push @public_notes, $infoset->{public_note};
push @locs, $infoset->{loc};
push @ccodes, $infoset->{ccode};
push @notforloans, $infoset->{notforloan};
push @uris, $infoset->{uri};
push @copynos, $infoset->{copyno};
push @budget_codes, $item_budget_id;
push @itemprices, $infoset->{price};
push @replacementprices, $infoset->{replacementprice};
push @itemcallnumbers, $infoset->{itemcallnumber};
push @coded_location_qualifiers, $infoset->{coded_location_qualifier};
push @barcodes, $infoset->{barcode};
push @enumchrons, $infoset->{enumchron};
}
}
my $item_fields = {
quantity => scalar(@homebranches),
homebranches => \@homebranches,
holdingbranches => \@holdingbranches,
itypes => \@itypes,
nonpublic_notes => \@nonpublic_notes,
public_notes => \@public_notes,
locs => \@locs,
ccodes => \@ccodes,
notforloans => \@notforloans,
uris => \@uris,
copynos => \@copynos,
budget_codes => \@budget_codes,
itemprices => \@itemprices,
replacementprices => \@replacementprices,
itemcallnumbers => \@itemcallnumbers,
coded_location_qualifiers => \@coded_location_qualifiers,
barcodes => \@barcodes,
enumchrons => \@enumchrons,
c_quantity => $marc_fields_to_order->{quantity},
c_budget_code => $marc_fields_to_order->{budget_code},
c_price => $marc_fields_to_order->{price},
c_discount => $marc_fields_to_order->{discount},
c_sort1 => $marc_fields_to_order->{sort1},
c_sort2 => $marc_fields_to_order->{sort2},
c_replacement_price => $marc_fields_to_order->{replacementprice},
};
return $item_fields;
}
1;