Bugzilla – Attachment 191467 Details for
Bug 41620
Make itemsearch.pl use Koha::CSV for CSV generation
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 41620: Introduce Koha::CSV::ItemSearch
f2fa8c0.patch (text/plain), 13.33 KB, created by
Tomás Cohen Arazi (tcohen)
on 2026-01-14 20:09:45 UTC
(
hide
)
Description:
Bug 41620: Introduce Koha::CSV::ItemSearch
Filename:
MIME Type:
Creator:
Tomás Cohen Arazi (tcohen)
Created:
2026-01-14 20:09:45 UTC
Size:
13.33 KB
patch
obsolete
>From f2fa8c018371ef4e6fee5f62ad29b31db7cd2aae Mon Sep 17 00:00:00 2001 >From: =?UTF-8?q?Tom=C3=A1s=20Cohen=20Arazi?= <tomascohen@theke.io> >Date: Wed, 14 Jan 2026 16:54:10 -0300 >Subject: [PATCH] Bug 41620: Introduce Koha::CSV::ItemSearch > >This patch adds Koha::CSV::ItemSearch, a specialized CSV generator for >item search results with predefined headers and formatting. > >The class: >- Extends Koha::CSV with always_quote enabled for Excel compatibility >- Provides 21 predefined column headers for item search exports >- Formats Koha::Item objects into CSV-ready field arrays >- Handles authorized values, dates, and library names >- Supports MARC21 and UNIMARC title formatting > >This replaces the template-based CSV generation in itemsearch.pl, >eliminating spacing issues and providing a testable, maintainable >approach to item search CSV exports. > >Test plan: >1. Apply patch >2. Run: > $ ktd --shell > k$ prove t/db_dependent/Koha/CSV/ItemSearch.t >=> SUCCESS: Tests pass! >3. Tests cover: > - Object initialization with always_quote enabled > - Header generation (21 columns) > - Item formatting with all field types > - Authorized value lookups > - Date formatting > - Library name resolution > - CSV output to filehandle >4. Sign off :-D >--- > Koha/CSV/ItemSearch.pm | 306 +++++++++++++++++++++++++++ > t/db_dependent/Koha/CSV/ItemSearch.t | 141 ++++++++++++ > 2 files changed, 447 insertions(+) > create mode 100644 Koha/CSV/ItemSearch.pm > create mode 100755 t/db_dependent/Koha/CSV/ItemSearch.t > >diff --git a/Koha/CSV/ItemSearch.pm b/Koha/CSV/ItemSearch.pm >new file mode 100644 >index 0000000000..ad23219224 >--- /dev/null >+++ b/Koha/CSV/ItemSearch.pm >@@ -0,0 +1,306 @@ >+package Koha::CSV::ItemSearch; >+ >+# Copyright 2026 Theke Solutions >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it >+# under the terms of the GNU General Public License as published by >+# the Free Software Foundation; either version 3 of the License, or >+# (at your option) any later version. >+# >+# Koha is distributed in the hope that it will be useful, but >+# WITHOUT ANY WARRANTY; without even the implied warranty of >+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+# GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <https://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use C4::Context; >+use Koha::DateUtils qw( dt_from_string output_pref ); >+use Koha::I18N qw( __ ); >+ >+use base 'Koha::CSV'; >+ >+=head1 NAME >+ >+Koha::CSV::ItemSearch - CSV generator for item search results >+ >+=head1 SYNOPSIS >+ >+ my $csv = Koha::CSV::ItemSearch->new(); >+ >+ # Print headers >+ $csv->print_headers($fh); >+ >+ # Print item rows >+ foreach my $item (@items) { >+ $csv->print_item($fh, $item); >+ } >+ >+=head1 DESCRIPTION >+ >+Specialized CSV generator for item search results with predefined headers >+and formatting. >+ >+=cut >+ >+=head2 new >+ >+ my $csv = Koha::CSV::ItemSearch->new(); >+ >+Creates a new Koha::CSV::ItemSearch object with always_quote enabled >+for Excel compatibility. >+ >+=cut >+ >+sub new { >+ my ( $class, $params ) = @_; >+ $params->{always_quote} = 1; >+ return $class->SUPER::new($params); >+} >+ >+=head2 headers >+ >+Returns arrayref of column headers for item search CSV export. >+ >+=cut >+ >+sub headers { >+ return [ >+ __('Title'), >+ __('Publication date'), >+ __('Publisher'), >+ __('Collection'), >+ __('Barcode'), >+ __('Item number'), >+ __('Serial enumeration'), >+ __('Call number'), >+ __('Home library'), >+ __('Current library'), >+ __('Shelving location'), >+ __('Item type'), >+ __('Inventory number'), >+ __('Not for loan status'), >+ __('Lost status'), >+ __('Withdrawn status'), >+ __('Damaged status'), >+ __('Date accessioned'), >+ __('Checkouts'), >+ __('Last checkout date'), >+ __('Due date'), >+ ]; >+} >+ >+=head2 print_headers >+ >+ $csv->print_headers($fh); >+ >+Prints the CSV headers to a filehandle. >+ >+=cut >+ >+sub print_headers { >+ my ( $self, $fh ) = @_; >+ return $self->print( $fh, $self->headers ); >+} >+ >+=head2 format_item >+ >+ my $fields = $csv->format_item($item); >+ >+Formats a Koha::Item object into an arrayref of CSV fields. >+ >+=cut >+ >+sub format_item { >+ my ( $self, $item ) = @_; >+ >+ my $biblio = $item->biblio; >+ my $biblioitem = $item->biblioitem; >+ >+ return [ >+ $self->_format_title($item), >+ $biblioitem->publicationyear || $biblio->copyrightdate || '', >+ $biblioitem->publishercode || '', >+ $self->_format_collection($item), >+ $item->barcode || '', >+ $item->itemnumber || '', >+ $item->enumchron || '', >+ $item->itemcallnumber || '', >+ $self->_format_branch( $item->homebranch ), >+ $self->_format_branch( $item->holdingbranch ), >+ $self->_format_location($item), >+ $self->_format_itemtype($item), >+ $item->stocknumber || '', >+ $self->_format_notforloan($item), >+ $self->_format_itemlost($item), >+ $self->_format_withdrawn($item), >+ $self->_format_damaged($item), >+ $self->_format_date( $item->dateaccessioned ), >+ $item->issues || 0, >+ $self->_format_date( $item->datelastborrowed ), >+ $self->_format_due_date($item), >+ ]; >+} >+ >+=head2 print_item >+ >+ $csv->print_item($fh, $item); >+ >+Formats and prints a Koha::Item to a filehandle as a CSV row. >+ >+=cut >+ >+sub print_item { >+ my ( $self, $fh, $item ) = @_; >+ return $self->print( $fh, $self->format_item($item) ); >+} >+ >+# Private formatting methods >+ >+sub _format_title { >+ my ( $self, $item ) = @_; >+ my $biblio = $item->biblio; >+ my $title = $biblio->title || ''; >+ my $author = $biblio->author || ''; >+ >+ if ( C4::Context->preference('marcflavour') eq 'UNIMARC' && $author ) { >+ return "$title by $author"; >+ } >+ return $author ? "$title $author" : $title; >+} >+ >+sub _format_collection { >+ my ( $self, $item ) = @_; >+ return '' unless $item->ccode; >+ >+ require Koha::AuthorisedValues; >+ my $av = Koha::AuthorisedValues->search( >+ { >+ category => 'CCODE', >+ authorised_value => $item->ccode, >+ } >+ )->next; >+ >+ return $av ? $av->lib : $item->ccode; >+} >+ >+sub _format_branch { >+ my ( $self, $branchcode ) = @_; >+ return '' unless $branchcode; >+ >+ require Koha::Libraries; >+ my $library = Koha::Libraries->find($branchcode); >+ return $library ? $library->branchname : $branchcode; >+} >+ >+sub _format_location { >+ my ( $self, $item ) = @_; >+ return '' unless $item->location; >+ >+ require Koha::AuthorisedValues; >+ my $av = Koha::AuthorisedValues->search( >+ { >+ category => 'LOC', >+ authorised_value => $item->location, >+ } >+ )->next; >+ >+ return $av ? $av->lib : $item->location; >+} >+ >+sub _format_itemtype { >+ my ( $self, $item ) = @_; >+ return '' unless $item->itype; >+ >+ require Koha::ItemTypes; >+ my $itemtype = Koha::ItemTypes->find( $item->itype ); >+ return $itemtype ? $itemtype->description : $item->itype; >+} >+ >+sub _format_notforloan { >+ my ( $self, $item ) = @_; >+ return '' unless defined $item->notforloan; >+ >+ require Koha::AuthorisedValues; >+ my $av = Koha::AuthorisedValues->search( >+ { >+ category => 'NOT_LOAN', >+ authorised_value => $item->notforloan, >+ } >+ )->next; >+ >+ return $av ? $av->lib : $item->notforloan; >+} >+ >+sub _format_itemlost { >+ my ( $self, $item ) = @_; >+ return '' unless $item->itemlost; >+ >+ require Koha::AuthorisedValues; >+ my $av = Koha::AuthorisedValues->search( >+ { >+ category => 'LOST', >+ authorised_value => $item->itemlost, >+ } >+ )->next; >+ >+ return $av ? $av->lib : ''; >+} >+ >+sub _format_withdrawn { >+ my ( $self, $item ) = @_; >+ return '' unless $item->withdrawn; >+ >+ require Koha::AuthorisedValues; >+ my $av = Koha::AuthorisedValues->search( >+ { >+ category => 'WITHDRAWN', >+ authorised_value => $item->withdrawn, >+ } >+ )->next; >+ >+ return $av ? $av->lib : ''; >+} >+ >+sub _format_damaged { >+ my ( $self, $item ) = @_; >+ return '' unless $item->damaged; >+ >+ require Koha::AuthorisedValues; >+ my $av = Koha::AuthorisedValues->search( >+ { >+ category => 'DAMAGED', >+ authorised_value => $item->damaged, >+ } >+ )->next; >+ >+ return $av ? $av->lib : ''; >+} >+ >+sub _format_date { >+ my ( $self, $date ) = @_; >+ return '' unless $date; >+ >+ return output_pref( >+ { >+ dt => dt_from_string($date), >+ dateformat => 'iso', >+ dateonly => 1, >+ } >+ ); >+} >+ >+sub _format_due_date { >+ my ( $self, $item ) = @_; >+ my $checkout = $item->checkout; >+ return '' unless $checkout; >+ >+ return $self->_format_date( $checkout->date_due ); >+} >+ >+1; >diff --git a/t/db_dependent/Koha/CSV/ItemSearch.t b/t/db_dependent/Koha/CSV/ItemSearch.t >new file mode 100755 >index 0000000000..d350702dc8 >--- /dev/null >+++ b/t/db_dependent/Koha/CSV/ItemSearch.t >@@ -0,0 +1,141 @@ >+#!/usr/bin/perl >+ >+# Copyright 2026 Theke Solutions >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it >+# under the terms of the GNU General Public License as published by >+# the Free Software Foundation; either version 3 of the License, or >+# (at your option) any later version. >+# >+# Koha is distributed in the hope that it will be useful, but >+# WITHOUT ANY WARRANTY; without even the implied warranty of >+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+# GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <https://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use Test::More tests => 7; >+use Test::NoWarnings; >+ >+use t::lib::TestBuilder; >+use t::lib::Mocks; >+ >+BEGIN { >+ use_ok('Koha::CSV::ItemSearch'); >+} >+ >+my $schema = Koha::Database->new->schema; >+my $builder = t::lib::TestBuilder->new; >+ >+subtest 'new() tests' => sub { >+ plan tests => 2; >+ >+ my $csv = Koha::CSV::ItemSearch->new(); >+ isa_ok( $csv, 'Koha::CSV::ItemSearch', 'Object created' ); >+ is( $csv->always_quote, 1, 'always_quote is enabled for Excel compatibility' ); >+}; >+ >+subtest 'headers() tests' => sub { >+ plan tests => 2; >+ >+ my $csv = Koha::CSV::ItemSearch->new(); >+ my $headers = $csv->headers(); >+ >+ isa_ok( $headers, 'ARRAY', 'headers returns arrayref' ); >+ is( scalar @$headers, 21, 'headers has 21 columns' ); >+}; >+ >+subtest 'format_item() tests' => sub { >+ plan tests => 6; >+ >+ $schema->storage->txn_begin; >+ >+ t::lib::Mocks::mock_preference( 'CSVDelimiter', ',' ); >+ >+ # Create test data >+ my $library = $builder->build_object( { class => 'Koha::Libraries' } ); >+ my $biblio = $builder->build_sample_biblio( >+ { >+ title => 'Test Title', >+ author => 'Test Author', >+ } >+ ); >+ my $item = $builder->build_sample_item( >+ { >+ biblionumber => $biblio->biblionumber, >+ homebranch => $library->branchcode, >+ holdingbranch => $library->branchcode, >+ barcode => '12345678', >+ } >+ ); >+ >+ my $csv = Koha::CSV::ItemSearch->new(); >+ my $fields = $csv->format_item($item); >+ >+ isa_ok( $fields, 'ARRAY', 'format_item returns arrayref' ); >+ is( scalar @$fields, 21, 'format_item returns 21 fields' ); >+ >+ # Check specific fields >+ like( $fields->[0], qr/Test Title/, 'Title field contains title' ); >+ is( $fields->[4], '12345678', 'Barcode field correct' ); >+ is( $fields->[5], $item->itemnumber, 'Item number field correct' ); >+ is( $fields->[8], $library->branchname, 'Home library field correct' ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest 'print_headers() tests' => sub { >+ plan tests => 2; >+ >+ t::lib::Mocks::mock_preference( 'CSVDelimiter', ',' ); >+ >+ my $csv = Koha::CSV::ItemSearch->new(); >+ >+ my $output = ''; >+ open my $fh, '>', \$output or die "Cannot open string as file: $!"; >+ >+ my $status = $csv->print_headers($fh); >+ ok( $status, 'print_headers returns true' ); >+ >+ close $fh; >+ >+ like( $output, qr/^"Title","Publication date"/, 'Headers printed correctly' ); >+}; >+ >+subtest 'print_item() tests' => sub { >+ plan tests => 2; >+ >+ $schema->storage->txn_begin; >+ >+ t::lib::Mocks::mock_preference( 'CSVDelimiter', ',' ); >+ >+ my $library = $builder->build_object( { class => 'Koha::Libraries' } ); >+ my $biblio = $builder->build_sample_biblio( { title => 'Test Book' } ); >+ my $item = $builder->build_sample_item( >+ { >+ biblionumber => $biblio->biblionumber, >+ homebranch => $library->branchcode, >+ holdingbranch => $library->branchcode, >+ barcode => '99999999', >+ } >+ ); >+ >+ my $csv = Koha::CSV::ItemSearch->new(); >+ >+ my $output = ''; >+ open my $fh, '>', \$output or die "Cannot open string as file: $!"; >+ >+ my $status = $csv->print_item( $fh, $item ); >+ ok( $status, 'print_item returns true' ); >+ >+ close $fh; >+ >+ like( $output, qr/Test Book/, 'Item data printed correctly' ); >+ >+ $schema->storage->txn_rollback; >+}; >-- >2.50.1 (Apple Git-155) >
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 41620
: 191467 |
191468