diff --git a/Koha/MarcOrder.pm b/Koha/MarcOrder.pm new file mode 100644 index 00000000000..5f5dc5e1ca3 --- /dev/null +++ b/Koha/MarcOrder.pm @@ -0,0 +1,911 @@ +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 + GetMarcQuantity +); +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 $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, + }); + + my $basket_id = _create_basket_for_file( + { + vendor_id => $vendor_id, + import_records => $import_records + } + ); + + 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}, + vendor => $vendor_record, + basket_id => $basket_id, + 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({ + import_records => $import_records, + 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 $vendor_id = $args->{vendor_id}; + my @import_records = $args->{import_records}->as_list; + my $marcrecord = $import_records[0]->get_marc_record; + my $marc_fields_to_order = _get_MarcFieldsToOrder_syspref_data( + 'MarcFieldsToOrder', $marcrecord, + [ 'sort1' ] + ); + my $filename = $marc_fields_to_order->{sort1}; + + # 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_MarcFieldsToOrder_syspref_data + + my $marc_fields_to_order = _get_MarcFieldsToOrder_syspref_data('MarcFieldsToOrder', $marcrecord, $fields); + + Fetches data from a marc record based on the mappings in the syspref MarcFieldsToOrder using the fields selected in $fields (array). + +=cut + +sub _get_MarcFieldsToOrder_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 $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; + if ( my $v = $record->subfield( $f, $sf ) ) { + $r->{$field_name} = $v; + } + last if $yaml->{$field}; + } + } + return $r; +} + +=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); + + 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 ( @$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 $budget_id = $args->{budget_id}; + my $basket_id = $args->{basket_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_fields_to_order = _get_MarcFieldsToOrder_syspref_data( + 'MarcFieldsToOrder', $marcrecord, + [ 'price', 'quantity', 'budget_code', 'discount', 'sort1', 'sort2' ] + ); + my $quantity = $marc_fields_to_order->{quantity}; + my $budget_code = $marc_fields_to_order->{budget_code} || $budget_id; # Use fallback from ordering profile if not mapped + my $price = $marc_fields_to_order->{price}; + my $discount = $marc_fields_to_order->{discount}; + my $sort1 = $marc_fields_to_order->{sort1}; + my $sort2 = $marc_fields_to_order->{sort2}; + my $mapped_budget; + if($budget_code) { + my $biblio_budget = GetBudgetByCode($budget_code); + if($biblio_budget) { + $mapped_budget = $biblio_budget->{budget_id}; + } else { + $mapped_budget = $budget_id; + } + } + + 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} || 0; + 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}; + my $itemcreation = 0; + + for (my $i = 0; $i < $item_quantity; $i++) { + $itemcreation = 1; + 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, + basketno => $basket_id, + itemnumbers => ($item->itemnumber), + 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(!$itemcreation) { + my %order_detail_hash = ( + biblionumber => $biblionumber, + basketno => $basket_id, + quantity => $quantity, + budget_id => $mapped_budget, + uncertainprice => 1, + sort1 => $sort1, + sort2 => $sort2, + ); + + if ($price){ + $order_detail_hash{tax_rate_on_ordering} = $vendor->tax_rate; + $order_detail_hash{tax_rate_on_receiving} = $vendor->tax_rate; + my $order_discount = $discount ? $discount : $vendor->discount; + $order_detail_hash{discount} = $order_discount; + $order_detail_hash{rrp} = $price; + $order_detail_hash{ecost} = $order_discount ? $price * ( 1 - $order_discount / 100 ) : $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}; + Koha::Plugins->call( + 'before_orderline_create', + { + marcrecord => $marcrecord, + orderline => \%order_detail_hash, + marcfields => $marc_fields_to_order + } + ); + 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; +} + + +=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 + +=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; +} + +1; \ No newline at end of file diff --git a/Koha/MarcOrderAccount.pm b/Koha/MarcOrderAccount.pm new file mode 100644 index 00000000000..9b8c8fa3598 --- /dev/null +++ b/Koha/MarcOrderAccount.pm @@ -0,0 +1,63 @@ +package Koha::MarcOrderAccount; + +# 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 Koha::Database; + +use base qw(Koha::Object); + +=head1 NAME + +Koha::MarcOrderAccount - Koha Marc Ordering Account Object class + +=head1 API + +=head2 Class Methods + +=cut + +=head3 vendor + +=cut + +sub vendor { + my ( $self ) = @_; + my $vendor_rs = $self->_result->vendor; + return unless $vendor_rs; + return Koha::Acquisition::Bookseller->_new_from_dbic($vendor_rs); +} + +=head3 budget + +=cut + +sub budget { + my ( $self ) = @_; + my $budget_rs = $self->_result->budget; + return Koha::Acquisition::Fund->_new_from_dbic( $budget_rs ); +} + +=head3 _type + +=cut + +sub _type { + return 'MarcOrderAccount'; +} + +1; \ No newline at end of file diff --git a/Koha/MarcOrderAccounts.pm b/Koha/MarcOrderAccounts.pm new file mode 100644 index 00000000000..f6ee407d658 --- /dev/null +++ b/Koha/MarcOrderAccounts.pm @@ -0,0 +1,51 @@ +package Koha::MarcOrderAccounts; + +# 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 Koha::Database; +use Koha::MarcOrderAccount; + +use base qw(Koha::Objects); + +=head1 NAME + +Koha::MarcOrderAccount - Koha Marc Ordering Account Object class + +=head1 API + +=head2 Class Methods + +=cut + +=head3 type + +=cut + +sub _type { + return 'MarcOrderAccount'; +} + +=head3 object_class + +=cut + +sub object_class { + return 'Koha::MarcOrderAccount'; +} + +1; diff --git a/Koha/Schema/Result/Aqbookseller.pm b/Koha/Schema/Result/Aqbookseller.pm index f9ce48cae47..8e49ddef7a6 100644 --- a/Koha/Schema/Result/Aqbookseller.pm +++ b/Koha/Schema/Result/Aqbookseller.pm @@ -494,6 +494,21 @@ __PACKAGE__->belongs_to( }, ); +=head2 marc_order_accounts + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "marc_order_accounts", + "Koha::Schema::Result::MarcOrderAccount", + { "foreign.vendor_id" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + =head2 vendor_edi_accounts Type: has_many @@ -510,8 +525,8 @@ __PACKAGE__->has_many( ); -# Created by DBIx::Class::Schema::Loader v0.07049 @ 2023-06-30 09:54:35 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:xjeOqpcdN3Kb1wmLGDjzLg +# Created by DBIx::Class::Schema::Loader v0.07049 @ 2023-07-12 16:43:09 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:4L14+X0NSAgyWGrKaFPR5w __PACKAGE__->add_columns( '+active' => { is_boolean => 1 }, diff --git a/Koha/Schema/Result/Aqbudget.pm b/Koha/Schema/Result/Aqbudget.pm index 182a7e39c58..d91cb19ec80 100644 --- a/Koha/Schema/Result/Aqbudget.pm +++ b/Koha/Schema/Result/Aqbudget.pm @@ -308,6 +308,21 @@ __PACKAGE__->belongs_to( }, ); +=head2 marc_order_accounts + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "marc_order_accounts", + "Koha::Schema::Result::MarcOrderAccount", + { "foreign.budget_id" => "self.budget_id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + =head2 suggestions Type: has_many @@ -349,8 +364,8 @@ Composing rels: L -> borrowernumber __PACKAGE__->many_to_many("borrowernumbers", "aqbudgetborrowers", "borrowernumber"); -# Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-01-21 13:39:29 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:sl+TGQXY85UWwS+Ld/vvyQ +# Created by DBIx::Class::Schema::Loader v0.07049 @ 2023-07-12 16:51:53 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:pCgGAZOv1TPHe6LIQhb1BQ __PACKAGE__->belongs_to( "budget", diff --git a/Koha/Schema/Result/MarcOrderAccount.pm b/Koha/Schema/Result/MarcOrderAccount.pm new file mode 100644 index 00000000000..be720da960e --- /dev/null +++ b/Koha/Schema/Result/MarcOrderAccount.pm @@ -0,0 +1,228 @@ +use utf8; +package Koha::Schema::Result::MarcOrderAccount; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Koha::Schema::Result::MarcOrderAccount + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("marc_order_accounts"); + +=head1 ACCESSORS + +=head2 id + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +unique identifier and primary key + +=head2 description + + data_type: 'varchar' + is_nullable: 0 + size: 250 + +description of this account + +=head2 vendor_id + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 1 + +vendor id for this account + +=head2 budget_id + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 1 + +budget id for this account + +=head2 download_directory + + data_type: 'mediumtext' + is_nullable: 1 + +download directory for this account + +=head2 matcher_id + + data_type: 'integer' + is_nullable: 1 + +the id of the match rule used (matchpoints.matcher_id) + +=head2 overlay_action + + data_type: 'varchar' + is_nullable: 1 + size: 50 + +how to handle duplicate records + +=head2 nomatch_action + + data_type: 'varchar' + is_nullable: 1 + size: 50 + +how to handle records where no match is found + +=head2 item_action + + data_type: 'varchar' + is_nullable: 1 + size: 50 + +what to do with item records + +=head2 parse_items + + data_type: 'tinyint' + is_nullable: 1 + +should items be parsed + +=head2 record_type + + data_type: 'varchar' + is_nullable: 1 + size: 50 + +type of record in the file + +=head2 encoding + + data_type: 'varchar' + is_nullable: 1 + size: 50 + +file encoding + +=head2 match_field + + data_type: 'varchar' + is_nullable: 1 + size: 10 + +the field that a vendor account has been mapped to in a marc record + +=head2 match_value + + data_type: 'varchar' + is_nullable: 1 + size: 50 + +the value to be matched against the marc record + +=cut + +__PACKAGE__->add_columns( + "id", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "description", + { data_type => "varchar", is_nullable => 0, size => 250 }, + "vendor_id", + { data_type => "integer", is_foreign_key => 1, is_nullable => 1 }, + "budget_id", + { data_type => "integer", is_foreign_key => 1, is_nullable => 1 }, + "download_directory", + { data_type => "mediumtext", is_nullable => 1 }, + "matcher_id", + { data_type => "integer", is_nullable => 1 }, + "overlay_action", + { data_type => "varchar", is_nullable => 1, size => 50 }, + "nomatch_action", + { data_type => "varchar", is_nullable => 1, size => 50 }, + "item_action", + { data_type => "varchar", is_nullable => 1, size => 50 }, + "parse_items", + { data_type => "tinyint", is_nullable => 1 }, + "record_type", + { data_type => "varchar", is_nullable => 1, size => 50 }, + "encoding", + { data_type => "varchar", is_nullable => 1, size => 50 }, + "match_field", + { data_type => "varchar", is_nullable => 1, size => 10 }, + "match_value", + { data_type => "varchar", is_nullable => 1, size => 50 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("id"); + +=head1 RELATIONS + +=head2 budget + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "budget", + "Koha::Schema::Result::Aqbudget", + { budget_id => "budget_id" }, + { + is_deferrable => 1, + join_type => "LEFT", + on_delete => "CASCADE", + on_update => "CASCADE", + }, +); + +=head2 vendor + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "vendor", + "Koha::Schema::Result::Aqbookseller", + { id => "vendor_id" }, + { + is_deferrable => 1, + join_type => "LEFT", + on_delete => "CASCADE", + on_update => "CASCADE", + }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07049 @ 2023-09-07 08:55:26 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:6pN1TLuqkf9rQaeQ7Wf26A + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/acqui/addorderiso2709.pl b/acqui/addorderiso2709.pl index fb31a8bcee4..7f4fbb51216 100755 --- a/acqui/addorderiso2709.pl +++ b/acqui/addorderiso2709.pl @@ -54,6 +54,7 @@ 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 @@ 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/admin/marc_order_accounts.pl b/admin/marc_order_accounts.pl new file mode 100644 index 00000000000..8365e56ff2f --- /dev/null +++ b/admin/marc_order_accounts.pl @@ -0,0 +1,134 @@ +#!/usr/bin/perl + +# A script that allows the user to create an account and profile for auto-creating orders from imported marc files +# The script displays account details and allows account creation/editing in the first instance +# If the "run" operation is passed then the script will run the process of creating orders + +# 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 CGI qw ( -utf8 ); + +use C4::Context; +use C4::Auth qw( get_template_and_user ); +use C4::Budgets qw( GetBudgets ); +use C4::Output qw( output_html_with_http_headers ); +use C4::Matcher; + +use Koha::UploadedFiles; +use Koha::ImportBatchProfiles; +use Koha::MarcOrder; +use Koha::Acquisition::Booksellers; +use Koha::MarcOrderAccount; +use Koha::MarcOrderAccounts; + +my $input = CGI->new; + +my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "admin/marc_order_accounts.tt", + query => $input, + type => "intranet", + } +); + +my $crypt = Koha::Encryption->new; + +my $op = $input->param('op'); +$op ||= 'display'; + +if( $op eq 'acct_form') { + $template->param( acct_form => 1 ); + my @vendors = Koha::Acquisition::Booksellers->search( + undef, + { + columns => [ 'name', 'id' ], + order_by => { -asc => 'name' } + } + )->as_list; + my $budgets = GetBudgets(); + $template->param( + vendors => \@vendors, + budgets => $budgets + ); + my @matchers = C4::Matcher::GetMatcherList(); + $template->param( available_matchers => \@matchers ); + + show_account($input, $template); +} elsif ( $op eq 'delete_acct' ) { + show_account($input, $template); + $template->param( delete_acct => 1); +} else { + if( $op eq 'save' ) { + + my $fields = { + id => scalar $input->param('id'), + description => scalar $input->param('description'), + vendor_id => scalar $input->param('vendor_id'), + budget_id => scalar $input->param('budget_id'), + download_directory => scalar $input->param('download_directory'), + matcher_id => scalar $input->param('matcher'), + overlay_action => scalar $input->param('overlay_action'), + nomatch_action => scalar $input->param('nomatch_action'), + parse_items => scalar $input->param('parse_items'), + item_action => scalar $input->param('item_action'), + record_type => scalar $input->param('record_type'), + encoding => scalar $input->param('encoding') || 'UTF-8', + match_field => scalar $input->param('match_field'), + match_value => scalar $input->param('match_value'), + }; + + if(scalar $input->param('id')) { + # Update existing account + my $account = Koha::MarcOrderAccounts->find(scalar $input->param('id')); + $account->update($fields); + } else { + # Add new account + my $new_account = Koha::MarcOrderAccount->new($fields); + $new_account->store; + } + } elsif ($op eq 'delete_confirmed') { + my $acct_id = $input->param('id'); + my $acct = Koha::MarcOrderAccounts->find($acct_id); + $acct->delete; + } + + $template->param( display => 1 ); + my @accounts = Koha::MarcOrderAccounts->search( + {}, + { + join => ['vendor', 'budget'] + } + )->as_list; + $template->param( accounts => \@accounts ); + +} + +output_html_with_http_headers $input, $cookie, $template->output; + +sub show_account { + my ($input, $template) = @_; + my $acct_id = $input->param('id'); + if ($acct_id) { + my $acct = Koha::MarcOrderAccounts->find($acct_id); + if ($acct) { + $template->param( account => $acct ); + } + } + return; +} \ No newline at end of file diff --git a/installer/data/mysql/atomicupdate/bug_34355-add_marc_order_accounts.pl b/installer/data/mysql/atomicupdate/bug_34355-add_marc_order_accounts.pl new file mode 100644 index 00000000000..149afbeee96 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_34355-add_marc_order_accounts.pl @@ -0,0 +1,47 @@ +use Modern::Perl; + +return { + bug_number => "34355", + description => "Add a table to allow creation of MARC order accounts and a syspref to activate it.", + up => sub { + my ($args) = @_; + my ( $dbh, $out ) = @$args{qw(dbh out)}; + + unless ( TableExists('marc_order_accounts') ) { + $dbh->do( + q{ + CREATE TABLE `marc_order_accounts` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'unique identifier and primary key', + `description` varchar(250) NOT NULL COMMENT 'description of this account', + `vendor_id` int(11) DEFAULT NULL COMMENT 'vendor id for this account', + `budget_id` int(11) DEFAULT NULL COMMENT 'budget id for this account', + `download_directory` mediumtext DEFAULT NULL COMMENT 'download directory for this account', + `matcher_id` int(11) DEFAULT NULL COMMENT 'the id of the match rule used (matchpoints.matcher_id)', + `overlay_action` varchar(50) DEFAULT NULL COMMENT 'how to handle duplicate records', + `nomatch_action` varchar(50) DEFAULT NULL COMMENT 'how to handle records where no match is found', + `item_action` varchar(50) DEFAULT NULL COMMENT 'what to do with item records', + `parse_items` tinyint(1) DEFAULT NULL COMMENT 'should items be parsed', + `record_type` varchar(50) DEFAULT NULL COMMENT 'type of record in the file', + `encoding` varchar(50) DEFAULT NULL COMMENT 'file encoding', + `match_field` varchar(10) DEFAULT NULL COMMENT 'the field that a vendor account has been mapped to in a marc record', + `match_value` varchar(50) DEFAULT NULL COMMENT 'the value to be matched against the marc record', + PRIMARY KEY (`id`), + CONSTRAINT `marc_ordering_account_ibfk_1` FOREIGN KEY (`vendor_id`) REFERENCES `aqbooksellers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `marc_ordering_account_ibfk_2` FOREIGN KEY (`budget_id`) REFERENCES `aqbudgets` (`budget_id`) ON DELETE CASCADE ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + } + ); + + say $out "Added new table 'marc_order_accounts'"; + } else { + say $out "Table 'marc_order_accounts' already exists"; + } + + $dbh->do( + q{ + INSERT IGNORE INTO systempreferences (variable, value, options, explanation, type) VALUES ('MarcOrderingAutomation', '0', 'NULL', 'Enables automatic order line creation from MARC records', 'YesNo'); + } + ); + + }, +}; diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index 270cca9f505..35255ac3379 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -4091,6 +4091,34 @@ CREATE TABLE `marc_modification_templates` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `marc_order_accounts` +-- + +DROP TABLE IF EXISTS `marc_order_accounts`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `marc_order_accounts` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'unique identifier and primary key', + `description` varchar(250) NOT NULL COMMENT 'description of this account', + `vendor_id` int(11) DEFAULT NULL COMMENT 'vendor id for this account', + `budget_id` int(11) DEFAULT NULL COMMENT 'budget id for this account', + `download_directory` mediumtext DEFAULT NULL COMMENT 'download directory for this account', + `matcher_id` int(11) DEFAULT NULL COMMENT 'the id of the match rule used (matchpoints.matcher_id)', + `overlay_action` varchar(50) DEFAULT NULL COMMENT 'how to handle duplicate records', + `nomatch_action` varchar(50) DEFAULT NULL COMMENT 'how to handle records where no match is found', + `item_action` varchar(50) DEFAULT NULL COMMENT 'what to do with item records', + `parse_items` tinyint(1) DEFAULT NULL COMMENT 'should items be parsed', + `record_type` varchar(50) DEFAULT NULL COMMENT 'type of record in the file', + `encoding` varchar(50) DEFAULT NULL COMMENT 'file encoding', + `match_field` varchar(10) DEFAULT NULL COMMENT 'the field that a vendor account has been mapped to in a marc record', + `match_value` varchar(50) DEFAULT NULL COMMENT 'the value to be matched against the marc record', + PRIMARY KEY (`id`), + CONSTRAINT `marc_ordering_account_ibfk_1` FOREIGN KEY (`vendor_id`) REFERENCES `aqbooksellers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `marc_ordering_account_ibfk_2` FOREIGN KEY (`budget_id`) REFERENCES `aqbudgets` (`budget_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `marc_overlay_rules` -- diff --git a/installer/data/mysql/mandatory/sysprefs.sql b/installer/data/mysql/mandatory/sysprefs.sql index 30197591a8b..c22b4a114e4 100644 --- a/installer/data/mysql/mandatory/sysprefs.sql +++ b/installer/data/mysql/mandatory/sysprefs.sql @@ -368,6 +368,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('MarcFieldsToOrder','',NULL,'Set the mapping values for a new order line created from a MARC record in a staged file. In a YAML format.','textarea'), ('MarcItemFieldsToOrder','',NULL,'Set the mapping values for new item records created from a MARC record in a staged file. In a YAML format.','textarea'), ('MarkLostItemsAsReturned','batchmod,moredetail,cronjob,additem,pendingreserves,onpayment','claim_returned|batchmod|moredetail|cronjob|additem|pendingreserves|onpayment','Mark items as returned when flagged as lost','multiple'), +('MarcOrderingAutomation','0',NULL,'Enables automatic order line creation from MARC records','YesNo'), ('MARCOrgCode','OSt','','Define MARC Organization Code for MARC21 records - http://www.loc.gov/marc/organizations/orgshome.html','free'), ('MARCOverlayRules','0',NULL,'Use the MARC record overlay rules system to decide what actions to take for each field when modifying records.','YesNo'), ('MaxFine',NULL,'','Maximum fine a patron can have for all late returns at one moment. Single item caps are specified in the circulation rules matrix.','Integer'), diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/acquisitions-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/acquisitions-menu.inc index e95bd8ecded..85df7143e4c 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/acquisitions-menu.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/acquisitions-menu.inc @@ -39,6 +39,9 @@
  • EDI accounts
  • Library EANs
  • [% END %] + [% IF Koha.Preference('MarcOrderingAutomation') %] +
  • MARC order accounts
  • + [% END %] [% IF CAN_user_acquisition_edit_invoices && CAN_user_parameters_manage_additional_fields %]
  • Manage invoice fields diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc index c0d492f2522..0496ef7e981 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc @@ -138,6 +138,9 @@
  • EDI accounts
  • Library EANs
  • [% END %] + [% IF Koha.Preference('MarcOrderingAutomation') %] +
  • MARC order accounts
  • + [% END %] [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt index 47b2142fea0..6c74fb60673 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt @@ -230,6 +230,10 @@
    Library EANs
    Manage library EDI EANs
    [% END %] + [% IF Koha.Preference('MarcOrderingAutomation') %] +
    MARC order accounts
    +
    Manage vendor accounts for automated order line creation from marc records
    + [% END %] [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/marc_order_accounts.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/marc_order_accounts.tt new file mode 100644 index 00000000000..2b7ecc49262 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/marc_order_accounts.tt @@ -0,0 +1,365 @@ +[% USE raw %] +[% USE Koha %] +[% USE Asset %] +[% PROCESS 'i18n.inc' %] +[% SET footerjs = 1 %] +[% INCLUDE 'doc-head-open.inc' %] + +MARC Order Accounts + +[% INCLUDE 'doc-head-close.inc' %] + + +[% WRAPPER 'header.inc' %] + [% INCLUDE 'prefs-admin-search.inc' %] +[% END %] + +[% WRAPPER 'sub-header.inc' %] + [% WRAPPER breadcrumbs %] + [% WRAPPER breadcrumb_item %] + Administration + [% END %] + + [% IF acct_form || delete_confirm %] + [% WRAPPER breadcrumb_item %] + MARC Order Accounts + [% END %] + [% ELSE %] + [% WRAPPER breadcrumb_item bc_active= 1 %] + MARC Order Accounts + [% END %] + [% END %] + [% END #/ WRAPPER breadcrumbs %] +[% END #/ WRAPPER sub-header.inc %] + +
    +
    +
    +
    + [% IF display %] + + [% IF ( accounts ) %] +

    Marc Ordering Accounts

    +
    + + + + + + + + + + [% FOREACH account IN accounts %] + + + + + + + + + [% END %] +
    IDVendorBudgetDescriptionDownload directoryActions
    [% account.id | html %][% account.vendor.name | html %][% account.budget.budget_name | html %][% account.description | html %][% account.download_directory | html %] + Edit + Delete +
    +
    + [% ELSE %] +
    + There are no MARC Order accounts. +
    + [% END %] + [% END %] + [% IF acct_form %] +

    + [% IF account %] + Modify account + [% ELSE %] + New account + [% END %] +

    +
    + + [% IF account %] + + [% END %] +
    + Account details +
      +
    1. + + +
    2. +
    3. + + +
      This budget will be used as the fallback value if the MARC records do not contain a mapped value for a budget code.
      +
    4. +
    5. + + +
    6. +
    7. + + +
      The download directory specifies the directory in your koha installation that should be searched for new files.
      +
    8. +
    9. + + +
      (Optional): If you have files from multiple vendors in the same file directory, the match field is the field in the marc record that will be checked to see if the file should be processed by this account.
      +
    10. +
    11. + + +
      (Optional): This is the value that will be checked against the match field to see if the file matches this account. If it does it will be processed by this account, if not it will be skipped.
      +
    12. +
    +
    +
    + File import settings +
      +
    1. + + +
    2. +
    3. + + +
    4. +
    +
    +
    + Record matching settings +
      +
    1. + + +
    2. +
    3. + + +
    4. +
    5. + + +
    6. +
    +
    +
    + Check for embedded item record data? +
      +
    1. + [% IF ( account.parse_items == 1 ) %] + + [% ELSE %] + + [% END %] + +
    2. +
    3. + [% IF ( account.parse_items == 0 ) %] + + [% ELSE %] + + [% END %] + +
    4. +
    +
      +
    1. + + +
    2. +
    +
    + +
    + + Cancel +
    +
    + [% END %] + [% IF delete_acct %] +
    +

    Delete this account?

    + + + + + + + + + + +
    VendorDescription
    [% account.vendor.name | html %][% account.description | html %]
    +
    + +
    + + + +
    +
    + +
    +
    + [% END %] +
    +
    + +
    + +
    +
    + +[% MACRO jsinclude BLOCK %] + [% Asset.js("js/admin-menu.js") | $raw %] +[% END %] +[% INCLUDE 'intranet-bottom.inc' %] \ No newline at end of file diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/acquisitions.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/acquisitions.pref index 4e2c18512e1..d9ebc830e78 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/acquisitions.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/acquisitions.pref @@ -113,6 +113,13 @@ Acquisitions: - '
    If you choose EmailAddressForSuggestions you have to enter a valid email address:' - pref: EmailAddressForSuggestions class: email + - + - pref: MarcOrderingAutomation + default: no + choices: + 1: Enable + 0: Disable + - Enables automatic order line creation from MARC records Printing: - - Use the diff --git a/misc/cronjobs/marc_ordering_process.pl b/misc/cronjobs/marc_ordering_process.pl new file mode 100644 index 00000000000..0ab28c4a1d3 --- /dev/null +++ b/misc/cronjobs/marc_ordering_process.pl @@ -0,0 +1,137 @@ + +#!/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; + print "No new files found\n" if scalar(@files) == 0; + + my $files_processed = 0; + + foreach my $filename ( @files ) { + say sprintf "Creating order lines from file %s", $filename if $verbose; + my $full_path = "$working_dir/$filename"; + my $args = { + filename => $filename, + filepath => $full_path, + profile => $acct, + agent => 'cron' + }; + if($acct->match_field && $acct->match_value) { + my $file_match = Koha::MarcOrder->match_file_to_account($args); + next if !$file_match; + } + if($confirm) { + my $result = Koha::MarcOrder->create_order_lines_from_file($args); + if($result->{success}) { + $files_processed++; + 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; + }; + } + } + say sprintf "%s file(s) processed", $files_processed unless $files_processed == 0; + print "Moving to next account\n\n"; +} +print "Process complete\n"; +cronlogaction({ action => 'End', info => "COMPLETED" }); +