@@ -, +, @@
or add them all to a list of patrons.
are now linked to the kept patron
---
Koha/Patrons.pm | 75 ++++++++++++
.../prog/en/modules/members/member.tt | 31 ++++-
.../prog/en/modules/members/merge-patrons.tt | 133 +++++++++++++++++++++
.../prog/en/modules/patron_lists/list.tt | 38 +++++-
members/merge-patrons.pl | 58 +++++++++
t/db_dependent/Patrons.t | 73 ++++++++++-
6 files changed, 401 insertions(+), 7 deletions(-)
create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/members/merge-patrons.tt
create mode 100755 members/merge-patrons.pl
--- a/Koha/Patrons.pm
+++ a/Koha/Patrons.pm
@@ -183,6 +183,81 @@ sub anonymise_issue_history {
$old_issues_to_anonymise->update( { 'old_issues.borrowernumber' => $anonymous_patron } );
}
+=head3 merge
+
+ Koha::Patrons->search->merge( { keeper => $borrowernumber, patrons => \@borrowernumbers } );
+
+ This subroutine merges a list of patrons into another patron record. This is accomplished by finding
+ all related patron ids for the patrons to be merged in other tables and changing the ids to be that
+ of the keeper patron.
+
+=cut
+
+sub merge {
+ my ( $self, $params ) = @_;
+
+ my $keeper = $params->{keeper};
+ my @borrowernumbers = @{ $params->{patrons} };
+
+ my $patron_to_keep = Koha::Patrons->find( $keeper );
+ return unless $patron_to_keep;
+
+ # Ensure the keeper isn't in the list of patrons to merge
+ @borrowernumbers = grep { $_ ne $keeper } @borrowernumbers;
+
+ my $schema = Koha::Database->new()->schema();
+
+ my $resultsets = {
+ Accountline => 'borrowernumber',
+ ArticleRequest => 'borrowernumber',
+ BorrowerAttribute => 'borrowernumber',
+ BorrowerDebarment => 'borrowernumber',
+ BorrowerFile => 'borrowernumber',
+ BorrowerModification => 'borrowernumber',
+ ClubEnrollment => 'borrowernumber',
+ Issue => 'borrowernumber',
+ ItemsLastBorrower => 'borrowernumber',
+ Linktracker => 'borrowernumber',
+ Message => 'borrowernumber',
+ MessageQueue => 'borrowernumber',
+ OldIssue => 'borrowernumber',
+ OldReserve => 'borrowernumber',
+ Rating => 'borrowernumber',
+ Reserve => 'borrowernumber',
+ Review => 'borrowernumber',
+ Statistic => 'borrowernumber',
+ SearchHistory => 'userid',
+ Suggestion => 'suggestedby',
+ TagAll => 'borrowernumber',
+ Virtualshelfcontent => 'borrowernumber',
+ Virtualshelfshare => 'borrowernumber',
+ Virtualshelve => 'owner',
+ };
+
+ my $results;
+
+ foreach my $borrowernumber (@borrowernumbers) {
+ my $patron = Koha::Patrons->find( $borrowernumber );
+
+ next unless $patron;
+
+ # Unbless for safety, the patron will end up being deleted
+ $results->{merged}->{$borrowernumber}->{patron} = $patron->unblessed;
+
+ while (my ($r, $field) = each(%$resultsets)) {
+ my $rs = $schema->resultset($r)->search({ $field => $borrowernumber} );
+ $results->{merged}->{ $borrowernumber }->{updated}->{$r} = $rs->count();
+ $rs->update( { $field => $keeper });
+ }
+
+ $patron->delete();
+ }
+
+ $results->{keeper} = $patron_to_keep;
+
+ return $results;
+}
+
=head3 type
=cut
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/member.tt
+++ a/koha-tmpl/intranet-tmpl/prog/en/modules/members/member.tt
@@ -11,6 +11,23 @@
+
+
+
+[% INCLUDE 'header.inc' %]
+[% INCLUDE 'patron-search.inc' %]
+
+[% BLOCK display_names %]
+ [% SWITCH rs %]
+ [% CASE 'Accountline' %]account lines
+ [% CASE 'ArticleRequest' %]article requests
+ [% CASE 'BorrowerAttribute' %]extended patron attributes
+ [% CASE 'BorrowerDebarment' %]patron restrictions
+ [% CASE 'BorrowerFile' %]patrons files
+ [% CASE 'BorrowerModification' %]patron modification requests
+ [% CASE 'ClubEnrollment' %]club enrollments
+ [% CASE 'Issue' %]checkouts
+ [% CASE 'ItemsLastBorrower' %]marks as last borrower of item
+ [% CASE 'Linktracker' %]tracked link clicks
+ [% CASE 'Message' %]patron messages
+ [% CASE 'MessageQueue' %]patron notices
+ [% CASE 'OldIssue' %]previous checkouts
+ [% CASE 'OldReserve' %]filled holds
+ [% CASE 'Rating' %]ratings
+ [% CASE 'Reserve' %]current holds
+ [% CASE 'Review' %]reviews
+ [% CASE 'Statistic' %]statistics
+ [% CASE 'SearchHistory' %]historical searches
+ [% CASE 'Suggestion' %]purchase suggestions
+ [% CASE 'TagAll' %]tags
+ [% CASE 'Virtualshelfcontent' %]list items
+ [% CASE 'Virtualshelfshare' %]list shares
+ [% CASE 'Virtualshelve' %]lists
+ [% CASE %][% rs %]
+ [% END %]
+[% END %]
+
+
+
+
+
+
+
Merge patron records
+
+ [% IF action == 'show' %]
+
Select patron to keep. Data from the other patrons will be transferred to this patron record and the remaining patron records will be deleted.
+
+ [% ELSIF action == 'merge' %]
+
Results
+
+
+ Patron records merged into [% results.keeper.firstname %] [% results.keeper.surname %] ([% results.keeper.cardnumber | html %])
+
+
+ [% FOREACH pair IN results.merged.pairs %]
+ [% SET patron = pair.value.patron %]
+
+
[% patron.firstname %] [% patron.surname %] ([% patron.cardnumber %])
+
+ [% USE Dumper %]
+ [% FOREACH r IN pair.value.updated.pairs %]
+ [% SET name = r.key %]
+ [% SET count = r.value %]
+ [% IF count %]
+
+ [% count %] [% PROCESS display_names rs = name %] transferred.
+ [% IF name == 'Reserve' %]
+ It is advisable to check for and resolve duplicate holds due to merging.
+ [% END %]
+
+ [% END %]
+ [% END %]
+ [% END %]
+ [% END %]
+
+
+[% INCLUDE 'intranet-bottom.inc' %]
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/patron_lists/list.tt
+++ a/koha-tmpl/intranet-tmpl/prog/en/modules/patron_lists/list.tt
@@ -45,9 +45,9 @@ $(document).ready(function() {
});
});
- $("#remove_patrons").submit(function(){
+ $('.list-remove').on('click', function(){
var checkedItems = $("input:checked");
- if ($(checkedItems).size() == 0) {
+ if ($(checkedItems).length == 0) {
alert(_("You must select one or more patrons to remove"));
return false;
}
@@ -59,6 +59,28 @@ $(document).ready(function() {
return false;
}
});
+
+ $('.merge-patrons').on('click', function() {
+ var checkedItems = $("input:checked");
+ if ($(checkedItems).length < 2) {
+ alert(_("You must select one or more patrons to remove"));
+ return false;
+ }
+ $(checkedItems).parents('tr').addClass("warn");
+ if (confirm(_("Are you sure you want to remove the selected patrons?"))) {
+ var merge_patrons_url = '/cgi-bin/koha/members/merge-patrons.pl?' +
+ $('.selection:checked')
+ .map(function() {
+ return "id=" + $( '#borrowernumber_' + $(this).val() ).val()
+ }).get().join('&');
+
+ window.location.href = merge_patrons_url;
+ return false;
+ } else {
+ $(checkedItems).parents('tr').removeClass("warn");
+ return false;
+ }
+ });
});
//]]>
@@ -107,6 +129,10 @@ $(document).ready(function() {
Remove selected
+ |
+
+ Merge selected patrons
+
- Remove selected patrons
+ Remove selected patrons
+ Merge selected patrons
--- a/members/merge-patrons.pl
+++ a/members/merge-patrons.pl
@@ -0,0 +1,58 @@
+#!/usr/bin/perl
+
+# Copyright ByWater Solutions 2017
+# 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 .
+
+use Modern::Perl;
+
+use CGI qw ( -utf8 );
+
+use C4::Auth;
+use C4::Output;
+use C4::Context;
+use Koha::Patrons;
+
+my $cgi = new CGI;
+
+my ( $template, $loggedinuser, $cookie, $flags ) = get_template_and_user(
+ {
+ template_name => "members/merge-patrons.tt",
+ query => $cgi,
+ type => "intranet",
+ authnotrequired => 0,
+ flagsrequired => { borrowers => 1 },
+ debug => 1,
+ }
+);
+
+my $action = $cgi->param('action') || 'show';
+my @ids = $cgi->multi_param('id');
+
+if ( $action eq 'show' ) {
+ my $patrons =
+ Koha::Patrons->search( { borrowernumber => { -in => \@ids } } );
+ $template->param( patrons => $patrons );
+} elsif ( $action eq 'merge' ) {
+ my $keeper = $cgi->param('keeper');
+ my $results = Koha::Patrons->merge( { keeper => $keeper, patrons => \@ids } );
+ $template->param( results => $results );
+}
+
+$template->param( action => $action );
+
+output_html_with_http_headers $cgi, $cookie, $template->output;
+
+1;
--- a/t/db_dependent/Patrons.t
+++ a/t/db_dependent/Patrons.t
@@ -17,7 +17,7 @@
use Modern::Perl;
-use Test::More tests => 17;
+use Test::More tests => 18;
use Test::Warn;
use C4::Context;
@@ -103,5 +103,76 @@ foreach my $b ( $patrons->as_list() ) {
is( $b->categorycode(), $categorycode, "Iteration returns a patron object" );
}
+subtest 'Test Koha::Patrons::merge' => sub {
+ plan tests => 98;
+
+ my $schema = Koha::Database->new()->schema();
+
+ my $resultsets = {
+ Accountline => 'borrowernumber',
+ ArticleRequest => 'borrowernumber',
+ BorrowerAttribute => 'borrowernumber',
+ BorrowerDebarment => 'borrowernumber',
+ BorrowerFile => 'borrowernumber',
+ BorrowerModification => 'borrowernumber',
+ ClubEnrollment => 'borrowernumber',
+ Issue => 'borrowernumber',
+ ItemsLastBorrower => 'borrowernumber',
+ Linktracker => 'borrowernumber',
+ Message => 'borrowernumber',
+ MessageQueue => 'borrowernumber',
+ OldIssue => 'borrowernumber',
+ OldReserve => 'borrowernumber',
+ Rating => 'borrowernumber',
+ Reserve => 'borrowernumber',
+ Review => 'borrowernumber',
+ Statistic => 'borrowernumber',
+ SearchHistory => 'userid',
+ Suggestion => 'suggestedby',
+ TagAll => 'borrowernumber',
+ Virtualshelfcontent => 'borrowernumber',
+ Virtualshelfshare => 'borrowernumber',
+ Virtualshelve => 'owner',
+ };
+
+ my $keeper = $builder->build( { source => 'Borrower' } )->{borrowernumber};
+ my $loser_1 = $builder->build( { source => 'Borrower' } )->{borrowernumber};
+ my $loser_2 = $builder->build( { source => 'Borrower' } )->{borrowernumber};
+
+ while (my ($r, $field) = each(%$resultsets)) {
+ $builder->build( { source => $r, value => { $field => $keeper } } );
+ $builder->build( { source => $r, value => { $field => $loser_1 } } );
+ $builder->build( { source => $r, value => { $field => $loser_2 } } );
+
+ my $keeper_rs =
+ $schema->resultset($r)->search( { $field => $keeper } );
+ is( $keeper_rs->count(), 1, "Found 1 $r rows for keeper" );
+
+ my $loser_1_rs =
+ $schema->resultset($r)->search( { $field => $loser_1 } );
+ is( $loser_1_rs->count(), 1, "Found 1 $r rows for loser_1" );
+
+ my $loser_2_rs =
+ $schema->resultset($r)->search( { $field => $loser_2 } );
+ is( $loser_2_rs->count(), 1, "Found 1 $r rows for loser_2" );
+ }
+
+ my $results = Koha::Patrons->merge(
+ {
+ keeper => $keeper,
+ patrons => [ $loser_1, $loser_2 ],
+ }
+ );
+
+ while (my ($r, $field) = each(%$resultsets)) {
+ my $keeper_rs =
+ $schema->resultset($r)->search( {$field => $keeper } );
+ is( $keeper_rs->count(), 3, "Found 2 $r rows for keeper" );
+ }
+
+ is( Koha::Patrons->find($loser_1), undef, 'Loser 1 has been deleted' );
+ is( Koha::Patrons->find($loser_2), undef, 'Loser 2 has been deleted' );
+};
+
$schema->storage->txn_rollback();
--