From 54a88c4e1b51b6d590797a6b5d2b0ee2804bd667 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 Sponsored-by: Westminster City Council Sponsored-by: Royal Borough of Kensington and Chelsea Sponsored-by: OpenFifth Signed-off-by: Hannah Dunne-Howrie --- 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 | 3 +- .../admin/preferences/acquisitions.pref | 10 ++ 6 files changed, 189 insertions(+), 11 deletions(-) create mode 100755 installer/data/mysql/atomicupdate/bug_40391.pl diff --git a/Koha/EDI.pm b/Koha/EDI.pm index 42d08a3d3f1..bcf4ad1a6cb 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 6dda2e857a8..dd4d7dfd674 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 a36bd7e1b5b..788942e4b19 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 0d77d5ba947..67d8ae766d2 100644 --- a/installer/data/mysql/mandatory/sysprefs.sql +++ b/installer/data/mysql/mandatory/sysprefs.sql @@ -235,7 +235,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), 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.51.0