Bug 39829: Allow custom title in SIP responses

This patch adds a custom aj_field_template to the sip accounts
This allows libraries to specify a template for the title (AJ) field in sip
messages

The template allows using item and biblio fields (item provides access to biblio. but we pass
both in order to makes the templates simpler

To test:
1 - Install patches
2 - updatedatabase, yarn build, restart_all
3 - Setup a sip account, KTD already has a sip patron, term1, you can use that
    don't add the aj_field_template yet
4 - Find or create a biblio with 245a and 245b like 245$aTitle : $bSubtitle and add item with TESTBARCODE
5 - From the command line:
    perl misc/sip_cli_emulator.pl -su term1 -sp term1 -l CPL -t CR -a localhost -p 6001 -m item_information --item TESTBARCODE
6 - Confirm AJ field shows "Title :" as expected
7 - Edit sip account, set aj_field_template to:
    [% biblio.title %] [% biblio.subtitle %] [% item.barcode %]
8 - Repeat the sip test
9 - Confirm you get the response like:
    Title : Subtitle TESTBARCODE
10 - Sign off!

Signed-off-by: Bob Bennhoff <bbennhoff@clicweb.org>
Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
This commit is contained in:
Nick Clemens 2026-08-15 15:45:08 +00:00 committed by Pedro Amorim
parent 6a040f8003
commit f1ca516b60
No known key found for this signature in database
GPG Key ID: 06B75BA7F3894D55
9 changed files with 144 additions and 24 deletions

View File

@ -97,6 +97,7 @@ sub add_field {
);
$value = '';
}
$value =~ s/\r\n|\r|\n/ /g; # CR or LF terminates a sip message
# Protect against them in sip text fields
@ -140,7 +141,7 @@ sub maybe_add {
}
return ( defined($value) && length($value) )
? add_field( $fid, $value )
? add_field( $fid, $value, $server )
: '';
}

View File

