Bug 37762: Circulation: Hook to checkouts/checkins

Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
Pedro Amorim 2026-02-23 11:40:23 -01:00 committed by Lucas Gass
parent 950c9ccb96
commit db7854682e
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
14 changed files with 267 additions and 14 deletions

View File

@ -105,6 +105,7 @@ use Koha::DateUtils qw( dt_from_string );
use Koha::Calendar;
use Koha::Checkouts;
use Koha::ILL::Requests;
use Koha::ILL::ISO18626::Requests;
use Koha::Items;
use Koha::Patrons;
use Koha::Patron::Debarments qw( DelUniqueDebarment AddUniqueDebarment );
@ -1405,6 +1406,25 @@ sub CanBookBeIssued {
}
}
if ( C4::Context->preference("ILLModule") ) {
my $is_ill_partner = ( $patron->categorycode eq C4::Context->preference("ILLPartnerCode") );
my $iso18626_hold = Koha::Holds->find(
{
itemnumber => $item_object->itemnumber,
borrowernumber => $patron->borrowernumber
}
);
if ($iso18626_hold) {
my $iso18626_request = $iso18626_hold->iso18626_request;
if ( $is_ill_partner && $iso18626_hold && $iso18626_request ) {
$needsconfirmation{SUPPLY_ILL} = 1;
$needsconfirmation{'iso18626_payload_supplyill'} = $iso18626_request->iso18626_request_id;
}
}
}
return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages );
}
@ -1646,6 +1666,7 @@ sub AddIssue {
my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout};
my $auto_renew = $params && $params->{auto_renew};
my $cancel_recall = $params && $params->{cancel_recall};
my $iso18626_payload = $params && $params->{iso18626_payload};
my $recall_id = $params && $params->{recall_id};
my $confirmations = $params->{confirmations};
my $forced = $params->{forced};
@ -2017,6 +2038,19 @@ sub AddIssue {
);
}
my $iso18626_request =
Koha::ILL::ISO18626::Requests->find( $params->{iso18626_payload}->{iso18626_payload_supplyill} );
if ($iso18626_request) {
$iso18626_request->issue_id( $issue->issue_id )->store();
$iso18626_request->progress_request(
'supplyingAgency',
{
'messageInfoNote' => $params->{iso18626_payload}->{iso18626_messageInfo_note},
'status' => 'Loaned'
}
);
}
Koha::Plugins->call(
'after_circ_action',
{
@ -2311,6 +2345,7 @@ sub AddReturn {
$return_date //= dt_from_string();
my $messages;
my $patron;
my $iso18626_request;
my $doreturn = 1;
my $validTransfer = 1;
my $stat_type = 'return';
@ -2332,6 +2367,7 @@ sub AddReturn {
"Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existent borrowernumber '"
. $issue->borrowernumber . "'\n"
. Dumper( $issue->unblessed ) . "\n";
$iso18626_request = $issue->iso18626_request;
} else {
$messages->{'NotIssued'} = $barcode;
$item->onloan(undef)->store( { skip_record_index => 1, skip_holds_queue => 1 } ) if defined $item->onloan;
@ -2821,6 +2857,13 @@ sub AddReturn {
if ( $doreturn and $issue ) {
my $checkin = Koha::Old::Checkouts->find( $issue->id );
if ($iso18626_request) {
$iso18626_request->progress_request(
'supplyingAgency',
{ 'status' => 'LoanCompleted' }
);
}
Koha::Plugins->call(
'after_circ_action',
{

View File

@ -29,6 +29,7 @@ use Koha::Checkouts::Renewals;
use Koha::Checkouts::ReturnClaims;
use Koha::Database;
use Koha::DateUtils qw( dt_from_string );
use Koha::ILL::ISO18626::Request;
use Koha::Items;
use Koha::Libraries;
@ -136,6 +137,19 @@ sub patron {
return Koha::Patron->_new_from_dbic($patron_rs);
}
=head3 iso18626_request
Returns the related Koha::ILL::ISO18626::Request object for this checkout
=cut
sub iso18626_request {
my ($self) = @_;
my $rs = $self->_result->iso18626_request;
return unless $rs;
return Koha::ILL::ISO18626::Request->_new_from_dbic($rs);
}
=head3 issuer
my $issuer = $checkout->issuer

View File

@ -108,6 +108,14 @@ ISO18626 Requesting Agency action that requires a manual response (yes or no)
ID of the hold related to this ISO18626 request
=head2 issue_id
data_type: 'integer'
is_foreign_key: 1
is_nullable: 1
ID of the checkout related to this ISO18626 request
=cut
__PACKAGE__->add_columns(
@ -181,6 +189,8 @@ __PACKAGE__->add_columns(
},
"hold_id",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
"issue_id",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
);
=head1 PRIMARY KEY
@ -197,6 +207,18 @@ __PACKAGE__->set_primary_key("iso18626_request_id");
=head1 UNIQUE CONSTRAINTS
=head2 C<uniq_issue_id>
=over 4
=item * L</issue_id>
=back
=cut
__PACKAGE__->add_unique_constraint("uniq_issue_id", ["issue_id"]);
=head2 C<uniq_reserve_id>
=over 4
@ -263,9 +285,29 @@ __PACKAGE__->belongs_to(
{ is_deferrable => 1, on_delete => "RESTRICT", on_update => "CASCADE" },
);
=head2 issue
# Created by DBIx::Class::Schema::Loader v0.07051 @ 2026-02-24 13:34:37
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:7KqpCOYXvp6skCK/lTer0g
Type: belongs_to
Related object: L<Koha::Schema::Result::Issue>
=cut
__PACKAGE__->belongs_to(
"issue",
"Koha::Schema::Result::Issue",
{ issue_id => "issue_id" },
{
is_deferrable => 1,
join_type => "LEFT",
on_delete => "SET NULL",
on_update => "CASCADE",
},
);
# Created by DBIx::Class::Schema::Loader v0.07051 @ 2026-02-24 13:40:32
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:IYorMR5/Rcx1W9dZv2WVSw
=head2 koha_object_class

View File

@ -330,6 +330,21 @@ __PACKAGE__->belongs_to(
{ is_deferrable => 1, on_delete => "RESTRICT", on_update => "CASCADE" },
);
=head2 iso18626_request
Type: might_have
Related object: L<Koha::Schema::Result::Iso18626Request>
=cut
__PACKAGE__->might_have(
"iso18626_request",
"Koha::Schema::Result::Iso18626Request",
{ "foreign.issue_id" => "self.issue_id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
=head2 issuer
Type: belongs_to
@ -366,8 +381,8 @@ __PACKAGE__->belongs_to(
);
# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-09-05 20:52:03
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:CHRrFP4okgRBpRmN5DG5iw
# Created by DBIx::Class::Schema::Loader v0.07051 @ 2026-02-24 13:37:30
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:A2+qWGBNGZgyi3aAVH5/wQ
__PACKAGE__->add_columns(
'+auto_renew' => { is_boolean => 1 },

View File

@ -39,6 +39,11 @@ properties:
requestingAgencyRequestId:
type: string
description: Requesting agency request ID or number
issue_id:
type:
- integer
- "null"
description: Associated checkout internal identifier
iso18626_requesting_agency_id:
type: integer
description: Associated ISO18626 requesting agency internal identifier

View File

@ -191,6 +191,16 @@ my $recall_id = $query->param('recall_id');
my $debt_confirmed = $query->param('debt_confirmed') || 0; # Don't show the debt error dialog twice
my $charges = $query->param('charges') || q{};
my @allowed_iso18626_fields = qw(
iso18626_payload_supplyill
iso18626_messageInfo_note
);
my %iso18626_payload = map {
my $val = $query->param($_);
defined $val ? ( $_ => $val ) : ()
} @allowed_iso18626_fields;
# Check if stickyduedate is turned off
if (@$barcodes) {
@ -597,6 +607,7 @@ if ( @$barcodes && $op eq 'cud-checkout' ) {
auto_renew => $session->param('auto_renew'),
switch_onsite_checkout => $switch_onsite_checkout,
cancel_recall => $cancel_recall,
iso18626_payload => \%iso18626_payload,
recall_id => $recall_id,
confirmations => [ grep { /^[A-Z_]+$/ } keys %{$needsconfirmation} ],
forced => [ keys %{$issuingimpossible} ]

View File

@ -33,4 +33,9 @@ my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
}
);
my $userenv = C4::Context->userenv;
$template->param(
userenv => $userenv,
);
output_html_with_http_headers $input, $cookie, $template->output;

View File

@ -3821,10 +3821,13 @@ CREATE TABLE `iso18626_requests` (
`service_type` enum('Copy','Loan','CopyOrLoan') NOT NULL COMMENT 'ISO18626 service type',
`pending_requesting_agency_action` enum('Cancel','Renew') DEFAULT NULL COMMENT 'ISO18626 Requesting Agency action that requires a manual response (yes or no)',
`hold_id` int(11) DEFAULT NULL COMMENT 'ID of the hold related to this ISO18626 request',
>>>>>>> 16e751e7126 (Bug 37762: Circulation: Hook to holds)
`issue_id` int(11) DEFAULT NULL COMMENT 'ID of the checkout related to this ISO18626 request',
PRIMARY KEY (`iso18626_request_id`),
UNIQUE KEY `uniq_reserve_id` (`hold_id`),
UNIQUE KEY `uniq_issue_id` (`issue_id`),
KEY `iso18626_rafk` (`iso18626_requesting_agency_id`),
CONSTRAINT `uniq_reserve_id` FOREIGN KEY (`hold_id`) REFERENCES `reserves` (`reserve_id`) ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT `uniq_issue_id` FOREIGN KEY (`issue_id`) REFERENCES `issues` (`issue_id`) ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT `iso18626_rafk` FOREIGN KEY (`iso18626_requesting_agency_id`) REFERENCES `iso18626_requesting_agencies` (`iso18626_requesting_agency_id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

View File

@ -217,6 +217,10 @@
</li>
[% END %]
[% IF ( SUPPLY_ILL ) %]
<li class="needsconfirm">You're checking out a supply ILL to an ILL partner. The corresponding supply ILL will be marked as 'Loaned'. Please enter the required information. </li>
[% END %]
[% IF TOO_MANY and TOO_MANY == 'TOO_MANY_CHECKOUTS' %]
<li class="needsconfirm too_many_checkouts">Too many checked out. [% current_loan_count | html %] checked out, only [% max_loans_allowed | html %] are allowed.</li>
<li class="needsconfirm too_many_checkouts2"> Maximum checkouts calculated from the circulation rule for [% INCLUDE circulation_rule_criteria rule=circulation_rule_TOO_MANY %] </li>
@ -405,6 +409,12 @@
[% IF ( DEBT ) %]<input type="hidden" name="debt_confirmed" value="1" />[% END %]
[% IF ( SUPPLY_ILL ) %]
<label for="iso18626_messageInfo_note">Enter ISO18626 message here</label>
<input type="text" name="iso18626_messageInfo_note" value="" />
<input type="hidden" name="iso18626_payload_supplyill" value="[% iso18626_payload_supplyill | html %]" />
[% END %]
[% IF ( INVALID_DATE ) %]
<p>
<input type="text" size="20" id="confirm_duedatespec" name="duedatespec" value="[% duedatespec | html %]" class="flatpickr" data-flatpickr-enable-time="true" data-flatpickr-on-close-focus="#barcode" />

View File

@ -29,6 +29,7 @@
[% INCLUDE 'js-date-format.inc' %]
<script nonce="[% Koha.CSPNonce | $raw %]">
const supplying_ill_table_settings = [% TablesSettings.GetTableSettings( 'illrequests', 'ill-requests', 'ill-requests', 'json' ) | $raw %];
const userenv = [% To.json(userenv) | $raw %];
</script>
[% Asset.js("js/vue/dist/ill.js") | $raw %]
[% END %]

View File

@ -248,6 +248,13 @@ function LoadIssuesTable() {
".</span>";
}
if (oObj.iso18626_request) {
title += create_iso18626_request_link(
oObj.iso18626_request.id,
oObj.iso18626_request.status
);
}
title +=
" " +
"<a href='/cgi-bin/koha/catalogue/moredetail.pl?biblionumber=" +

View File

@ -14,6 +14,13 @@ export class ItemAPIClient {
params,
headers,
}),
get: id =>
this.httpClient.get({
endpoint: "items/" + id,
headers: {
"x-koha-embed": "+strings",
},
}),
};
}

View File

@ -86,17 +86,18 @@ export default {
},
{
id: "Loaned",
confirm_message: $__(
"The item is currently on loan to the requesting library for this request"
),
button_label: $__("Mark as loaned"),
icon: "fa-box",
dont_show: iso18626_request => true, //This is handled by Koha check out
// confirm_message: $__(
// "The item is currently on loan to the requesting library for this request"
// ),
// button_label: $__("Mark as loaned"),
// icon: "fa-box",
next_actions: [
"Overdue",
"LoanCompleted",
"CompletedWithoutReturn",
], //[ 'Recalled', 'HoldReturn' ]
action_inputs: [],
// action_inputs: [],
},
{
id: "Overdue",
@ -639,6 +640,32 @@ export default {
});
}
if (resource?.hold?.item_id) {
show_buttons.push({
cssClass: "btn btn-primary",
title: "Mark as loaned (Checkout)",
icon: "fa-box",
onClick: () => {
if (resource.hold.item_id) {
getItem(resource.hold.item_id).then(
result => {
const item_barcode = result.external_id;
performCheckout({
borrowernumber:
resource.requesting_agency
.patron_id,
branch: userenv?.branch,
barcode: item_barcode,
supplyill: resource.iso18626_request_id,
});
},
error => {}
);
}
},
});
}
if (resource.status == "RequestReceived") {
show_buttons.push({
cssClass: "btn btn-primary",
@ -685,6 +712,32 @@ export default {
};
};
const performCheckout = params => {
const url = "/cgi-bin/koha/circ/circulation.pl";
const csrfMeta = document.querySelector('meta[name="csrf-token"]');
params.csrf_token = csrfMeta
? csrfMeta.getAttribute("content")
: "";
params.op = "cud-checkout";
const form = document.createElement("form");
form.method = "POST";
form.action = url;
for (const key in params) {
if (params.hasOwnProperty(key)) {
const hiddenField = document.createElement("input");
hiddenField.type = "hidden";
hiddenField.name = key;
hiddenField.value = params[key];
form.appendChild(hiddenField);
}
}
document.body.appendChild(form);
form.submit();
};
const baseResource = useBaseResource({
resourceName: "iso18626_request",
nameAttr: "iso18626_request_id",
@ -784,6 +837,13 @@ export default {
hideIn: ["List", "Form"],
group: $__("Request details"),
},
{
name: "issue_id",
label: $__("Checkout"),
type: "boolean",
hideIn: ["List", "Form"],
group: $__("Request details"),
},
{
group: $__("ISO18626 Messages"),
name: "messages",
@ -831,7 +891,18 @@ export default {
is_hidden: 0,
cannot_be_toggled: 0,
cannot_be_modified: 0,
columnname: "requestingAgencyId",
columnname: "requesting_agency",
render: function (data, type, row, meta) {
return (
'<a target="_blank" href="/cgi-bin/koha/ill/ill-requests.pl?' +
"op=illview&amp;illrequest_id=" +
encodeURIComponent(data) +
'">' +
escape_str(row.id_prefix) +
escape_str(data) +
"</a>"
);
},
},
{
is_hidden: 0,
@ -885,6 +956,16 @@ export default {
},
};
const getItem = async item_id => {
const client = APIClient.item;
return await client.items.get(item_id).then(
result => {
return result;
},
error => {}
);
};
const afterResourceFetch = (componentData, resource, caller) => {
if (caller === "show") {
//TODO: Use dateformat sys pref?

View File

@ -31,6 +31,7 @@ use Koha::AuthorisedValues;
use Koha::DateUtils qw( dt_from_string output_pref );
use Koha::Items;
use Koha::ItemTypes;
use Koha::ILL::ISO18626::Requests;
my $input = CGI->new;
@ -242,6 +243,8 @@ while ( my $c = $sth->fetchrow_hashref() ) {
}
}
my $iso18626_request = Koha::ILL::ISO18626::Requests->get_request_by_issue_id( $c->{issue_id} );
my $checkout = {
DT_RowId => $c->{itemnumber} . '-' . $c->{borrowernumber},
title => $c->{title},
@ -310,6 +313,12 @@ while ( my $c = $sth->fetchrow_hashref() ) {
},
issued_today => !$c->{not_issued_today},
recalled => $recalled,
iso18626_request => $iso18626_request
? {
id => $iso18626_request->iso18626_request_id,
status => $iso18626_request->status,
}
: undef,
};
if ( $c->{not_issued_today} ) {