From d6b1ee613f5061ac9014086a347840b7a98e5dfe Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Tue, 22 Jul 2025 14:08:59 +0100 Subject: [PATCH] Bug 40383: Add JSON representation to Koha::Edifact and JSON download to edimsg.pl This patch implements a JSON representation of EDIFACT interchanges for enhanced programmatic access and debugging capabilities. Backend Changes: - Add to_json() method to Koha::Edifact module - Structured JSON output with header, messages array, and trailer - Segment parsing with raw data, elements arrays, and line_id tracking - Line item grouping using LIN segment identifiers Frontend Changes: - Add JSON download button to edimsg.pl page - Support for ?format=json parameter and Accept: application/json header - Proper HTTP content-type handling for JSON responses - Error handling for missing messages in JSON format JSON Structure: { "header": "UNB+UNOC:3+...", "messages": [ { "header": "UNH+1+QUOTES:D:93A:UN:EAN008'", "segments": [ { "tag": "LIN", "raw": "LIN+1+1+9780552150040:EN'", "elements": ["1", "1", "9780552150040:EN"], "line_id": "1" } ], "trailer": "UNT+25+1'" } ], "trailer": "UNZ+1+1'" } Test plan: 1. Apply this patch 2. Ensure you have EDIFACT messages in your system 3. Go to Acquisitions > EDIFACT messages 4. Click "View message" for any message 5. Click the new "JSON" button 6. Verify JSON downloads with proper structure 7. Verify direct URL access with ?format=json parameter works 8. Test with Accept: application/json header --- Koha/Edifact.pm | 115 ++++++++++++++++++ acqui/edimsg.pl | 37 +++++- .../prog/en/modules/acqui/edimsg.tt | 7 ++ 3 files changed, 155 insertions(+), 4 deletions(-) diff --git a/Koha/Edifact.pm b/Koha/Edifact.pm index 7479a9b42b5..5985c1ff77d 100644 --- a/Koha/Edifact.pm +++ b/Koha/Edifact.pm @@ -21,6 +21,7 @@ use strict; use warnings; use File::Slurp qw( read_file ); use Carp qw( carp croak ); +use JSON qw( encode_json ); use Koha::Edifact::Segment; use Koha::Edifact::Message; @@ -172,6 +173,113 @@ sub message_array { return $msg_arr; } +sub to_json { + my $self = shift; + + # Find interchange header (UNB) + my $header_seg; + my $trailer_seg; + my @all_segments = @{ $self->{transmission} }; + + # Get header and trailer + for my $i ( 0 .. $#all_segments ) { + if ( $all_segments[$i]->tag eq 'UNB' ) { + $header_seg = $all_segments[$i]; + } elsif ( $all_segments[$i]->tag eq 'UNZ' ) { + $trailer_seg = $all_segments[$i]; + } + } + + # Build messages array + my @json_messages; + my $current_message; + my @current_segments; + my $current_line_id; + + for my $segment (@all_segments) { + my $tag = $segment->tag; + + if ( $tag eq 'UNH' ) { + + # Start new message + if ($current_message) { + $current_message->{segments} = [@current_segments]; + push @json_messages, $current_message; + } + $current_message = { + header => $segment->as_string, + segments => [], + }; + @current_segments = (); + $current_line_id = undef; + } elsif ( $tag eq 'UNT' ) { + + # End current message + if ($current_message) { + $current_message->{trailer} = $segment->as_string; + $current_message->{segments} = [@current_segments]; + push @json_messages, $current_message; + } + $current_message = undef; + @current_segments = (); + $current_line_id = undef; + } elsif ( $tag eq 'UNB' || $tag eq 'UNZ' ) { + + # Skip - these are handled separately + next; + } elsif ($current_message) { + + # Process segment within message + my $segment_data = { + tag => $tag, + raw => $segment->as_string, + elements => [], + }; + + # Extract elements + my $elem_count = 0; + while ( defined( my $elem = $segment->elem($elem_count) ) ) { + push @{ $segment_data->{elements} }, $elem; + $elem_count++; + } + + # Handle line_id assignment + if ( $tag eq 'LIN' ) { + $current_line_id = $segment->elem(0); + $segment_data->{line_id} = $current_line_id if defined $current_line_id; + } elsif ( $tag eq 'UNS' ) { + + # UNS (Section Control) marks transition to message summary + # Reset line_id so subsequent segments are treated as message-level + $current_line_id = undef; + } elsif ( defined $current_line_id && $tag !~ /^(UNH|UNT|UNS|CNT)$/ ) { + + # Assign line_id to segments that belong to current line item + # Exclude message control segments (UNH, UNT, UNS, CNT) + # After UNS, current_line_id is undefined, so segments are message-level + $segment_data->{line_id} = $current_line_id; + } + + push @current_segments, $segment_data; + } + } + + # Handle any remaining message + if ($current_message) { + $current_message->{segments} = [@current_segments]; + push @json_messages, $current_message; + } + + # Build final structure + my $json_structure = { + header => $header_seg ? $header_seg->as_string : undef, + messages => \@json_messages, + trailer => $trailer_seg ? $trailer_seg->as_string : undef, + }; + + return encode_json($json_structure); +} + # # internal parsing routines used in _init # @@ -291,6 +399,13 @@ Edifact - Edifact message handler return an array of Message objects contained in the Edifact transmission +=head2 to_json + + returns a JSON representation of the complete edifact interchange. + The JSON structure includes the interchange header, all messages with their + segments (both raw and parsed), and the interchange trailer. + Line items are grouped using line_id extracted from LIN segments. + =head1 Internal Methods =head2 _init diff --git a/acqui/edimsg.pl b/acqui/edimsg.pl index 8a49359e310..5891d7671e6 100755 --- a/acqui/edimsg.pl +++ b/acqui/edimsg.pl @@ -20,11 +20,18 @@ use Modern::Perl; use CGI; use Koha::Database; +use Koha::Edifact; use C4::Koha; use C4::Auth qw( get_template_and_user ); -use C4::Output qw( output_html_with_http_headers ); +use C4::Output qw( output_html_with_http_headers output_with_http_headers ); my $q = CGI->new; + +# Check if JSON output is requested +my $format = $q->param('format') || ''; +my $accept_header = $ENV{HTTP_ACCEPT} || ''; +my $wants_json = ( $format eq 'json' ) || ( $accept_header =~ m|application/json| ); + my ( $template, $loggedinuser, $cookie, $userflags ) = get_template_and_user( { template_name => 'acqui/edimsg.tt', @@ -33,17 +40,39 @@ my ( $template, $loggedinuser, $cookie, $userflags ) = get_template_and_user( flagsrequired => { acquisition => 'edi_manage' }, } ); + my $msg_id = $q->param('id'); my $schema = Koha::Database->new()->schema(); my $msg = $schema->resultset('EdifactMessage')->find($msg_id); + if ($msg) { my $transmission = $msg->raw_msg; - my @segments = segmentize($transmission); - $template->param( segments => \@segments ); + if ($wants_json) { + + # Return JSON representation using Koha::Edifact + my $edifact = Koha::Edifact->new( { transmission => $transmission } ); + my $json_output = $edifact->to_json(); + + output_with_http_headers( $q, $cookie, $json_output, 'json', '200 OK' ); + exit; + } else { + + # Return HTML representation (existing behavior) + my @segments = segmentize($transmission); + $template->param( + segments => \@segments, + id => $msg_id + ); + } } else { - $template->param( no_message => 1 ); + if ($wants_json) { + output_with_http_headers( $q, $cookie, '{"error":"Message not found"}', 'json', '404 Not Found' ); + exit; + } else { + $template->param( no_message => 1 ); + } } output_html_with_http_headers( $q, $cookie, $template->output ); diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/edimsg.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/edimsg.tt index 0f4a85e1419..eafc8927aee 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/edimsg.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/edimsg.tt @@ -32,6 +32,13 @@ [% IF no_message %]
The requested message cannot be displayed
[% ELSE %] +
+
+
+ JSON +
+
+