@ -24,7 +24,8 @@ use Koha::Patrons;
use Koha::Patron::Attributes;
use Koha::Plugins;
use Koha::Items;
use Koha::DateUtils qw( output_pref );
use Koha::DateUtils qw( output_pref );
use Koha::TemplateUtils qw( process_tt );
use UNIVERSAL::can;
@ -625,10 +626,10 @@ sub handle_checkout {
$resp .= timestamp;
# Now for the variable fields
$resp .= add_field( FID_INST_ID, $inst, $server );
$resp .= add_field( FID_PATRON_ID, $patron_id, $server );
$resp .= add_field( FID_ITEM_ID, $item_id, $server );
$resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
$resp .= add_field( FID_INST_ID, $inst, $server );
$resp .= add_field( FID_PATRON_ID, $patron_id, $server );
$resp .= add_field( FID_ITEM_ID, $item_id, $server );
$resp .= add_field( FID_TITLE_ID, _format_title( { item => $item, server => $server } ) );
if ( $item->due_date ) {
my $due_date =
$account->{format_due_date}
@ -663,7 +664,7 @@ sub handle_checkout {
# If the item is valid, provide the title, otherwise
# leave it blank
$resp .= add_field( FID_TITLE_ID, $item ? $item->title_id : '', $server );
$resp .= add_field( FID_TITLE_ID, $item ? _format_title( { item => $item, server => $server } ) : "" );
# Due date is required. Since it didn't get checked out,
# it's not due, so leave the date blank
@ -763,7 +764,7 @@ sub handle_checkin {
if ($item) {
$resp .= add_field( FID_PERM_LOCN, $item->permanent_location, $server );
$resp .= maybe_add( FID_TITLE_ID, $item->title_id, $server );
$resp .= maybe_add( FID_TITLE_ID, _format_title( { item => $item, server => $server } ) );
$resp .= $item->build_additional_item_fields_string($server);
} else {
$resp .= add_field( FID_PERM_LOCN, "", $server );
@ -1343,8 +1344,8 @@ sub handle_item_information {
$resp .= maybe_add( FID_SCREEN_MSG, "Item is damaged", $server );
}
$resp .= add_field( FID_ITEM_ID, $item->id, $server );
$resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
$resp .= add_field( FID_ITEM_ID, $item->id, $server );
$resp .= add_field( FID_TITLE_ID, _format_title( { item => $item, server => $server } ) );
$resp .= maybe_add( FID_MEDIA_TYPE, $item->sip_media_type, $server );
$resp .= maybe_add( FID_PERM_LOCN, $item->permanent_location, $server );
@ -1429,8 +1430,8 @@ sub handle_item_status_update {
$resp .= $status->ok ? '1' : '0';
$resp .= timestamp;
$resp .= add_field( FID_ITEM_ID, $item->id, $server );
$resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
$resp .= add_field( FID_ITEM_ID, $item->id, $server );
$resp .= add_field( FID_TITLE_ID, _format_title( { item => $item, server => $server } ) );
$resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties, $server );
}
@ -1551,10 +1552,10 @@ sub handle_hold {
( $status->expiration_date )
and $resp .= maybe_add( FID_EXPIRATION, timestamp( $status->expiration_date ), $server );
$resp .= maybe_add( FID_QUEUE_POS, $status->queue_position, $server );
$resp .= maybe_add( FID_PICKUP_LOCN, $status->pickup_location, $server );
$resp .= maybe_add( FID_ITEM_ID, $status->item->id, $server );
$resp .= maybe_add( FID_TITLE_ID, $status->item->title_id, $server );
$resp .= maybe_add( FID_QUEUE_POS, $status->queue_position, $server );
$resp .= maybe_add( FID_PICKUP_LOCN, $status->pickup_location, $server );
$resp .= maybe_add( FID_ITEM_ID, $status->item->id, $server );
$resp .= maybe_add( FID_TITLE_ID, _format_title( { item => $status->item, server => $server } ), $server );
} else {
# Not ok. still need required fields
@ -1618,9 +1619,9 @@ sub handle_renew {
}
$resp .= sipbool( desensitize( { status => $status, patron => $patron, server => $server } ) );
$resp .= timestamp;
$resp .= add_field( FID_PATRON_ID, $patron->id, $server );
$resp .= add_field( FID_ITEM_ID, $item->id, $server );
$resp .= add_field( FID_TITLE_ID, $item->title_id, $server );
$resp .= add_field( FID_PATRON_ID, $patron->id, $server );
$resp .= add_field( FID_ITEM_ID, $item->id, $server );
$resp .= add_field( FID_TITLE_ID, _format_title( { item => $item, server => $server } ) );
if ( $item->due_date ) {
$resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ), $server );
} else {
@ -1641,10 +1642,13 @@ sub handle_renew {
# If we found the patron or the item, the return the ILS
# information, otherwise echo back the information we received
# from the terminal
$resp .= add_field( FID_PATRON_ID, $patron ? $patron->id : $patron_id, $server );
$resp .= add_field( FID_ITEM_ID, $item ? $item->id : $item_id, $server );
$resp .= add_field( FID_TITLE_ID, $item ? $item->title_id : $title_id, $server );
$resp .= add_field( FID_DUE_DATE, '', $server );
$resp .= add_field( FID_PATRON_ID, $patron ? $patron->id : $patron_id, $server );
$resp .= add_field( FID_ITEM_ID, $item ? $item->id : $item_id, $server );
$resp .= add_field(
FID_TITLE_ID, $item ? _format_title( { item => $item, server => $server } ) : $title_id,
$server
);
$resp .= add_field( FID_DUE_DATE, '', $server );
}
if ( $status->fee_amount ) {
@ -1900,6 +1904,19 @@ sub desensitize {
return 1;
}
sub _format_title {
my ($params) = @_;
my $server = $params->{server};
my $item = $params->{item};
if ( defined $server->{account}->{aj_field_template} && $server->{account}->{aj_field_template} ne '' ) {
return process_tt(
$server->{account}->{aj_field_template},
{ biblio => $item->{object}->biblio, item => $item->{object} }
);
} else {
return $item->title_id;
}
}
1;
__END__

View File

@ -13,6 +13,11 @@ properties:
type:
- string
- 'null'
aj_field_template:
description: aj field template
type:
- string
- 'null'
allow_additional_materials_checkout:
description: allow additional materials checkout
type:

View File

@ -0,0 +1,25 @@
use Modern::Perl;
use Koha::Installer::Output qw(say_warning say_success say_info);
return {
bug_number => "39829",
description => "Add SIP aj_field_template to allow customizing title info",
up => sub {
my ($args) = @_;
my ( $dbh, $out ) = @$args{qw(dbh out)};
unless ( column_exists( 'sip_accounts', 'aj_field_template' ) ) {
$dbh->do(
q{
ALTER TABLE sip_accounts
ADD COLUMN
`aj_field_template` text NULL
AFTER `ae_field_template`
}
);
say $out "Added column 'sip_accounts.aj_field_template'";
} else {
say $out "Column already existed: 'sip_accounts.aj_field_template'";
}
},
};

View File

@ -6333,6 +6333,7 @@ CREATE TABLE `sip_accounts` (
`sip_account_id` int(11) NOT NULL AUTO_INCREMENT,
`sip_institution_id` int(11) NOT NULL COMMENT 'Foreign key to sip_institutions.sip_institution_id',
`ae_field_template` text DEFAULT NULL,
`aj_field_template` text DEFAULT NULL,
`allow_additional_materials_checkout` tinyint(1) DEFAULT NULL,
`allow_empty_passwords` tinyint(1) DEFAULT NULL,
`allow_fields` varchar(255) DEFAULT NULL,

View File

@ -957,6 +957,19 @@ export default {
"(Hold patron name) is the name of the patron who has placed a hold on the item being checked in. Use Template Toolkit syntax - the patron fields (Koha::Patron) for the hold patron and item fields are available."
),
},
{
name: "aj_field_template",
type: "textarea",
label: "AJ " + __("field template"),
placeholder: "[% biblio.title %]",
hideIn: ["List"],
group: "templates",
toolTip:
"AJ " +
__(
"(Title) is the item's title as sent to the SIP2 client. Use Template Toolkit syntax - the item and biblio fields (Koha::Item, Koha::Biblio) are available."
),
},
];
const baseResource = useBaseResource({

View File

@ -3,6 +3,7 @@ import { mount } from "@cypress/vue";
function get_account() {
return {
ae_field_template: "",
aj_field_template: "",
allow_additional_materials_checkout: false,
allow_empty_passwords: false,
allow_fields: null,

View File

@ -260,6 +260,7 @@ subtest 'template fields accept long text' => sub {
class => 'Koha::SIP2::Accounts',
value => {
ae_field_template => $template,
aj_field_template => $template,
av_field_template => $template,
da_field_template => $template,
},
@ -268,6 +269,7 @@ subtest 'template fields accept long text' => sub {
my $stored = $account->get_from_storage;
is( $stored->ae_field_template, $template, 'ae_field_template can store large templates' );
is( $stored->aj_field_template, $template, 'aj_field_template can store large templates' );
is( $stored->av_field_template, $template, 'av_field_template can store large templates' );
is( $stored->da_field_template, $template, 'da_field_template can store large templates' );

View File

@ -22,7 +22,7 @@
use Modern::Perl;
use Test::NoWarnings;
use Test::More tests => 23;
use Test::More tests => 24;
use Test::Exception;
use Test::MockObject;
use Test::MockModule;
@ -2009,6 +2009,61 @@ subtest 'Test convert_nonprinting_characters' => sub {
$schema->storage->txn_rollback;
};
subtest 'Test _format_title' => sub {
plan tests => 2;
my $schema = Koha::Database->new->schema;
$schema->storage->txn_begin;
my $builder = t::lib::TestBuilder->new();
my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode};
my ( $response, $findpatron );
my $mocks = create_mocks( \$response, \$findpatron, \$branchcode );
my $mockILS = $mocks->{ils};
my $server = { ils => $mockILS, account => {} };
my $patron = $builder->build(
{
source => 'Borrower',
value => {
password => hash_password(PATRON_PW),
branchcode => $branchcode,
},
}
);
my $biblio = $builder->build_sample_biblio(
{
title => 'Title',
}
);
$biblio->subtitle("Subtitle")->store;
my $item = $builder->build_sample_item(
{
damaged => 0,
withdrawn => 0,
itemlost => 0,
restricted => 0,
homebranch => $branchcode,
holdingbranch => $branchcode,
biblionumber => $biblio->biblionumber
}
);
my $sip_item = C4::SIP::ILS::Item->new( $item->barcode );
delete $server->{account}->{aj_field_template};
is(
C4::SIP::Sip::MsgType::_format_title( { item => $sip_item, server => $server } ), "Title",
"When aj_field_template undefined, we get the simple title"
);
$server->{account}->{aj_field_template} = "[% biblio.title %] [% biblio.subtitle %] [% item.barcode %]";
is(
C4::SIP::Sip::MsgType::_format_title( { item => $sip_item, server => $server } ),
"Title Subtitle " . $item->barcode, "When aj_field_template set, we get expected fields"
);
$schema->storage->txn_rollback;
};
# Helper routines
sub create_mocks {