From db7854682ee22d408f3c8b75dfecfa65ebb94fcd Mon Sep 17 00:00:00 2001 From: Pedro Amorim Date: Mon, 23 Feb 2026 11:40:23 -0100 Subject: [PATCH] Bug 37762: Circulation: Hook to checkouts/checkins Signed-off-by: Jason Robb Signed-off-by: Lisette Scheer Signed-off-by: Lucas Gass --- C4/Circulation.pm | 43 +++++++++ Koha/Checkout.pm | 14 +++ Koha/Schema/Result/Iso18626Request.pm | 46 ++++++++- Koha/Schema/Result/Issue.pm | 19 +++- .../swagger/definitions/iso18626_request.yaml | 5 + circ/circulation.pl | 11 +++ ill/ill.pl | 5 + installer/data/mysql/kohastructure.sql | 5 +- .../prog/en/modules/circ/circulation.tt | 10 ++ .../intranet-tmpl/prog/en/modules/ill/ill.tt | 1 + koha-tmpl/intranet-tmpl/prog/js/checkouts.js | 7 ++ .../prog/js/fetch/item-api-client.js | 7 ++ .../vue/components/ILL/SupplyingResource.vue | 95 +++++++++++++++++-- svc/checkouts | 13 ++- 14 files changed, 267 insertions(+), 14 deletions(-) diff --git a/C4/Circulation.pm b/C4/Circulation.pm index 6148c3152a..d55930c3dc 100644 --- a/C4/Circulation.pm +++ b/C4/Circulation.pm @@ -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', { diff --git a/Koha/Checkout.pm b/Koha/Checkout.pm index 7c95a5d2a5..1e5a5d4283 100644 --- a/Koha/Checkout.pm +++ b/Koha/Checkout.pm @@ -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 diff --git a/Koha/Schema/Result/Iso18626Request.pm b/Koha/Schema/Result/Iso18626Request.pm index 1a49f828a9..ef00aa4088 100644 --- a/Koha/Schema/Result/Iso18626Request.pm +++ b/Koha/Schema/Result/Iso18626Request.pm @@ -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 + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("uniq_issue_id", ["issue_id"]); + =head2 C =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 + +=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 diff --git a/Koha/Schema/Result/Issue.pm b/Koha/Schema/Result/Issue.pm index 1228c4df71..71b9680608 100644 --- a/Koha/Schema/Result/Issue.pm +++ b/Koha/Schema/Result/Issue.pm @@ -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 + +=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 }, diff --git a/api/v1/swagger/definitions/iso18626_request.yaml b/api/v1/swagger/definitions/iso18626_request.yaml index f80447d657..fcdcad3843 100644 --- a/api/v1/swagger/definitions/iso18626_request.yaml +++ b/api/v1/swagger/definitions/iso18626_request.yaml @@ -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 diff --git a/circ/circulation.pl b/circ/circulation.pl index 54c9ee0364..3b147d865f 100755 --- a/circ/circulation.pl +++ b/circ/circulation.pl @@ -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} ] diff --git a/ill/ill.pl b/ill/ill.pl index 7d3d3ca483..765a040418 100755 --- a/ill/ill.pl +++ b/ill/ill.pl @@ -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; diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index 59dce8ad91..460b16d5bc 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -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 */; diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt index 6d17304352..f5218f6ac0 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt @@ -217,6 +217,10 @@ [% END %] + [% IF ( SUPPLY_ILL ) %] +
  • 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.
  • + [% END %] + [% IF TOO_MANY and TOO_MANY == 'TOO_MANY_CHECKOUTS' %]
  • Too many checked out. [% current_loan_count | html %] checked out, only [% max_loans_allowed | html %] are allowed.
  • Maximum checkouts calculated from the circulation rule for [% INCLUDE circulation_rule_criteria rule=circulation_rule_TOO_MANY %]
  • @@ -405,6 +409,12 @@ [% IF ( DEBT ) %][% END %] + [% IF ( SUPPLY_ILL ) %] + + + + [% END %] + [% IF ( INVALID_DATE ) %]

    diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill.tt index ec0c6a43c4..404af85d12 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill.tt @@ -29,6 +29,7 @@ [% INCLUDE 'js-date-format.inc' %] [% Asset.js("js/vue/dist/ill.js") | $raw %] [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/js/checkouts.js b/koha-tmpl/intranet-tmpl/prog/js/checkouts.js index 0bb1f74139..00246d0cb9 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/checkouts.js +++ b/koha-tmpl/intranet-tmpl/prog/js/checkouts.js @@ -248,6 +248,13 @@ function LoadIssuesTable() { "."; } + if (oObj.iso18626_request) { + title += create_iso18626_request_link( + oObj.iso18626_request.id, + oObj.iso18626_request.status + ); + } + title += " " + " { + 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 ( + '' + + escape_str(row.id_prefix) + + escape_str(data) + + "" + ); + }, }, { 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? diff --git a/svc/checkouts b/svc/checkouts index 3d568e357d..b3cf97d1df 100755 --- a/svc/checkouts +++ b/svc/checkouts @@ -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}, @@ -308,8 +311,14 @@ while ( my $c = $sth->fetchrow_hashref() ) { firstname => $c->{firstname}, cardnumber => $c->{cardnumber}, }, - issued_today => !$c->{not_issued_today}, - recalled => $recalled, + 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} ) {