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 <https://www.westminster.gov.uk/>
Sponsored-by: Royal Borough of Kensington and Chelsea <https://www.rbkc.gov.uk/>
Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
Martin Renvoize 2025-08-01 14:30:11 +00:00 committed by Lucas Gass
parent 14064e9459
commit 76c2523b12
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
6 changed files with 192 additions and 11 deletions

View File

@ -54,6 +54,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;
@ -1083,7 +1084,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};
@ -1091,6 +1114,9 @@ sub quote_item {
if ( $new_item->{$lsq_field} ) {
$item_hash->{$lsq_field} = $new_item->{$lsq_field};
}
if ( $new_item->{$lsl_field} ) {
$item_hash->{$lsl_field} = $new_item->{$lsl_field};
}
if ( $new_item->{itemcallnumber} ) {
$item_hash->{itemcallnumber} = $new_item->{itemcallnumber};
}
@ -1167,7 +1193,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;
@ -1217,6 +1265,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 ) = @_;
@ -1370,8 +1466,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 = {};

View File

@ -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',

View File

@ -388,6 +388,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' ) {
@ -395,12 +396,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 {
@ -421,13 +433,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(
@ -558,6 +580,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;
@ -573,10 +596,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} };

View File

@ -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" );
},
};

View File

@ -236,7 +236,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),

View File

@ -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."