From a9f4b5f86d4ac2ff411a6b80d34d32beb4d15264 Mon Sep 17 00:00:00 2001 From: Matt Blenkinsop Date: Mon, 24 Jul 2023 13:51:43 +0000 Subject: [PATCH 01/11] Bug 34355: DB schema and atomic update file --- Koha/Schema/Result/Aqbookseller.pm | 19 +- Koha/Schema/Result/Aqbudget.pm | 19 +- Koha/Schema/Result/MarcOrderAccount.pm | 208 ++++++++++++++++++ .../bug_34355-create_environment.pl | 61 +++++ installer/data/mysql/kohastructure.sql | 26 +++ installer/data/mysql/mandatory/sysprefs.sql | 1 + 6 files changed, 330 insertions(+), 4 deletions(-) create mode 100644 Koha/Schema/Result/MarcOrderAccount.pm create mode 100644 installer/data/mysql/atomicupdate/bug_34355-create_environment.pl 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..88f6119290c --- /dev/null +++ b/Koha/Schema/Result/MarcOrderAccount.pm @@ -0,0 +1,208 @@ +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 + +=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 }, +); + +=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-07-18 16:31:16 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:vMLrmisXQnn2e60qW7ppnA + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/installer/data/mysql/atomicupdate/bug_34355-create_environment.pl b/installer/data/mysql/atomicupdate/bug_34355-create_environment.pl new file mode 100644 index 00000000000..bcc85e7bf80 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_34355-create_environment.pl @@ -0,0 +1,61 @@ +use Modern::Perl; + +return { + bug_number => "N/A", + description => "Set up requirements for testing", + up => sub { + my ($args) = @_; + my ($dbh, $out) = @$args{qw(dbh out)}; + $dbh->do(q{ + UPDATE systempreferences SET value = 1 WHERE variable="AggressiveMatchOnISBN"; + }); + $dbh->do(q{ + INSERT INTO systempreferences (variable, value, options, explanation, type) VALUES ('MarcOrderingAutomation', '0', 'NULL', 'Enables automatic order line creation from MARC records', 'YesNo'); + }); + $dbh->do(q{ + UPDATE systempreferences SET value = 'homebranch: 975$a +holdingbranch: 975$b +itype: 975$y +nonpublic_note: 975$x +public_note: 975$z +loc: 975$c +ccode: 975$8 +notforloan: 975$7 +uri: 975$u +copyno: 975$n +quantity: 975$q +budget_code: 975$h +price: 975$p +replacementprice: 975$v' + WHERE variable="MarcItemFieldsToOrder"; + }); + $dbh->do(q{ + UPDATE systempreferences SET value = 'price: 975$p +quantity: 975$q +budget_code: 975$h' + WHERE variable="MarcFieldsToOrder"; + }); + $dbh->do(q{ + INSERT INTO import_batch_profiles (name, matcher_id, overlay_action, nomatch_action, item_action, parse_items, record_type, encoding, format) VALUES ('MARCOrder', 1, 'ignore', 'create_new', 'always_add', 1, 'biblio', 'UTF-8', 'MARCXML') + }); + $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', + 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; + }); + }, +}; diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index 270cca9f505..a59040f4bbb 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -4091,6 +4091,32 @@ 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', + 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'), From 0f1a8a142b48826aa14d03477a59d58713245d40 Mon Sep 17 00:00:00 2001 From: Matt Blenkinsop Date: Mon, 24 Jul 2023 13:52:18 +0000 Subject: [PATCH 02/11] Bug 34355: Update menus --- .../intranet-tmpl/prog/en/includes/acquisitions-menu.inc | 3 +++ koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc | 3 +++ .../intranet-tmpl/prog/en/modules/admin/admin-home.tt | 4 ++++ .../prog/en/modules/admin/preferences/acquisitions.pref | 7 +++++++ 4 files changed, 17 insertions(+) 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/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 From ebc840a4bed8fedb75438ada2a8a13293337996b Mon Sep 17 00:00:00 2001 From: Matt Blenkinsop Date: Mon, 24 Jul 2023 13:52:41 +0000 Subject: [PATCH 03/11] Bug 34355: Add account creation --- Koha/MarcOrderAccount.pm | 63 +++++ Koha/MarcOrderAccounts.pm | 51 ++++ admin/marc_order_accounts.pl | 132 ++++++++++ .../en/modules/admin/marc_order_accounts.tt | 246 ++++++++++++++++++ 4 files changed, 492 insertions(+) create mode 100644 Koha/MarcOrderAccount.pm create mode 100644 Koha/MarcOrderAccounts.pm create mode 100644 admin/marc_order_accounts.pl create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/admin/marc_order_accounts.tt 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/admin/marc_order_accounts.pl b/admin/marc_order_accounts.pl new file mode 100644 index 00000000000..7d19934ea64 --- /dev/null +++ b/admin/marc_order_accounts.pl @@ -0,0 +1,132 @@ +#!/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', + }; + + 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/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..c5e68e92fcb --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/marc_order_accounts.tt @@ -0,0 +1,246 @@ +[% 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. +
    +
    +
    + File import settings +
      +
    1. + + +
    2. +
    3. + + +
    4. +
    +
    +
    + Record matching settings +
      +
    1. + + +
    2. +
    3. + + [% INCLUDE 'tools-overlay-action.inc' %] +
    4. +
    5. + + [% INCLUDE 'tools-nomatch-action.inc' %] +
    6. +
    +
    +
    + Check for embedded item record data? +
      +
    1. + + +
    2. +
    3. + + +
    4. +
    +
      +
    1. + [% INCLUDE 'tools-item-action.inc' %] +
    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 From e517e8860dec89bc5d2c351ee82b43deff6d0848 Mon Sep 17 00:00:00 2001 From: Matt Blenkinsop Date: Mon, 24 Jul 2023 13:53:22 +0000 Subject: [PATCH 04/11] 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 00000000000..fb073ce9db3 --- /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 fb31a8bcee4..f62916ff3b3 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/misc/cronjobs/marc_ordering_process.pl b/misc/cronjobs/marc_ordering_process.pl new file mode 100644 index 00000000000..cf0b7312311 --- /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" }); + From f2503d9d9d28af66f82191938c1874181013b392 Mon Sep 17 00:00:00 2001 From: Matt Blenkinsop Date: Tue, 8 Aug 2023 10:39:10 +0000 Subject: [PATCH 05/11] Bug 34355: Make item addition dependent on MarcItemFieldsToOrder mappings and introduce MarcFieldsToOrder to cronjob --- Koha/MarcOrder.pm | 123 ++++++++++++++++++++----- acqui/addorderiso2709.pl | 2 +- misc/cronjobs/marc_ordering_process.pl | 6 +- 3 files changed, 106 insertions(+), 25 deletions(-) diff --git a/Koha/MarcOrder.pm b/Koha/MarcOrder.pm index fb073ce9db3..88788ff3d88 100644 --- a/Koha/MarcOrder.pm +++ b/Koha/MarcOrder.pm @@ -325,6 +325,41 @@ sub _stage_file { }; } +=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); @@ -359,8 +394,6 @@ sub _get_MarcItemFieldsToOrder_syspref_data { } @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}; @@ -375,21 +408,24 @@ sub _get_MarcItemFieldsToOrder_syspref_data { $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}; + 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; } - push @result, $r; } + return $result[0]; } @@ -416,7 +452,6 @@ sub _verify_number_of_fields { 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 }; } @@ -539,6 +574,23 @@ sub add_items_from_import_record { 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}; @@ -550,7 +602,7 @@ sub add_items_from_import_record { 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_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} ) { @@ -566,13 +618,10 @@ sub add_items_from_import_record { 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."; - } + my $itemcreation = 0; for (my $i = 0; $i < $item_quantity; $i++) { + $itemcreation = 1; my $item = Koha::Item->new({ biblionumber => $biblionumber, homebranch => $item_homebranch, @@ -592,8 +641,8 @@ sub add_items_from_import_record { my %order_detail_hash = ( biblionumber => $biblionumber, - itemnumbers => ($item->itemnumber), basketno => $basket_id, + itemnumbers => ($item->itemnumber), quantity => 1, budget_id => $item_budget_id, currency => $vendor->listprice, @@ -615,6 +664,34 @@ sub add_items_from_import_record { 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}; + push @order_line_details, \%order_detail_hash; + } } if($agent eq 'client') { @@ -750,7 +827,7 @@ sub create_order_lines { my $order_line_details = $args->{order_line_details}; foreach my $order_detail ( @{ $order_line_details } ) { - my @itemnumbers = $order_detail->{itemnumbers}; + my @itemnumbers = $order_detail->{itemnumbers} || (); delete($order_detail->{itemnumber}); my $order = Koha::Acquisition::Order->new( \%{ $order_detail } ); $order->populate_with_prices_for_ordering(); diff --git a/acqui/addorderiso2709.pl b/acqui/addorderiso2709.pl index f62916ff3b3..7f4fbb51216 100755 --- a/acqui/addorderiso2709.pl +++ b/acqui/addorderiso2709.pl @@ -201,7 +201,7 @@ matcher_id => $matcher_id, overlay_action => $overlay_action, agent => 'client', - import_record_id_selected => @import_record_id_selected, + import_record_id_selected => \@import_record_id_selected, client_item_fields => $client_item_fields, basket_id => $cgiparams->{'basketno'}, vendor => $bookseller, diff --git a/misc/cronjobs/marc_ordering_process.pl b/misc/cronjobs/marc_ordering_process.pl index cf0b7312311..c1a740401e3 100644 --- a/misc/cronjobs/marc_ordering_process.pl +++ b/misc/cronjobs/marc_ordering_process.pl @@ -100,6 +100,9 @@ =head1 OPTIONS 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; @@ -113,6 +116,7 @@ =head1 OPTIONS }; 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 { @@ -121,7 +125,7 @@ =head1 OPTIONS }; } } - print "All files completed\n"; + say sprintf "%s files processed", $files_processed unless $files_processed == 0; print "Moving to next account\n\n"; } print "Process complete\n"; From 818afb623dfc4a1aab6cd47f3070b6ab527543e3 Mon Sep 17 00:00:00 2001 From: Matt Blenkinsop Date: Thu, 10 Aug 2023 14:23:18 +0000 Subject: [PATCH 06/11] Bug 34355: Fix referencing of import_record_id_selected --- Koha/MarcOrder.pm | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Koha/MarcOrder.pm b/Koha/MarcOrder.pm index 88788ff3d88..fbecd795463 100644 --- a/Koha/MarcOrder.pm +++ b/Koha/MarcOrder.pm @@ -42,6 +42,7 @@ use C4::Biblio qw( AddBiblio GetMarcFromKohaField TransformHtmlToXml + GetMarcQuantity ); use C4::Items qw( AddItemFromMarc ); use C4::Budgets qw( GetBudgetByCode ); @@ -163,7 +164,7 @@ 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 $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}; @@ -179,7 +180,7 @@ sub import_record_and_create_order_lines { matcher_id => $matcher_id, overlay_action => $overlay_action, agent => $agent, - import_record_id_selected => @import_record_id_selected, + import_record_id_selected => $import_record_id_selected, }); return { @@ -476,7 +477,7 @@ 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 $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}; @@ -489,7 +490,7 @@ sub add_biblios_from_import_record { record_result => 0, duplicates_in_batch => 0, skip => 1 - } if not grep { $_ eq $import_record->import_record_id } @import_record_id_selected; + } 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"; From 07848a8fbcbd6b93fe7c1ef02ce3c47214ac30a5 Mon Sep 17 00:00:00 2001 From: Matt Blenkinsop Date: Fri, 18 Aug 2023 08:43:53 +0000 Subject: [PATCH 07/11] Bug 34355: Fix form rendering issue --- .../en/modules/admin/marc_order_accounts.tt | 145 +++++++++++++++--- 1 file changed, 127 insertions(+), 18 deletions(-) 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 index c5e68e92fcb..1ef0c0f1174 100644 --- 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 @@ -7,9 +7,6 @@ MARC Order Accounts - [% INCLUDE 'doc-head-close.inc' %] @@ -137,19 +134,59 @@ MARC Order Accounts
  • @@ -161,7 +198,7 @@ MARC Order Accounts + [% IF ( account.overlay_action == 'replace' ) %] + + [% IF ( account.overlay_action == 'create_new' ) %] + + [% IF ( account.overlay_action == 'ignore' ) %] + +
  • - [% INCLUDE 'tools-nomatch-action.inc' %] +
  • @@ -183,17 +252,57 @@ MARC Order Accounts Check for embedded item record data?
    1. - + [% IF ( account.parse_items == 1 ) %] + + [% ELSE %] + + [% END %]
    2. - + [% IF ( account.parse_items == 0 ) %] + + [% ELSE %] + + [% END %]
      -
    1. - [% INCLUDE 'tools-item-action.inc' %] +
    2. + +
    From 06d0c823bb1091825cbbc0c3feb37e2d81834e06 Mon Sep 17 00:00:00 2001 From: Matt Blenkinsop Date: Wed, 23 Aug 2023 08:27:00 +0000 Subject: [PATCH 08/11] Bug 34355: Make atomicupdate file idempotent --- .../bug_34355-add_marc_order_accounts.pl | 43 +++++++++++++ .../bug_34355-create_environment.pl | 61 ------------------- 2 files changed, 43 insertions(+), 61 deletions(-) create mode 100644 installer/data/mysql/atomicupdate/bug_34355-add_marc_order_accounts.pl delete mode 100644 installer/data/mysql/atomicupdate/bug_34355-create_environment.pl 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..f0a5f2ab216 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_34355-add_marc_order_accounts.pl @@ -0,0 +1,43 @@ +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', + 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/atomicupdate/bug_34355-create_environment.pl b/installer/data/mysql/atomicupdate/bug_34355-create_environment.pl deleted file mode 100644 index bcc85e7bf80..00000000000 --- a/installer/data/mysql/atomicupdate/bug_34355-create_environment.pl +++ /dev/null @@ -1,61 +0,0 @@ -use Modern::Perl; - -return { - bug_number => "N/A", - description => "Set up requirements for testing", - up => sub { - my ($args) = @_; - my ($dbh, $out) = @$args{qw(dbh out)}; - $dbh->do(q{ - UPDATE systempreferences SET value = 1 WHERE variable="AggressiveMatchOnISBN"; - }); - $dbh->do(q{ - INSERT INTO systempreferences (variable, value, options, explanation, type) VALUES ('MarcOrderingAutomation', '0', 'NULL', 'Enables automatic order line creation from MARC records', 'YesNo'); - }); - $dbh->do(q{ - UPDATE systempreferences SET value = 'homebranch: 975$a -holdingbranch: 975$b -itype: 975$y -nonpublic_note: 975$x -public_note: 975$z -loc: 975$c -ccode: 975$8 -notforloan: 975$7 -uri: 975$u -copyno: 975$n -quantity: 975$q -budget_code: 975$h -price: 975$p -replacementprice: 975$v' - WHERE variable="MarcItemFieldsToOrder"; - }); - $dbh->do(q{ - UPDATE systempreferences SET value = 'price: 975$p -quantity: 975$q -budget_code: 975$h' - WHERE variable="MarcFieldsToOrder"; - }); - $dbh->do(q{ - INSERT INTO import_batch_profiles (name, matcher_id, overlay_action, nomatch_action, item_action, parse_items, record_type, encoding, format) VALUES ('MARCOrder', 1, 'ignore', 'create_new', 'always_add', 1, 'biblio', 'UTF-8', 'MARCXML') - }); - $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', - 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; - }); - }, -}; From db8cb9c39d5cf2fb84c85daa6edba70cacae9a15 Mon Sep 17 00:00:00 2001 From: Matt Blenkinsop Date: Wed, 6 Sep 2023 15:59:58 +0000 Subject: [PATCH 09/11] Add PO number to sort1 as the basket name --- Koha/MarcOrder.pm | 59 ++++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/Koha/MarcOrder.pm b/Koha/MarcOrder.pm index fbecd795463..d9f875b299f 100644 --- a/Koha/MarcOrder.pm +++ b/Koha/MarcOrder.pm @@ -89,11 +89,6 @@ sub create_order_lines_from_file { 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, @@ -116,6 +111,13 @@ sub create_order_lines_from_file { 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, @@ -128,11 +130,11 @@ sub create_order_lines_from_file { 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, + record_result => $result->{record_result}, + vendor => $vendor_record, + basket_id => $basket_id, + budget_id => $budget_id, + agent => $agent, }); my $order_lines = create_order_lines({ @@ -210,7 +212,7 @@ sub import_record_and_create_order_lines { =head3 _create_basket_for_file my $basket_id = _create_basket_for_file({ - filename => $filename, + import_records => $import_records, vendor_id => $vendor_id }); @@ -221,8 +223,14 @@ sub import_record_and_create_order_lines { sub _create_basket_for_file { my ( $args ) = @_; - my $filename = $args->{filename}; - my $vendor_id = $args->{vendor_id}; + 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; @@ -563,19 +571,22 @@ sub add_biblios_from_import_record { 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 $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 $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}; @@ -643,7 +654,7 @@ sub add_items_from_import_record { my %order_detail_hash = ( biblionumber => $biblionumber, basketno => $basket_id, - itemnumbers => ($item->itemnumber), + itemnumbers => ($item->itemnumber), quantity => 1, budget_id => $item_budget_id, currency => $vendor->listprice, From ba4ab812be676ca811b0f15dfd372d70b92c57f9 Mon Sep 17 00:00:00 2001 From: Matt Blenkinsop Date: Thu, 7 Sep 2023 10:47:15 +0000 Subject: [PATCH 10/11] Add match_field and match_value to allow multiple accounts in the same file directory --- Koha/MarcOrder.pm | 46 +++++++++++++++++++ Koha/Schema/Result/MarcOrderAccount.pm | 24 +++++++++- admin/marc_order_accounts.pl | 2 + .../bug_34355-add_marc_order_accounts.pl | 16 ++++--- installer/data/mysql/kohastructure.sql | 2 + .../en/modules/admin/marc_order_accounts.tt | 10 ++++ misc/cronjobs/marc_ordering_process.pl | 20 ++++---- 7 files changed, 104 insertions(+), 16 deletions(-) diff --git a/Koha/MarcOrder.pm b/Koha/MarcOrder.pm index d9f875b299f..d47babe43d2 100644 --- a/Koha/MarcOrder.pm +++ b/Koha/MarcOrder.pm @@ -852,4 +852,50 @@ sub create_order_lines { 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/Schema/Result/MarcOrderAccount.pm b/Koha/Schema/Result/MarcOrderAccount.pm index 88f6119290c..be720da960e 100644 --- a/Koha/Schema/Result/MarcOrderAccount.pm +++ b/Koha/Schema/Result/MarcOrderAccount.pm @@ -116,6 +116,22 @@ type of record in the file 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( @@ -143,6 +159,10 @@ __PACKAGE__->add_columns( { 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 @@ -200,8 +220,8 @@ __PACKAGE__->belongs_to( ); -# Created by DBIx::Class::Schema::Loader v0.07049 @ 2023-07-18 16:31:16 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:vMLrmisXQnn2e60qW7ppnA +# 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 diff --git a/admin/marc_order_accounts.pl b/admin/marc_order_accounts.pl index 7d19934ea64..8365e56ff2f 100644 --- a/admin/marc_order_accounts.pl +++ b/admin/marc_order_accounts.pl @@ -89,6 +89,8 @@ 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')) { 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 index f0a5f2ab216..149afbeee96 100644 --- a/installer/data/mysql/atomicupdate/bug_34355-add_marc_order_accounts.pl +++ b/installer/data/mysql/atomicupdate/bug_34355-add_marc_order_accounts.pl @@ -1,14 +1,15 @@ use Modern::Perl; return { - bug_number => "34355", + bug_number => "34355", description => "Add a table to allow creation of MARC order accounts and a syspref to activate it.", - up => sub { + up => sub { my ($args) = @_; - my ($dbh, $out) = @$args{qw(dbh out)}; + my ( $dbh, $out ) = @$args{qw(dbh out)}; - unless( TableExists('marc_order_accounts') ) { - $dbh->do(q{ + 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', @@ -22,11 +23,14 @@ `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 { diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index a59040f4bbb..35255ac3379 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -4111,6 +4111,8 @@ CREATE TABLE `marc_order_accounts` ( `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 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 index 1ef0c0f1174..2b7ecc49262 100644 --- 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 @@ -126,6 +126,16 @@ MARC Order Accounts
    The download directory specifies the directory in your koha installation that should be searched for new files.
    +
  • + + +
    (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.
    +
  • +
  • + + +
    (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.
    +
  • diff --git a/misc/cronjobs/marc_ordering_process.pl b/misc/cronjobs/marc_ordering_process.pl index c1a740401e3..0ab28c4a1d3 100644 --- a/misc/cronjobs/marc_ordering_process.pl +++ b/misc/cronjobs/marc_ordering_process.pl @@ -106,14 +106,18 @@ =head1 OPTIONS 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 $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}) { $files_processed++; @@ -125,7 +129,7 @@ =head1 OPTIONS }; } } - say sprintf "%s files processed", $files_processed unless $files_processed == 0; + say sprintf "%s file(s) processed", $files_processed unless $files_processed == 0; print "Moving to next account\n\n"; } print "Process complete\n"; From 6accdad64afd3ae21e5bb7bc672d31c42db729d6 Mon Sep 17 00:00:00 2001 From: Matt Blenkinsop Date: Tue, 12 Sep 2023 11:48:32 +0000 Subject: [PATCH 11/11] Add a plugin hook - before_orderline_create --- Koha/MarcOrder.pm | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Koha/MarcOrder.pm b/Koha/MarcOrder.pm index d47babe43d2..5f5dc5e1ca3 100644 --- a/Koha/MarcOrder.pm +++ b/Koha/MarcOrder.pm @@ -54,6 +54,7 @@ use Koha::Import::Records; use Koha::Acquisition::Currencies; use Koha::Acquisition::Booksellers; use Koha::Acquisition::Baskets; +use Koha::Plugins; =head1 NAME @@ -702,7 +703,16 @@ sub add_items_from_import_record { } $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; + } }