From fca77a4b75e2c73a77082b4b7c3334e4a184219e Mon Sep 17 00:00:00 2001 From: Matt Blenkinsop Date: Mon, 24 Jul 2023 13:53:22 +0000 Subject: [PATCH] Bug 34355: Add cronjob, MarcOrder object and refactored addorderiso2709 script --- Koha/MarcOrder.pm | 766 +++++++++++++++++++++++++ acqui/addorderiso2709.pl | 266 ++------- misc/cronjobs/marc_ordering_process.pl | 129 +++++ 3 files changed, 951 insertions(+), 210 deletions(-) create mode 100644 Koha/MarcOrder.pm create mode 100644 misc/cronjobs/marc_ordering_process.pl diff --git a/Koha/MarcOrder.pm b/Koha/MarcOrder.pm new file mode 100644 index 0000000000..fb073ce9db --- /dev/null +++ b/Koha/MarcOrder.pm @@ -0,0 +1,766 @@ +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 . + +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 +); +use C4::Search qw( FindDuplicate ); +use C4::Acquisition qw( NewBasket ); +use C4::Biblio qw( + AddBiblio + GetMarcFromKohaField + TransformHtmlToXml +); +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; + +=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_biblios_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, + }); + + my $order_lines = create_order_lines({ + order_line_details => $order_line_details + }); + }; + 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_biblios_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 $order_line_details = 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 + }); + + my $order_lines = create_order_lines({ + order_line_details => $order_line_details + }); + + 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{} ); + + return $basketno; +} + +=head3 _stage_file + + $file->_stage_file($params) + + Stages a file directly using parameters from a marc ordering account and without using the background job + This function is a mirror of Koha::BackgroundJob::StageMARCForImport->process but with the background job functionality removed + +=cut + +sub _stage_file { + my ( $args ) = @_; + + my $record_type = $args->{record_type}; + my $encoding = $args->{encoding}; + my $format = $args->{format}; + my $filepath = $args->{filepath}; + my $filename = $args->{filename}; + my $marc_modification_template = $args->{marc_modification_template}; + my $comments = $args->{comments}; + my $parse_items = $args->{parse_items}; + my $matcher_id = $args->{matcher_id}; + my $overlay_action = $args->{overlay_action}; + my $nomatch_action = $args->{nomatch_action}; + my $item_action = $args->{item_action}; + + my @messages; + my ( $batch_id, $num_valid, $num_items, @import_errors ); + my $num_with_matches = 0; + my $checked_matches = 0; + my $matcher_failed = 0; + my $matcher_code = ""; + + my $schema = Koha::Database->new->schema; + try { + $schema->storage->txn_begin; + + my ( $errors, $marcrecords ); + if ( $format eq 'MARCXML' ) { + ( $errors, $marcrecords ) = + C4::ImportBatch::RecordsFromMARCXMLFile( $filepath, $encoding ); + } + elsif ( $format eq 'ISO2709' ) { + ( $errors, $marcrecords ) = + C4::ImportBatch::RecordsFromISO2709File( $filepath, $record_type, + $encoding ); + } + else { # plugin based + $errors = []; + $marcrecords = + C4::ImportBatch::RecordsFromMarcPlugin( $filepath, $format, + $encoding ); + } + + ( $batch_id, $num_valid, $num_items, @import_errors ) = BatchStageMarcRecords( + $record_type, $encoding, + $marcrecords, $filename, + $marc_modification_template, $comments, + '', $parse_items, + 0 + ); + + if ($matcher_id) { + my $matcher = C4::Matcher->fetch($matcher_id); + if ( defined $matcher ) { + $checked_matches = 1; + $matcher_code = $matcher->code(); + $num_with_matches = + BatchFindDuplicates( $batch_id, $matcher, 10); + SetImportBatchMatcher( $batch_id, $matcher_id ); + SetImportBatchOverlayAction( $batch_id, $overlay_action ); + SetImportBatchNoMatchAction( $batch_id, $nomatch_action ); + SetImportBatchItemAction( $batch_id, $item_action ); + $schema->storage->txn_commit; + } + else { + $matcher_failed = 1; + $schema->storage->txn_rollback; + } + } else { + $schema->storage->txn_commit; + } + + return $batch_id; + } + catch { + warn $_; + $schema->storage->txn_rollback; + die "Something terrible has happened!" + if ( $_ =~ /Rollback failed/ ); # TODO Check test: Rollback failed + }; +} + +=head3 _get_MarcItemFieldsToOrder_syspref_data + + my $marc_item_fields_to_order = _get_MarcItemFieldsToOrder_syspref_data('MarcItemFieldsToOrder', $marcrecord, $fields); + + Fetches data from a marc record based on the mappings in the syspref MarcItemFieldsToOrder using the fields selected in $fields (array). + +=cut + +sub _get_MarcItemFieldsToOrder_syspref_data { + my ($syspref_name, $record, $field_list) = @_; + my $syspref = C4::Context->preference($syspref_name); + $syspref = "$syspref\n\n"; + my $yaml = eval { + YAML::XS::Load(Encode::encode_utf8($syspref)); + }; + if ( $@ ) { + warn "Unable to parse $syspref syspref : $@"; + return (); + } + my @result; + my @tags_list; + + # Check tags in syspref definition + for my $field_name ( @$field_list ) { + next unless exists $yaml->{$field_name}; + my @fields = split /\|/, $yaml->{$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); + + die "System preference MarcItemFieldsToOrder has not been filled in. Please set the mapping values to use this cron script." if scalar(@tags_list == 0); + + 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; + } + + for (my $i = 0; $i < $tags_count->{count}; $i++) { + my $r; + for my $field_name ( @$field_list ) { + next unless exists $yaml->{$field_name}; + my @fields = split /\|/, $yaml->{$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 $yaml->{$field}; + } + } + push @result, $r; + } + return $result[0]; +} + +=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_biblios_from_import_record + + my ($record_results, $duplicates_in_batch) = add_biblios_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_biblios_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; + + if ( $biblionumber ) { + $import_record->status('imported')->store; + if( $overlay_action eq 'replace' ){ + my $biblio = Koha::Biblios->find( $biblionumber ); + $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}; + my @order_line_details; + + if($agent eq 'cron') { + my $marc_item_fields_to_order = _get_MarcItemFieldsToOrder_syspref_data('MarcItemFieldsToOrder', $marcrecord, ['homebranch', 'holdingbranch', 'itype', 'nonpublic_note', 'public_note', 'loc', 'ccode', 'notforloan', 'uri', 'copyno', 'price', 'replacementprice', 'itemcallnumber', 'quantity', 'budget_code']); + my $item_homebranch = $marc_item_fields_to_order->{homebranch}; + my $item_holdingbranch = $marc_item_fields_to_order->{holdingbranch}; + my $item_itype = $marc_item_fields_to_order->{itype}; + my $item_nonpublic_note = $marc_item_fields_to_order->{nonpublic_note}; + my $item_public_note = $marc_item_fields_to_order->{public_note}; + my $item_loc = $marc_item_fields_to_order->{loc}; + my $item_ccode = $marc_item_fields_to_order->{ccode}; + my $item_notforloan = $marc_item_fields_to_order->{notforloan}; + my $item_uri = $marc_item_fields_to_order->{uri}; + my $item_copyno = $marc_item_fields_to_order->{copyno}; + my $item_quantity = $marc_item_fields_to_order->{quantity}; + my $item_budget_code = $marc_item_fields_to_order->{budget_code}; + my $item_budget_id; + if ( $marc_item_fields_to_order->{budget_code} ) { + my $item_budget = GetBudgetByCode( $marc_item_fields_to_order->{budget_code} ); + if ( $item_budget ) { + $item_budget_id = $item_budget->{budget_id}; + } else { + $item_budget_id = $budget_id; + } + } else { + $item_budget_id = $budget_id; + } + my $item_price = $marc_item_fields_to_order->{price}; + my $item_replacement_price = $marc_item_fields_to_order->{replacementprice}; + my $item_callnumber = $marc_item_fields_to_order->{itemcallnumber}; + + if(!$item_quantity) { + my $isbn = $marcrecord->subfield( '020', "a" ); + warn "No quantity found for record with ISBN: $isbn. No items will be added."; + } + + for (my $i = 0; $i < $item_quantity; $i++) { + my $item = Koha::Item->new({ + biblionumber => $biblionumber, + homebranch => $item_homebranch, + holdingbranch => $item_holdingbranch, + itype => $item_itype, + itemnotes_nonpublic => $item_nonpublic_note, + itemnotes => $item_public_note, + location => $item_loc, + ccode => $item_ccode, + notforloan => $item_notforloan, + uri => $item_uri, + copynumber => $item_copyno, + price => $item_price, + replacementprice => $item_replacement_price, + itemcallnumber => $item_callnumber, + })->store; + + my %order_detail_hash = ( + biblionumber => $biblionumber, + itemnumbers => ($item->itemnumber), + basketno => $basket_id, + quantity => 1, + budget_id => $item_budget_id, + currency => $vendor->listprice, + ); + + if($item_price) { + $order_detail_hash{tax_rate_on_ordering} = $vendor->tax_rate; + $order_detail_hash{tax_rate_on_receiving} = $vendor->tax_rate; + $order_detail_hash{discount} = $vendor->discount; + $order_detail_hash{rrp} = $item_price; + $order_detail_hash{ecost} = $vendor->discount ? $item_price * ( 1 - $vendor->discount / 100 ) : $item_price; + $order_detail_hash{listprice} = $order_detail_hash{rrp} / $active_currency->rate; + $order_detail_hash{unitprice} = $order_detail_hash{ecost}; + } else { + $order_detail_hash{listprice} = 0; + } + $order_detail_hash{replacementprice} = $item_replacement_price || 0; + $order_detail_hash{uncertainprice} = 0 if $order_detail_hash{listprice}; + + push @order_line_details, \%order_detail_hash; + } + } + + if($agent eq 'client') { + my $homebranches = $client_item_fields->{homebranches}; + my $count = scalar @$homebranches; + my $holdingbranches = $client_item_fields->{holdingbranches}; + my $itypes = $client_item_fields->{itypes}; + my $nonpublic_notes = $client_item_fields->{nonpublic_notes}; + my $public_notes = $client_item_fields->{public_notes}; + my $locs = $client_item_fields->{locs}; + my $ccodes = $client_item_fields->{ccodes}; + my $notforloans = $client_item_fields->{notforloans}; + my $uris = $client_item_fields->{uris}; + my $copynos = $client_item_fields->{copynos}; + my $budget_codes = $client_item_fields->{budget_codes}; + my $itemprices = $client_item_fields->{itemprices}; + my $replacementprices = $client_item_fields->{replacementprices}; + my $itemcallnumbers = $client_item_fields->{itemcallnumbers}; + + my $itemcreation; + for (my $i = 0; $i < $count; $i++) { + $itemcreation = 1; + my $item = Koha::Item->new( + { + biblionumber => $biblionumber, + homebranch => @$homebranches[$i], + holdingbranch => @$holdingbranches[$i], + itemnotes_nonpublic => @$nonpublic_notes[$i], + itemnotes => @$public_notes[$i], + location => @$locs[$i], + ccode => @$ccodes[$i], + itype => @$itypes[$i], + notforloan => @$notforloans[$i], + uri => @$uris[$i], + copynumber => @$copynos[$i], + price => @$itemprices[$i], + replacementprice => @$replacementprices[$i], + itemcallnumber => @$itemcallnumbers[$i], + } + )->store; + + my %order_detail_hash = ( + biblionumber => $biblionumber, + itemnumbers => ($item->itemnumber), + basketno => $basket_id, + quantity => 1, + budget_id => @$budget_codes[$i] || $budget_id, # If no budget selected in the UI, default to the budget on the ordering account + currency => $vendor->listprice, + ); + + if(@$itemprices[$i]) { + $order_detail_hash{tax_rate_on_ordering} = $vendor->tax_rate; + $order_detail_hash{tax_rate_on_receiving} = $vendor->tax_rate; + my $order_discount = $client_item_fields->{c_discount} ? $client_item_fields->{c_discount} : $vendor->discount; + $order_detail_hash{discount} = $order_discount; + $order_detail_hash{rrp} = @$itemprices[$i]; + $order_detail_hash{ecost} = $order_discount ? @$itemprices[$i] * ( 1 - $order_discount / 100 ) : @$itemprices[$i]; + $order_detail_hash{listprice} = $order_detail_hash{rrp} / $active_currency->rate; + $order_detail_hash{unitprice} = $order_detail_hash{ecost}; + } else { + $order_detail_hash{listprice} = 0; + } + $order_detail_hash{replacementprice} = @$replacementprices[$i] || 0; + $order_detail_hash{uncertainprice} = 0 if $order_detail_hash{listprice}; + + push @order_line_details, \%order_detail_hash; + } + + if(!$itemcreation) { + my $quantity = GetMarcQuantity($marcrecord, C4::Context->preference('marcflavour')) || 1; + my %order_detail_hash = ( + biblionumber => $biblionumber, + basketno => $basket_id, + quantity => $client_item_fields->{c_quantity}, + budget_id => $client_item_fields->{c_budget_id}, + uncertainprice => 1, + sort1 => $client_item_fields->{c_sort1}, + sort2 => $client_item_fields->{c_sort2}, + order_internalnote => $client_item_fields->{all_order_internalnote}, + order_vendornote => $client_item_fields->{all_order_vendornote}, + currency => $client_item_fields->{all_currency}, + replacementprice => $client_item_fields->{c_replacement_price}, + ); + if ($client_item_fields->{c_price}){ + $order_detail_hash{tax_rate_on_ordering} = $vendor->tax_rate; + $order_detail_hash{tax_rate_on_receiving} = $vendor->tax_rate; + my $order_discount = $client_item_fields->{c_discount} ? $client_item_fields->{c_discount} : $vendor->discount; + $order_detail_hash{discount} = $order_discount; + $order_detail_hash{rrp} = $client_item_fields->{c_price}; + $order_detail_hash{ecost} = $order_discount ? $client_item_fields->{c_price} * ( 1 - $order_discount / 100 ) : $client_item_fields->{c_price}; + $order_detail_hash{listprice} = $order_detail_hash{rrp} / $active_currency->rate; + $order_detail_hash{unitprice} = $order_detail_hash{ecost}; + } else { + $order_detail_hash{listprice} = 0; + } + + $order_detail_hash{uncertainprice} = 0 if $order_detail_hash{listprice}; + + # Add items if applicable parsing the item sent by the form, and create an item just for the import_record_id we are dealing with + my $basket = Koha::Acquisition::Baskets->find( $basket_id ); + $order_detail_hash{itemnumbers} = (); + if ( $basket->effective_create_items eq 'ordering' && !$basket->is_standing ) { + my @tags = $client_item_fields->{tag}; + my @subfields = $client_item_fields->{subfield}; + my @field_values = $client_item_fields->{field_value}; + my @serials = $client_item_fields->{serial}; + my $xml = TransformHtmlToXml( \@tags, \@subfields, \@field_values ); + my $record = MARC::Record::new_from_xml( $xml, 'UTF-8' ); + for ( my $qtyloop=1; $qtyloop <= $client_item_fields->{c_quantity}; $qtyloop++ ) { + my ( $biblionumber, undef, $itemnumber ) = AddItemFromMarc( $record, $biblionumber ); + push @{ $order_detail_hash{itemnumbers} }, $itemnumber; + } + } + push @order_line_details, \%order_detail_hash; + } + } + return \@order_line_details; +} + +=head3 create_order_lines + + my $order_lines = create_order_lines({ + order_line_details => $order_line_details + }); + + Creates order lines based on an array of order line details + +=cut + +sub create_order_lines { + my ( $args ) = @_; + + my $order_line_details = $args->{order_line_details}; + + foreach my $order_detail ( @{ $order_line_details } ) { + my @itemnumbers = $order_detail->{itemnumbers}; + delete($order_detail->{itemnumber}); + my $order = Koha::Acquisition::Order->new( \%{ $order_detail } ); + $order->populate_with_prices_for_ordering(); + $order->populate_with_prices_for_receiving(); + $order->store; + foreach my $itemnumber ( @itemnumbers ) { + $order->add_item( $itemnumber ); + } + } + return; +} + +1; \ No newline at end of file diff --git a/acqui/addorderiso2709.pl b/acqui/addorderiso2709.pl index fb31a8bcee..f62916ff3b 100755 --- a/acqui/addorderiso2709.pl +++ b/acqui/addorderiso2709.pl @@ -54,6 +54,7 @@ use Koha::Acquisition::Booksellers; use Koha::ImportBatches; use Koha::Import::Records; use Koha::Patrons; +use Koha::MarcOrder; my $input = CGI->new; my ($template, $loggedinuser, $cookie, $userflags) = get_template_and_user({ @@ -153,218 +154,63 @@ if ($op eq ""){ my @sort2 = $input->multi_param('sort2'); my $matcher_id = $input->param('matcher_id'); my $active_currency = Koha::Acquisition::Currencies->get_active; - my $biblio_count = 0; while( my $import_record = $import_records->next ){ - $biblio_count++; - my $duplifound = 0; - # Check if this import_record_id was selected - next 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 $c_quantity = shift( @quantities ) || GetMarcQuantity($marcrecord, C4::Context->preference('marcflavour') ) || 1; - my $c_budget_id = shift( @budgets_id ) || $input->param('all_budget_id') || $budget_id; - my $c_discount = shift ( @discount); - my $c_sort1 = shift( @sort1 ) || $input->param('all_sort1') || ''; - my $c_sort2 = shift( @sort2 ) || $input->param('all_sort2') || ''; - my $c_replacement_price = shift( @orderreplacementprices ); - my $c_price = shift( @prices ) || GetMarcPrice($marcrecord, C4::Context->preference('marcflavour')); - - # Insert the biblio, or find it through matcher - if ( $biblionumber ) { # If matched during staging we can continue - $import_record->status('imported')->store; - if( $overlay_action eq 'replace' ){ - my $biblio = Koha::Biblios->find( $biblionumber ); - $import_record->replace({ biblio => $biblio }); - } - } else { # Otherwise we check for duplicates, and skip if they exist - if ($matcher_id) { - if ( $matcher_id eq '_TITLE_AUTHOR_' ) { - $duplifound = 1 if FindDuplicate($marcrecord); - } - else { - my $matcher = C4::Matcher->fetch($matcher_id); - my @matches = $matcher->get_matches( $marcrecord, my $max_matches = 1 ); - $duplifound = 1 if @matches; - } - - $duplinbatch = $import_batch_id and next if $duplifound; - } - - # remove hyphens (-) from ISBN - # FIXME: This should probably be optional - my ( $isbnfield, $isbnsubfield ) = GetMarcFromKohaField( 'biblioitems.isbn' ); - if ( $marcrecord->field($isbnfield) ) { - foreach my $field ( $marcrecord->field($isbnfield) ) { - foreach my $subfield ( $field->subfield($isbnsubfield) ) { - my $newisbn = $field->subfield($isbnsubfield); - $newisbn =~ s/-//g; - $field->update( $isbnsubfield => $newisbn ); - } - } - } - - # add the biblio - ( $biblionumber, undef ) = AddBiblio( $marcrecord, $cgiparams->{'frameworkcode'} || '' ); - $import_record->status('imported')->store; - } - - $import_record->import_biblio->matched_biblionumber($biblionumber)->store; - - # Add items from MarcItemFieldsToOrder - my @homebranches = $input->multi_param('homebranch_' . $import_record->import_record_id); - my $count = scalar @homebranches; - my @holdingbranches = $input->multi_param('holdingbranch_' . $import_record->import_record_id); - my @itypes = $input->multi_param('itype_' . $import_record->import_record_id); - my @nonpublic_notes = $input->multi_param('nonpublic_note_' . $import_record->import_record_id); - my @public_notes = $input->multi_param('public_note_' . $import_record->import_record_id); - my @locs = $input->multi_param('loc_' . $import_record->import_record_id); - my @ccodes = $input->multi_param('ccode_' . $import_record->import_record_id); - my @notforloans = $input->multi_param('notforloan_' . $import_record->import_record_id); - my @uris = $input->multi_param('uri_' . $import_record->import_record_id); - my @copynos = $input->multi_param('copyno_' . $import_record->import_record_id); - my @budget_codes = $input->multi_param('budget_code_' . $import_record->import_record_id); - my @itemprices = $input->multi_param('itemprice_' . $import_record->import_record_id); + my $marcrecord = $import_record->get_marc_record || die "couldn't translate marc information"; + my @homebranches = $input->multi_param('homebranch_' . $import_record->import_record_id); + my @holdingbranches = $input->multi_param('holdingbranch_' . $import_record->import_record_id); + my @itypes = $input->multi_param('itype_' . $import_record->import_record_id); + my @nonpublic_notes = $input->multi_param('nonpublic_note_' . $import_record->import_record_id); + my @public_notes = $input->multi_param('public_note_' . $import_record->import_record_id); + my @locs = $input->multi_param('loc_' . $import_record->import_record_id); + my @ccodes = $input->multi_param('ccode_' . $import_record->import_record_id); + my @notforloans = $input->multi_param('notforloan_' . $import_record->import_record_id); + my @uris = $input->multi_param('uri_' . $import_record->import_record_id); + my @copynos = $input->multi_param('copyno_' . $import_record->import_record_id); + my @budget_codes = $input->multi_param('budget_code_' . $import_record->import_record_id); + my @itemprices = $input->multi_param('itemprice_' . $import_record->import_record_id); my @replacementprices = $input->multi_param('replacementprice_' . $import_record->import_record_id); - my @itemcallnumbers = $input->multi_param('itemcallnumber_' . $import_record->import_record_id); - my $itemcreation = 0; - - my @itemnumbers; - for (my $i = 0; $i < $count; $i++) { - $itemcreation = 1; - my $item = Koha::Item->new( - { - biblionumber => $biblionumber, - homebranch => $homebranches[$i], - holdingbranch => $holdingbranches[$i], - itemnotes_nonpublic => $nonpublic_notes[$i], - itemnotes => $public_notes[$i], - location => $locs[$i], - ccode => $ccodes[$i], - itype => $itypes[$i], - notforloan => $notforloans[$i], - uri => $uris[$i], - copynumber => $copynos[$i], - price => $itemprices[$i], - replacementprice => $replacementprices[$i], - itemcallnumber => $itemcallnumbers[$i], - } - )->store; - push( @itemnumbers, $item->itemnumber ); - } - if ($itemcreation == 1) { - # Group orderlines from MarcItemFieldsToOrder - my $budget_hash; - for (my $i = 0; $i < $count; $i++) { - $budget_hash->{$budget_codes[$i]}->{quantity} += 1; - $budget_hash->{$budget_codes[$i]}->{price} = $itemprices[$i]; - $budget_hash->{$budget_codes[$i]}->{replacementprice} = $replacementprices[$i]; - $budget_hash->{$budget_codes[$i]}->{itemnumbers} //= []; - push @{ $budget_hash->{$budget_codes[$i]}->{itemnumbers} }, $itemnumbers[$i]; - } - - # Create orderlines from MarcItemFieldsToOrder - while(my ($budget_id, $infos) = each %$budget_hash) { - if ($budget_id) { - my %orderinfo = ( - biblionumber => $biblionumber, - basketno => $cgiparams->{'basketno'}, - quantity => $infos->{quantity}, - budget_id => $budget_id, - currency => $cgiparams->{'all_currency'}, - ); - - my $price = $infos->{price}; - if ($price){ - # in France, the cents separator is the , but sometimes, ppl use a . - # in this case, the price will be x100 when unformatted ! Replace the . by a , to get a proper price calculation - $price =~ s/\./,/ if C4::Context->preference("CurrencyFormat") eq "FR"; - $price = Koha::Number::Price->new($price)->unformat; - $orderinfo{tax_rate_on_ordering} = $bookseller->tax_rate; - $orderinfo{tax_rate_on_receiving} = $bookseller->tax_rate; - my $order_discount = $c_discount ? $c_discount : $bookseller->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}; - } 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}; - - 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 { - # 3rd add order - my $patron = Koha::Patrons->find( $loggedinuser ); - # get quantity in the MARC record (1 if none) - my $quantity = GetMarcQuantity($marcrecord, C4::Context->preference('marcflavour')) || 1; - my %orderinfo = ( - biblionumber => $biblionumber, - basketno => $cgiparams->{'basketno'}, - quantity => $c_quantity, - branchcode => $patron->branchcode, - budget_id => $c_budget_id, - uncertainprice => 1, - sort1 => $c_sort1, - sort2 => $c_sort2, - order_internalnote => $cgiparams->{'all_order_internalnote'}, - order_vendornote => $cgiparams->{'all_order_vendornote'}, - currency => $cgiparams->{'all_currency'}, - replacementprice => $c_replacement_price, - ); - # get the price if there is one. - if ($c_price){ - # in France, the cents separator is the , but sometimes, ppl use a . - # in this case, the price will be x100 when unformatted ! Replace the . by a , to get a proper price calculation - $c_price =~ s/\./,/ if C4::Context->preference("CurrencyFormat") eq "FR"; - $c_price = Koha::Number::Price->new($c_price)->unformat; - $orderinfo{tax_rate_on_ordering} = $bookseller->tax_rate; - $orderinfo{tax_rate_on_receiving} = $bookseller->tax_rate; - my $order_discount = $c_discount ? $c_discount : $bookseller->discount; - $orderinfo{discount} = $order_discount; - $orderinfo{rrp} = $c_price; - $orderinfo{ecost} = $order_discount ? $c_price * ( 1 - $order_discount / 100 ) : $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}; - - my $order = Koha::Acquisition::Order->new( \%orderinfo ); - $order->populate_with_prices_for_ordering(); - $order->populate_with_prices_for_receiving(); - $order->store; - - # 4th, add items if applicable - # parse the item sent by the form, and create an item just for the import_record_id we are dealing with - # this is not optimised, but it's working ! - if ( $basket->effective_create_items eq 'ordering' && !$basket->is_standing ) { - my @tags = $input->multi_param('tag'); - my @subfields = $input->multi_param('subfield'); - my @field_values = $input->multi_param('field_value'); - my @serials = $input->multi_param('serial'); - my $xml = TransformHtmlToXml( \@tags, \@subfields, \@field_values ); - my $record = MARC::Record::new_from_xml( $xml, 'UTF-8' ); - for (my $qtyloop=1;$qtyloop <= $c_quantity;$qtyloop++) { - my ( $biblionumber, undef, $itemnumber ) = AddItemFromMarc( $record, $biblionumber ); - $order->add_item( $itemnumber ); - } - } - } + my @itemcallnumbers = $input->multi_param('itemcallnumber_' . $import_record->import_record_id); + + my $client_item_fields = { + 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, + c_quantity => shift( @quantities ) || GetMarcQuantity($marcrecord, C4::Context->preference('marcflavour') ) || 1, + c_budget_id => shift( @budgets_id ) || $input->param('all_budget_id') || $budget_id, + c_discount => shift ( @discount), + c_sort1 => shift( @sort1 ) || $input->param('all_sort1') || '', + c_sort2 => shift( @sort2 ) || $input->param('all_sort2') || '', + c_replacement_price => shift( @orderreplacementprices ), + c_price => shift( @prices ) || GetMarcPrice($marcrecord, C4::Context->preference('marcflavour')), + }; + + my $args = { + import_batch_id => $import_batch_id, + import_record => $import_record, + matcher_id => $matcher_id, + overlay_action => $overlay_action, + agent => 'client', + import_record_id_selected => @import_record_id_selected, + client_item_fields => $client_item_fields, + basket_id => $cgiparams->{'basketno'}, + vendor => $bookseller, + budget_id => $budget_id, + }; + my $result = Koha::MarcOrder->import_record_and_create_order_lines($args); + + $duplinbatch = $result->{duplicates_in_batch} if $result->{duplicates_in_batch}; + next if $result->{skip}; # If a duplicate is found, or the import record wasn't selected it will be skipped $imported++; } diff --git a/misc/cronjobs/marc_ordering_process.pl b/misc/cronjobs/marc_ordering_process.pl new file mode 100644 index 0000000000..cf0b731231 --- /dev/null +++ b/misc/cronjobs/marc_ordering_process.pl @@ -0,0 +1,129 @@ + +#!/usr/bin/perl + +# This file is part of Koha. +# +# Copyright (C) 2023 PTFS Europe Ltd +# +# 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 . + +=head1 NAME + +marc_ordering_process.pl - cron script to retrieve marc files and create order lines + +=head1 SYNOPSIS + +./marc_ordering_process.pl [-c|--confirm] [-v|--verbose] + +or, in crontab: +# Once every day +0 3 * * * marc_ordering_process.pl -c + +=head1 DESCRIPTION + +This script searches for new marc files in an SFTP location +If there are new files, it stages those files, adds bilbios/items and creates order lines + +=head1 OPTIONS + +=over + +=item B<-v|--verbose> + +Print report to standard out. + +=item B<-c|--confirm> + +Without this parameter no changes will be made + +=back + +=cut + +use Modern::Perl; +use Pod::Usage qw( pod2usage ); +use Getopt::Long qw( GetOptions ); +use File::Copy qw( copy move ); + +use Koha::Script -cron; +use Koha::MarcOrder; +use Koha::MarcOrderAccounts; + +use C4::Log qw( cronlogaction ); + +my $command_line_options = join(" ",@ARGV); + +my ( $help, $verbose, $confirm ); +GetOptions( + 'h|help' => \$help, + 'v|verbose' => \$verbose, + 'c|confirm' => \$confirm, +) || pod2usage(1); + +pod2usage(0) if $help; + +cronlogaction({ info => $command_line_options }); + +$verbose = 1 unless $verbose or $confirm; +print "Test run only\n" unless $confirm; + +print "Fetching marc ordering accounts\n" if $verbose; +my @accounts = Koha::MarcOrderAccounts->search( + {}, + { + join => ['vendor', 'budget'] + } +)->as_list; + +if(scalar(@accounts) == 0) { + print "No accounts found - you must create a Marc order account for this cronjob to run\n" if $verbose; +} + +foreach my $acct ( @accounts ) { + if($verbose) { + say sprintf "Starting marc ordering process for %s", $acct->vendor->name; + say sprintf "Looking for new files in %s", $acct->download_directory; + } + + my $working_dir = $acct->download_directory; + opendir my $dir, $working_dir or die "Can't open filepath"; + my @files = grep { /\.(mrc|marcxml|mrk)/i } readdir $dir; + closedir $dir; + + foreach my $filename ( @files ) { + say sprintf "Creating order lines from file %s", $filename if $verbose; + if($confirm) { + my $full_path = "$working_dir/$filename"; + my $args = { + filename => $filename, + filepath => $full_path, + profile => $acct, + agent => 'cron' + }; + my $result = Koha::MarcOrder->create_order_lines_from_file($args); + if($result->{success}) { + say sprintf "Successfully processed file: %s", $filename if $verbose; + unlink $full_path; + } else { + say sprintf "Error processing file: %s", $filename if $verbose; + say sprintf "Error message: %s", $result->{error} if $verbose; + }; + } + } + print "All files completed\n"; + print "Moving to next account\n\n"; +} +print "Process complete\n"; +cronlogaction({ action => 'End', info => "COMPLETED" }); + -- 2.37.1 (Apple Git-137.1)