From 813cb88dcff88a24e629d757cab2a99da70b838a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Cohen=20Arazi?= Date: Wed, 14 Jan 2026 20:05:52 +0000 Subject: [PATCH] Bug 41620: Use Koha::CSV::ItemSearch for CSV exports This patch replaces template-based CSV generation with Koha::CSV::ItemSearch in both itemsearch.pl and item-export.pl, eliminating spacing issues around delimiters that caused Excel to display quotes as literal characters. Changes: - Update itemsearch.pl to use Koha::CSV::ItemSearch - Update item-export.pl to use Koha::CSV::ItemSearch - Remove obsolete CSV templates - Stream output directly without template overhead This fixes the issue where extra spaces around commas in the templates caused Excel to treat quotes as part of the data rather than CSV delimiters. Test plan: 1. Apply patches 2. Test itemsearch.pl: a. Go to Cataloging > Item search b. Perform a search c. Click "Export results" > "CSV" d. Open items.csv in Excel => SUCCESS: Fields display without quotes 3. Test item-export.pl: a. Go to a bibliographic record b. Select multiple items c. Click "Export selected items" d. Open items.csv in Excel => SUCCESS: Fields display without quotes 4. Verify all 21 columns are present and correctly formatted 5. Verify dates, authorized values, and library names display correctly 6. Sign off :-D --- catalogue/item-export.pl | 17 ++++++++++------- catalogue/itemsearch.pl | 16 +++++++++++++--- .../includes/catalogue/itemsearch_item.csv.inc | 11 ----------- .../csv_headers/catalogue/itemsearch.tt | 7 ------- .../prog/en/modules/catalogue/itemsearch_csv.tt | 6 ------ 5 files changed, 23 insertions(+), 34 deletions(-) delete mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.csv.inc delete mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/csv_headers/catalogue/itemsearch.tt delete mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch_csv.tt diff --git a/catalogue/item-export.pl b/catalogue/item-export.pl index 422f7490e9..5dbfbf3931 100755 --- a/catalogue/item-export.pl +++ b/catalogue/item-export.pl @@ -23,12 +23,14 @@ use CGI; use C4::Auth qw( get_template_and_user ); use C4::Output; +use Koha::CSV::ItemSearch; +use Koha::Items; my $cgi = CGI->new; my ( $template, $borrowernumber, $cookie ) = get_template_and_user( { - template_name => 'catalogue/itemsearch_csv.tt', + template_name => 'catalogue/itemsearch.tt', query => $cgi, type => 'intranet', flagsrequired => { catalogue => 1 }, @@ -54,16 +56,17 @@ if ( $format eq 'barcodes' ) { exit; } -$template->param( - results => $items, -); - +# CSV export print $cgi->header( { type => 'text/csv', attachment => 'items.csv', } ); -for my $line ( split '\n', $template->output ) { - print "$line\n" unless $line =~ m|^\s*$|; + +my $csv = Koha::CSV::ItemSearch->new(); +$csv->print_headers(*STDOUT); + +while ( my $item = $items->next ) { + $csv->print_item( *STDOUT, $item ); } diff --git a/catalogue/itemsearch.pl b/catalogue/itemsearch.pl index ae8dd8241d..d5f4784810 100755 --- a/catalogue/itemsearch.pl +++ b/catalogue/itemsearch.pl @@ -85,8 +85,8 @@ if ( defined $format and $format eq 'json' ) { $cgi->param( 'op', @op ); $cgi->param( 'c', @c ); } elsif ( defined $format and $format eq 'csv' ) { - $template_name = 'catalogue/itemsearch_csv.tt'; + # CSV export will be handled directly, no template needed # Retrieve all results $cgi->param( 'rows', 0 ); } elsif ( defined $format and $format eq 'barcodes' ) { @@ -304,6 +304,9 @@ if ( defined $format and $format ne 'shareable' ) { ); if ( $format eq 'csv' ) { + require Koha::CSV::ItemSearch; + require Koha::Items; + print $cgi->header( { type => 'text/csv', @@ -311,8 +314,15 @@ if ( defined $format and $format ne 'shareable' ) { } ); - for my $line ( split '\n', $template->output ) { - print "$line\n" unless $line =~ m|^\s*$|; + my $csv = Koha::CSV::ItemSearch->new(); + + # Print headers + $csv->print_headers(*STDOUT); + + # Print each item + foreach my $item_data (@$results) { + my $item = Koha::Items->find( $item_data->{itemnumber} ); + $csv->print_item( *STDOUT, $item ) if $item; } } elsif ( $format eq 'json' ) { $template->param( draw => scalar $cgi->param('draw') ); diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.csv.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.csv.inc deleted file mode 100644 index 9dd823445e..0000000000 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.csv.inc +++ /dev/null @@ -1,11 +0,0 @@ -[%- USE raw -%] -[%- USE Branches -%] -[%- USE Koha -%] -[%- USE KohaDates -%] -[%- USE ItemTypes -%] -[%- USE AuthorisedValues -%] -[%- SET biblio = item.biblio -%] -[%- SET biblioitem = item.biblioitem -%] -[%- SET delimiter = Koha.CSVDelimiter() -%] -"[% biblio.title | replace('"', '""') | $raw %] -[% IF ( Koha.Preference( 'marcflavour' ) == 'UNIMARC' && biblio.author ) %]by[% END %][% biblio.author | replace('"', '""') | $raw %]"[%- delimiter | $raw -%]"[% (biblioitem.publicationyear || biblio.copyrightdate) | replace('"', '""') | $raw %]"[%- delimiter | $raw -%]"[% biblioitem.publishercode | replace('"', '""') | $raw %]"[%- delimiter | $raw -%]"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.ccode', authorised_value => item.ccode ) | replace('"', '""') | $raw %]"[%- delimiter | $raw -%]"[% item.barcode | replace('"', '""') | $raw %]"[%- delimiter | $raw -%]"[% item.itemnumber | replace('"', '""') | $raw %]"[%- delimiter | $raw -%]"[% item.enumchron | replace('"', '""') | $raw %]"[%- delimiter | $raw -%]"[% item.itemcallnumber | replace('"', '""') | $raw %]"[%- delimiter | $raw -%]"[% Branches.GetName(item.homebranch) | replace('"', '""') | $raw %]"[%- delimiter | $raw -%]"[% Branches.GetName(item.holdingbranch) | replace('"', '""') | $raw %]"[%- delimiter | $raw -%]"[% AuthorisedValues.GetDescriptionByKohaField(frameworkcode => biblio.frameworkcode, kohafield => 'items.location', authorised_value => item.location) | replace('"', '""') | $raw %]"[%- delimiter | $raw -%]"[% ItemTypes.GetDescription(item.itype) | replace('"', '""') | $raw %]"[%- delimiter | $raw -%]"[% item.stocknumber | replace('"', '""') | $raw %]"[%- delimiter | $raw -%]"[% AuthorisedValues.GetDescriptionByKohaField(frameworkcode => biblio.frameworkcode, kohafield => 'items.notforloan', authorised_value => item.notforloan) | replace('"', '""') | $raw %]"[%- delimiter | $raw -%]"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.itemlost', authorised_value => item.itemlost ) || "" | replace('"', '""') | $raw %]"[%- delimiter | $raw -%]"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.withdrawn', authorised_value => item.withdrawn ) || "" | replace('"', '""') | $raw %]"[%- delimiter | $raw -%]"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.damaged', authorised_value => item.damaged ) || "" | replace('"', '""') | $raw %]"[%- delimiter | $raw -%]"[% item.dateaccessioned | $KohaDates | $raw %]"[%- delimiter | $raw -%]"[% (item.issues || 0) | $raw %]"[%- delimiter | $raw -%]"[% item.datelastborrowed | $KohaDates | $raw %]"[%- delimiter | $raw -%]"[% IF item.checkout %][% item.checkout.date_due | $KohaDates | $raw %][% END %]" diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/csv_headers/catalogue/itemsearch.tt b/koha-tmpl/intranet-tmpl/prog/en/includes/csv_headers/catalogue/itemsearch.tt deleted file mode 100644 index c6b49eecf8..0000000000 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/csv_headers/catalogue/itemsearch.tt +++ /dev/null @@ -1,7 +0,0 @@ -[%- USE raw -%] -[%- USE Koha -%] -[%- PROCESS 'i18n.inc' -%] -[%- SET delimiter = Koha.CSVDelimiter() -%] -[%- BLOCK -%] - "[% t("Title") | html %]"[%- delimiter | $raw -%]"[% t("Publication date") | html %]"[%- delimiter | $raw -%]"[% t("Publisher") | html %]"[%- delimiter | $raw -%]"[% t("Collection") | html %]"[%- delimiter | $raw -%]"[% t("Barcode") | html %]"[%- delimiter | $raw -%]"[% t("Item number") | html %]"[%- delimiter | $raw -%]"[% t("Serial enumeration") | html %]"[%- delimiter | $raw -%]"[% t("Call number") | html %]"[%- delimiter | $raw -%]"[% t("Home library") | html %]"[%- delimiter | $raw -%]"[% t("Current library") | html %]"[%- delimiter | $raw -%]"[% t("Shelving location") | html %]"[%- delimiter | $raw -%]"[% t("Item type") | html %]"[%- delimiter | $raw -%]"[% t("Inventory number") | html %]"[%- delimiter | $raw -%]"[% t("Not for loan status") | html %]"[%- delimiter | $raw -%]"[% t("Lost status") | html %]"[%- delimiter | $raw -%]"[% t("Withdrawn status") | html %]"[%- delimiter | $raw -%]"[% t("Damaged status") | html %]"[%- delimiter | $raw %]"[% t("Date accessioned") | html %]"[%- delimiter | $raw -%]"[% t("Checkouts") | html %]"[%- delimiter | $raw -%]"[% t("Last checkout date") | html %]"[%- delimiter | $raw -%]"[% t("Due date") | html %]" -[%- END -%] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch_csv.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch_csv.tt deleted file mode 100644 index 6b58dc7b02..0000000000 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch_csv.tt +++ /dev/null @@ -1,6 +0,0 @@ -[%- INCLUDE csv_headers/catalogue/itemsearch.tt -%] -[%- INCLUDE empty_line.inc -%] -[%- FOREACH item IN results -%] - [%- INCLUDE 'catalogue/itemsearch_item.csv.inc' item = item -%] - [%- INCLUDE empty_line.inc -%] -[%- END -%] -- 2.50.1 (Apple Git-155)