From cdb64201440f38be551d3541eb883547df36c2f7 Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Fri, 1 Aug 2025 14:30:11 +0000 Subject: [PATCH] Bug 40391: Add support for GIR:LSL (Library Sub-Location) in EDIFACT processing This implements support for the new EDItEUR/BiC GIR:LSL (Library Sub-Location) field to complement the existing GIR:LSQ (Library Sequence) functionality, allowing flexible mapping of both fields to location and collection item fields. Changes: - Add LSL mapping to GIR field extraction in Koha::Edifact::Line - Update gir_segments() in Koha::Edifact::Order to handle both LSL and LSQ preferences independently - Add validation functions in Koha::EDI using Koha::AuthorisedValues for location/collection codes - Add EdifactLSL system preference for independent LSL field mapping - Update EdifactLSQ to include empty option for ignore functionality - Add admin interface configuration for LSL preference The implementation provides maximum flexibility: - EdifactLSQ can be set to: location, ccode, or empty (ignore) - EdifactLSL can be set to: location, ccode, or empty (ignore) - Both fields can be mapped independently or to same target field - Validation ensures codes exist in LOC/CCODE authorised values - Maintains full backwards compatibility with existing LSQ-only setups Test plan: 1. Apply both patches 2. Update database: installer/data/mysql/updatedatabase.pl 3. Verify new system preferences in Administration > System preferences > Acquisitions: - EdifactLSQ: "Map EDI sequence code (GIR+LSQ) to Koha Item field" - EdifactLSL: "Map EDI sub-location code (GIR+LSL) to Koha Item field" 4. Create test authorised values: - LOC category: Add values like FICTION, REFERENCE - CCODE category: Add values like ADULT, CHILD 5. Set preferences to test combinations: - EdifactLSQ=location, EdifactLSL=ccode - EdifactLSQ=ccode, EdifactLSL=location - EdifactLSQ=location, EdifactLSL="" (empty) 6. Process EDIFACT messages containing GIR+LSL and GIR+LSQ segments 7. Verify items created with correct location/ccode values per preferences 8. Test invalid codes generate appropriate error messages 9. Confirm existing LSQ-only configurations continue working unchanged 10. Run: prove t/Edifact.t t/db_dependent/Koha/EDI.t t/db_dependent/Koha/Edifact/Order.t --- Koha/EDI.pm | 110 +++++++++++++++++- Koha/Edifact/Line.pm | 1 + Koha/Edifact/Order.pm | 45 +++++-- .../data/mysql/atomicupdate/bug_40391.pl | 31 +++++ installer/data/mysql/mandatory/sysprefs.sql | 5 +- .../admin/preferences/acquisitions.pref | 10 ++ 6 files changed, 190 insertions(+), 12 deletions(-) create mode 100755 installer/data/mysql/atomicupdate/bug_40391.pl diff --git a/Koha/EDI.pm b/Koha/EDI.pm index 58b3209fd14..3058a31b260 100644 --- a/Koha/EDI.pm +++ b/Koha/EDI.pm @@ -44,6 +44,7 @@ use Koha::Plugins; # Adds plugin dirs to @INC use Koha::Plugins::Handler; use Koha::Acquisition::Baskets; use Koha::Acquisition::Booksellers; +use Koha::AuthorisedValues; use Koha::Util::FrameworkPlugin qw( biblio_008 ); our $VERSION = 1.1; @@ -993,7 +994,29 @@ sub quote_item { }; my $lsq_field = C4::Context->preference('EdifactLSQ'); - $new_item->{$lsq_field} = $item->girfield( 'sequence_code', $occurrence ); + my $lsl_field = C4::Context->preference('EdifactLSL'); + + # Handle LSQ field + if ( $lsq_field && $item->girfield( 'sequence_code', $occurrence ) ) { + my $lsq_value = $item->girfield( 'sequence_code', $occurrence ); + my $valid = + ( $lsq_field eq 'location' ) + ? _validate_location_code( $lsq_value, $quote_message ) + : _validate_collection_code( $lsq_value, $quote_message ); + + $new_item->{$lsq_field} = $lsq_value if $valid; + } + + # Handle LSL field + if ( $lsl_field && $item->girfield( 'sub_location_code', $occurrence ) ) { + my $lsl_value = $item->girfield( 'sub_location_code', $occurrence ); + my $valid = + ( $lsl_field eq 'location' ) + ? _validate_location_code( $lsl_value, $quote_message ) + : _validate_collection_code( $lsl_value, $quote_message ); + + $new_item->{$lsl_field} = $lsl_value if $valid; + } if ( $new_item->{itype} ) { $item_hash->{itype} = $new_item->{itype}; @@ -1077,7 +1100,29 @@ sub quote_item { homebranch => $item->girfield( 'branch', $occurrence ), }; my $lsq_field = C4::Context->preference('EdifactLSQ'); - $new_item->{$lsq_field} = $item->girfield( 'sequence_code', $occurrence ); + my $lsl_field = C4::Context->preference('EdifactLSL'); + + # Handle LSQ mapping with validation + if ( $lsq_field && $item->girfield( 'sequence_code', $occurrence ) ) { + my $lsq_value = $item->girfield( 'sequence_code', $occurrence ); + my $valid = + ( $lsq_field eq 'location' ) + ? _validate_location_code( $lsq_value, $quote_message ) + : _validate_collection_code( $lsq_value, $quote_message ); + + $new_item->{$lsq_field} = $lsq_value if $valid; + } + + # Handle LSL mapping with validation + if ( $lsl_field && $item->girfield( 'sub_location_code', $occurrence ) ) { + my $lsl_value = $item->girfield( 'sub_location_code', $occurrence ); + my $valid = + ( $lsl_field eq 'location' ) + ? _validate_location_code( $lsl_value, $quote_message ) + : _validate_collection_code( $lsl_value, $quote_message ); + + $new_item->{$lsl_field} = $lsl_value if $valid; + } $new_item->{biblionumber} = $bib->{biblionumber}; $new_item->{biblioitemnumber} = $bib->{biblioitemnumber}; my $kitem = Koha::Item->new($new_item)->store; @@ -1127,6 +1172,54 @@ sub get_edifact_ean { return $eans->[0]; } +sub _validate_location_code { + my ( $location_code, $quote_message ) = @_; + + return 1 unless $location_code; # Skip if empty + + my $av = Koha::AuthorisedValues->find( + { + category => 'LOC', + authorised_value => $location_code + } + ); + + unless ($av) { + $quote_message->add_to_edifact_errors( + { + section => "GIR+LSQ/LSL validation", + details => "Invalid location code '$location_code' - not found in LOC authorized values" + } + ); + return 0; + } + return 1; +} + +sub _validate_collection_code { + my ( $ccode, $quote_message ) = @_; + + return 1 unless $ccode; # Skip if empty + + my $av = Koha::AuthorisedValues->find( + { + category => 'CCODE', + authorised_value => $ccode + } + ); + + unless ($av) { + $quote_message->add_to_edifact_errors( + { + section => "GIR+LSQ/LSL validation", + details => "Invalid collection code '$ccode' - not found in CCODE authorized values" + } + ); + return 0; + } + return 1; +} + # We should not need to have a routine to do this here sub _discounted_price { my ( $discount, $price, $discounted_price ) = @_; @@ -1280,8 +1373,19 @@ sub _create_item_from_quote { $item_hash->{booksellerid} = $quote->vendor_id; $item_hash->{price} = $item_hash->{replacementprice} = $item->price; $item_hash->{itype} = $item->girfield('stock_category'); + my $lsq_field = C4::Context->preference('EdifactLSQ'); - $item_hash->{$lsq_field} = $item->girfield('sequence_code'); + my $lsl_field = C4::Context->preference('EdifactLSL'); + + # Handle LSQ mapping + if ( $lsq_field && $item->girfield('sequence_code') ) { + $item_hash->{$lsq_field} = $item->girfield('sequence_code'); + } + + # Handle LSL mapping + if ( $lsl_field && $item->girfield('sub_location_code') ) { + $item_hash->{$lsl_field} = $item->girfield('sub_location_code'); + } my $note = {}; diff --git a/Koha/Edifact/Line.pm b/Koha/Edifact/Line.pm index 4c55e06f757..6feb8c865b2 100644 --- a/Koha/Edifact/Line.pm +++ b/Koha/Edifact/Line.pm @@ -658,6 +658,7 @@ sub extract_gir { LRS => 'record_sublocation', LSM => 'shelfmark', LSQ => 'sequence_code', + LSL => 'sub_location_code', LST => 'stock_category', LSZ => 'size_code', LVC => 'coded_servicing_instruction', diff --git a/Koha/Edifact/Order.pm b/Koha/Edifact/Order.pm index 0bf67829cae..ea6ac9689ec 100644 --- a/Koha/Edifact/Order.pm +++ b/Koha/Edifact/Order.pm @@ -371,6 +371,7 @@ sub order_line { # we dont currently support this in koha # GIR copy-related data my $lsq_field = C4::Context->preference('EdifactLSQ'); + my $lsl_field = C4::Context->preference('EdifactLSL'); my @items; if ( $basket->effective_create_items eq 'ordering' ) { @@ -378,12 +379,23 @@ sub order_line { foreach my $item (@linked_itemnumbers) { my $i_obj = $schema->resultset('Item')->find( $item->itemnumber ); if ( defined $i_obj ) { - push @items, { + my $item_data = { branchcode => $i_obj->get_column('homebranch'), itype => $i_obj->effective_itemtype, - $lsq_field => $i_obj->$lsq_field, itemcallnumber => $i_obj->itemcallnumber, }; + + # Handle LSQ mapping + if ($lsq_field) { + $item_data->{$lsq_field} = $i_obj->$lsq_field; + } + + # Handle LSL mapping + if ($lsl_field) { + $item_data->{$lsl_field} = $i_obj->$lsl_field; + } + + push @items, $item_data; } } } else { @@ -404,13 +416,23 @@ sub order_line { my $item_fields = []; for my $item (@items) { - push @{$item_fields}, - { + my $item_field_data = { branchcode => $item->{branchcode}, itype => $item->{itype}, - $lsq_field => $item->{$lsq_field}, itemcallnumber => $item->{itemcallnumber}, - }; + }; + + # Handle LSQ mapping + if ( $lsq_field && $item->{$lsq_field} ) { + $item_field_data->{$lsq_field} = $item->{$lsq_field}; + } + + # Handle LSL mapping + if ( $lsl_field && $item->{$lsl_field} ) { + $item_field_data->{$lsl_field} = $item->{$lsl_field}; + } + + push @{$item_fields}, $item_field_data; } $self->add_seg( gir_segments( @@ -541,6 +563,7 @@ sub gir_segments { my @segments; my $sequence_no = 1; my $lsq_field = C4::Context->preference('EdifactLSQ'); + my $lsl_field = C4::Context->preference('EdifactLSL'); foreach my $item (@onorderitems) { my $elements_added = 0; my @gir_elements; @@ -556,10 +579,18 @@ sub gir_segments { push @gir_elements, { identity_number => 'LST', data => $item->{itype} }; } - if ( $item->{$lsq_field} ) { + + # Handle LSQ mapping + if ( $lsq_field && $item->{$lsq_field} ) { push @gir_elements, { identity_number => 'LSQ', data => $item->{$lsq_field} }; } + + # Handle LSL mapping + if ( $lsl_field && $item->{$lsl_field} ) { + push @gir_elements, + { identity_number => 'LSL', data => $item->{$lsl_field} }; + } if ( $item->{itemcallnumber} ) { push @gir_elements, { identity_number => 'LSM', data => $item->{itemcallnumber} }; diff --git a/installer/data/mysql/atomicupdate/bug_40391.pl b/installer/data/mysql/atomicupdate/bug_40391.pl new file mode 100755 index 00000000000..2bf91805c70 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_40391.pl @@ -0,0 +1,31 @@ +use Modern::Perl; +use Koha::Installer::Output qw(say_warning say_success say_info); + +return { + bug_number => "40391", + description => "Add EdifactLSL system preference for GIR:LSL (Library Sub-Location) field mapping", + up => sub { + my ($args) = @_; + my ( $dbh, $out ) = @$args{qw(dbh out)}; + + # Update existing EdifactLSQ preference to include empty option + $dbh->do( + q{ + UPDATE systempreferences + SET options = 'location|ccode|', explanation = 'Map EDI sequence code (GIR+LSQ) to Koha Item field, empty to ignore' + WHERE variable = 'EdifactLSQ' + } + ); + + # Add new EdifactLSL preference + $dbh->do( + q{ + INSERT IGNORE INTO systempreferences (variable, value, options, explanation, type) + VALUES ('EdifactLSL', '', 'location|ccode|', 'Map EDI sub-location code (GIR+LSL) to Koha Item field, empty to ignore', 'Choice') + } + ); + + say_success( $out, "Updated EdifactLSQ system preference to include empty option" ); + say_success( $out, "Added EdifactLSL system preference for GIR:LSL field mapping" ); + }, +}; diff --git a/installer/data/mysql/mandatory/sysprefs.sql b/installer/data/mysql/mandatory/sysprefs.sql index 76954d20229..549ff29dc90 100644 --- a/installer/data/mysql/mandatory/sysprefs.sql +++ b/installer/data/mysql/mandatory/sysprefs.sql @@ -233,7 +233,8 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('EasyAnalyticalRecords','0','','If on, display in the catalogue screens tools to easily setup analytical record relationships','YesNo'), ('EDIFACT','0',NULL,'Enables EDIFACT acquisitions functions','YesNo'), ('EdifactInvoiceImport', 'automatic', 'automatic|manual', "If on, don't auto-import EDI invoices, just keep them in the database with the status 'new'", 'Choice'), -('EdifactLSQ', 'location', 'location|ccode', 'Map EDI sequence code (GIR+LSQ) to Koha Item field', 'Choice'), +('EdifactLSL', 'ccode', 'location|ccode|', 'Map EDI sub-location code (GIR+LSL) to Koha Item field, empty to ignore', 'Choice'), +('EdifactLSQ', 'location', 'location|ccode|', 'Map EDI sequence code (GIR+LSQ) to Koha Item field, empty to ignore', 'Choice'), ('ElasticsearchBoostFieldMatch', '0', NULL, 'Add a "match" query to es when searching, will follow indexes chosen in advanced search, or use title-cover for generic keyword or title index search', 'YesNo'), ('ElasticsearchCrossFields', '1', '', 'Enable "cross_fields" option for searches using Elastic search.', 'YesNo'), ('ElasticsearchIndexStatus_authorities', '0', 'Authorities index status', NULL, NULL), @@ -889,4 +890,4 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('z3950AuthorAuthFields','701,702,700',NULL,'Define the MARC biblio fields for Personal Name Authorities to fill biblio.author','free'), ('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo'), ('z3950Status','','','This syspref allows to define custom YAML based rules for marking items unavailable in z3950 results.','Textarea') -; \ No newline at end of file +; diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/acquisitions.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/acquisitions.pref index 7f1a8860956..96b3b6b2b1d 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 @@ -185,4 +185,14 @@ Acquisitions: choices: location: "location" ccode: "collection" + "": "ignore" + - " in items." + - + - "Map sub-location code (GIR:LSL) field to " + - pref: EdifactLSL + default: "" + choices: + location: "location" + ccode: "collection" + "": "ignore" - " in items." -- 2.50.1