From 716b90e4000a1c5730b9db9753debbc94093b24b Mon Sep 17 00:00:00 2001 From: Lucas Gass Date: Wed, 22 Oct 2025 19:13:07 +0000 Subject: [PATCH] Bug 34069: Implement tool for restoring deleted patrons --- Koha/Old/Patron.pm | 39 +++ .../tools/restore_deleted_borrowers.tt | 241 ++++++++++++++++++ .../prog/en/modules/tools/tools-home.tt | 5 + tools/restore_deleted_borrowers.pl | 154 +++++++++++ 4 files changed, 439 insertions(+) create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/tools/restore_deleted_borrowers.tt create mode 100755 tools/restore_deleted_borrowers.pl diff --git a/Koha/Old/Patron.pm b/Koha/Old/Patron.pm index 6e9b899cc6a..88ef74f0057 100644 --- a/Koha/Old/Patron.pm +++ b/Koha/Old/Patron.pm @@ -29,6 +29,45 @@ Koha::Old::Patron - Koha Old::Patron Object class =cut +=head3 restore_deleted_borrower + + my $patron = $old_patron->restore_deleted_borrower; + +Restores a deleted patron from the deletedborrowers table back to the borrowers table. +This only restores the patron account record itself, not any associated historical data. +The patron will retain their original borrowernumber. + +Returns the restored Koha::Patron object on success. +Throws an exception on failure. + +=cut + +sub restore_deleted_borrower { + my ($self) = @_; + + my $schema = Koha::Database->new->schema; + my $restored_patron; + + $schema->txn_do( + sub { + # Retrive all the data about this patron from deleteborrowers table + my $patron_data = $self->unblessed; + + # Create the Koha::Patron object + my $patron = Koha::Patron->new($patron_data); + + # Create the "new" patron using SUPER::store to bypass any Koha::Patron->store() and restore the patron as it was + $restored_patron = $patron->SUPER::store(); + + # Delete the entry from deletedborrowers + Koha::Old::Patrons->search( { borrowernumber => $patron_data->{borrowernumber} } )->delete; + + } + ); + + return $restored_patron; +} + =head2 Internal methods =head3 _type diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/restore_deleted_borrowers.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/restore_deleted_borrowers.tt new file mode 100644 index 00000000000..f3b70e98af1 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/restore_deleted_borrowers.tt @@ -0,0 +1,241 @@ +[% USE raw %] +[% USE Asset %] +[% SET footerjs = 1 %] +[% USE Branches %] +[% USE Categories %] +[% USE KohaDates %] +[% PROCESS 'html_helpers.inc' %] +[% PROCESS 'i18n.inc' %] +[% INCLUDE 'doc-head-open.inc' %] +[% FILTER collapse %] + [% t("Restore deleted patrons") | html %] › [% t("Tools") | html %] › [% t("Koha") | html %] +[% END %] +[% INCLUDE 'doc-head-close.inc' %] + + + +[% WRAPPER 'header.inc' %] + [% INCLUDE 'patron-search-header.inc' %] +[% END %] +[% WRAPPER 'sub-header.inc' %] + [% WRAPPER breadcrumbs %] + [% WRAPPER breadcrumb_item %] + Tools + [% END %] + [% IF ( view == 'restored' ) %] + [% WRAPPER breadcrumb_item %] + Restore deleted patrons + [% END %] + [% WRAPPER breadcrumb_item bc_active=1 %] + Restored patrons + [% END %] + [% ELSE %] + [% WRAPPER breadcrumb_item bc_active=1 %] + Restore deleted patrons + [% END %] + [% END %] + [% END #/ WRAPPER breadcrumbs %] +[% END #/ WRAPPER sub-header.inc %] + +
+
+
+
+ [% IF view == "search" %] +

Restore deleted patrons

+ +
+
+
+ Search for deleted patrons: +
    +
  1. + + +
  2. +
  3. + + +
  4. +
  5. + + +
  6. +
  7. + + +
  8. +
  9. + + +
  10. +
  11. + + +
  12. +
+
+
+ + +
+
+
+ [% ELSIF view == "results" %] +

Restore deleted patrons

+ + [% IF deleted_patrons.size > 0 %] +
+

Deleted patrons found: [% deleted_patrons.size | html %]

+ +
+ [% INCLUDE 'csrf-token.inc' %] + + + + + + + + + + + + + + + + + [% FOREACH patron IN deleted_patrons %] + + + + + + + + + + + [% END %] + +
Card numberBorrowernumberSurnameFirst nameCategoryLibraryDate deleted
+ + [% patron.cardnumber | html %][% patron.borrowernumber | html %][% patron.surname | html %][% patron.firstname | html %][% patron.category.description | html %][% patron.library.branchname | html %][% patron.updated_on | $KohaDates %]
+ +
+ +
+
+
+ [% ELSE %] +
No deleted patrons found matching your search criteria.
+
+ New search +
+ [% END %] + [% ELSIF view == "restored" %] +

Patrons restored

+ + [% IF restored_patrons.size > 0 %] +
+

Successfully restored [% restored_patrons.size | html %] patron(s).

+
+ +
+ + + + + + + + + + + + + [% FOREACH patron IN restored_patrons %] + + + + + + + + + [% END %] + +
Card numberBorrowernumberNameCategoryLibraryActions
[% patron.cardnumber | html %][% patron.borrowernumber | html %][% patron.firstname | html %] [% patron.surname | html %][% patron.category.description | html %][% patron.library.branchname | html %] + View patron +
+
+ [% END %] + + [% IF errors.size > 0 %] +
+

Errors occurred:

+
    + [% FOREACH error IN errors %] +
  • [% error | html %]
  • + [% END %] +
+
+ [% END %] + +
+ Restore more patrons +
+ [% END %] +
+
+ + +
+ +
+ +
+ +
+ + +[% MACRO jsinclude BLOCK %] + [% Asset.js("js/tools-menu.js") | $raw %] + [% INCLUDE 'datatables.inc' %] + +[% END %] + +[% INCLUDE 'intranet-bottom.inc' %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt index 6f12d5fc69a..9e092f43140 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt @@ -43,6 +43,11 @@
Manage patron clubs
[% END %] + [% IF (CAN_user_tools_restore_deleted_borrowers) %] +
Restore deleted patrons
+
Restore deleted patrons from the deletedborrowers table
+ [% END %] + [% IF ( CAN_user_tools_moderate_comments ) %]
Comments diff --git a/tools/restore_deleted_borrowers.pl b/tools/restore_deleted_borrowers.pl new file mode 100755 index 00000000000..1a8e1b675f6 --- /dev/null +++ b/tools/restore_deleted_borrowers.pl @@ -0,0 +1,154 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Copyright 2024 Koha Development Team +# +# 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 +# + +use Modern::Perl; +use CGI qw ( -utf8 ); +use Try::Tiny; +use Scalar::Util qw( blessed ); + +use C4::Auth qw( get_template_and_user ); +use C4::Output qw( output_html_with_http_headers ); +use C4::Context; + +use Koha::Old::Patrons; +use Koha::Patron::Categories; +use Koha::Libraries; + +my $input = CGI->new; +my $op = $input->param('op') || 'search'; + +my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "tools/restore_deleted_borrowers.tt", + query => $input, + type => "intranet", + flagsrequired => { tools => 'restore_deleted_patrons' }, + } +); + +if ( $op eq 'search' ) { + + # Get search parameters + my $cardnumber = $input->param('cardnumber'); + my $borrowernumber = $input->param('borrowernumber'); + my $surname = $input->param('surname'); + my $firstname = $input->param('firstname'); + my $email = $input->param('email'); + my $categorycode = $input->param('categorycode'); + my $branchcode = $input->param('branchcode'); + + # No empty searches, if no search critrea is added, do nothing. + if ( $cardnumber || $borrowernumber || $surname || $firstname || $email || $categorycode || $branchcode ) { + + # empty search parameters + my %search_params; + + # these params should be exact matches + $search_params{cardnumber} = $cardnumber if $cardnumber; + $search_params{borrowernumber} = $borrowernumber if $borrowernumber; + $search_params{categorycode} = $categorycode if $categorycode; + $search_params{branchcode} = $branchcode if $branchcode; + + # these params don't necessarily have to be exact matches + $search_params{surname} = { 'like' => "%$surname%" } if $surname; + $search_params{firstname} = { 'like' => "%$firstname%" } if $firstname; + $search_params{email} = { 'like' => "%$email%" } if $email; + + my $deleted_patrons_rs = Koha::Old::Patrons->search( + \%search_params, + { + order_by => { -desc => 'updated_on' }, + rows => 100, + } + ); + + my @deleted_patrons; + + while ( my $patron = $deleted_patrons_rs->next ) { + + #collect patron data to use in the results table + my $patron_data = { + borrowernumber => $patron->borrowernumber, + cardnumber => $patron->cardnumber, + surname => $patron->surname, + firstname => $patron->firstname, + email => $patron->email, + branchcode => $patron->branchcode, + updated_on => $patron->updated_on, + }; + + push @deleted_patrons, $patron_data; + } + + $template->param( + view => 'results', + deleted_patrons => \@deleted_patrons, + cardnumber => $cardnumber, + borrowernumber => $borrowernumber, + surname => $surname, + firstname => $firstname, + email => $email, + ); + } else { + $template->param( view => 'search' ); + } + +} elsif ( $op eq 'cud-restore' ) { + + my @borrowernumbers = $input->multi_param('borrowernumber'); + + my @restored_patrons; + my @errors; + + foreach my $borrowernumber (@borrowernumbers) { + try { + # Find the deleted patron + my $deleted_patron = Koha::Old::Patrons->search( { borrowernumber => $borrowernumber } )->next; + + unless ($deleted_patron) { + push @errors, "Borrowernumber $borrowernumber not found in deleted patrons"; + next; + } + + # Attempt to restore + my $restored_patron = $deleted_patron->restore_deleted_borrower; + + push @restored_patrons, { + borrowernumber => $restored_patron->borrowernumber, + cardnumber => $restored_patron->cardnumber, + firstname => $restored_patron->firstname, + surname => $restored_patron->surname, + }; + } catch { + push @errors, "Error restoring borrower $borrowernumber: $_"; + }; + } + + $template->param( + view => 'restored', + restored_patrons => \@restored_patrons, + errors => \@errors, + ); + +} else { + $template->param( view => 'search' ); +} + +output_html_with_http_headers $input, $cookie, $template->output; -- 2.39.5