use base qw(Koha::Objects);
+our $RESULTSET_PATRON_ID_MAPPING = {
+ 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',
+};
+
=head1 NAME
Koha::Patron - Koha Patron Object class
return $nb_rows;
}
+=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 $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(%$RESULTSET_PATRON_ID_MAPPING)) {
+ 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
<div id="searchheader">
<h3>Patrons found for: <span id="searchpattern">[% IF searchmember %] for '[% searchmember | html %]'[% END %]</span></h3>
</div>
- [% IF CAN_user_tools_manage_patron_lists %]
+ [% IF CAN_user_tools_manage_patron_lists || CAN_user_borrowers %]
<div id="searchheader">
<div>
+ [% IF CAN_user_tools_manage_patron_lists %]
<a href="#" id="select_all"><i class="fa fa-check"></i> Select all</a>
|
<a href="#" id="clear_all"><i class="fa fa-remove"></i> Clear all</a>
<input id="add_to_patron_list_submit" type="submit" class="submit" value="Save">
</span>
+ [% END %]
+
+ [% IF CAN_user_tools_manage_patron_lists && CAN_user_borrowers %]
+ |
+ [% END %]
+
+ [% IF CAN_user_borrowers %]
+ <button id="merge-patrons" type="submit">Merge selected patrons</button>
+ [% END %]
</div>
- </div>
+ </div>
[% END %]
<table id="memberresultst">
[% Asset.js("js/members-menu.js") %]
<script type="text/javascript">
$(document).ready(function() {
+ $('#merge-patrons').prop('disabled', true);
+ $('#memberresultst').on('change', 'input.selection', function() {
+ if ( $('.selection:checked').length > 1 ) {
+ $('#merge-patrons').prop('disabled', false);
+ } else {
+ $('#merge-patrons').prop('disabled', true);
+ }
+ });
+ $('#merge-patrons').on('click', function() {
+ var merge_patrons_url = 'merge-patrons.pl?' + $('.selection:checked')
+ .map(function() {
+ return "id=" + $(this).val()
+ }).get().join('&');
+
+ window.location.href = merge_patrons_url;
+ });
+
$('#add_to_patron_list_submit').prop('disabled', true);
$('#new_patron_list').hide();
--- /dev/null
+[% USE Branches %]
+[% USE Categories %]
+[% USE KohaDates %]
+[% INCLUDE 'doc-head-open.inc' %]
+<title>Koha › Patrons › Merge patron records</title>
+[% INCLUDE 'doc-head-close.inc' %]
+
+<script type="text/javascript">
+//<![CDATA[
+$(document).ready(function() {
+ $('#merge-patrons').prop('disabled', true);
+ $('#patron-merge-table').on('change', 'input', function() {
+ if ( $('.keeper:checked').length > 0 ) {
+ $('#merge-patrons').prop('disabled', false);
+ } else {
+ $('#merge-patrons').prop('disabled', true);
+ }
+ });
+});
+//]]>
+</script>
+
+</head>
+<body id="pat_merge" class="pat">
+[% 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 %]
+
+<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> › <a href="/cgi-bin/koha/members/members-home.pl">Patrons</a> › Merge patron records</div>
+
+<div id="doc2" class="yui-t7">
+ <div id="bd">
+ <div id="yui-main">
+ <h3>Merge patron records</h3>
+
+ [% IF action == 'show' %]
+ <p>Select patron to keep. Data from the other patrons will be transferred to this patron record and the remaining patron records will be deleted.</p>
+ <form type="post" action="merge-patrons.pl">
+ <table id="patron-merge-table" class="datatable">
+ <thead>
+ <tr>
+ <th> </th>
+ <th>Card</th>
+ <th>Name</th>
+ <th>Date of birth</th>
+ <th>Category</th>
+ <th>Library</th>
+ <th>Expires on</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ [% FOREACH p IN patrons %]
+ <tr>
+ <td><input class='keeper' type='radio' name='keeper' value='[% p.id %]' /></td>
+ <td>[% p.cardnumber | html %]</td>
+ <td>[% p.firstname | html %] [% p.surname | html %]</td>
+ <td>[% p.dateofbirth | $KohaDates %]</td>
+ <td>[% Categories.GetName( p.categorycode ) %] ([% p.categorycode %])</td>
+ <td>[% Branches.GetName( p.branchcode ) %]</td>
+ <td>[% p.dateexpiry | $KohaDates %]</td>
+ [% END %]
+ </tbody>
+ </table>
+
+ [% FOREACH p IN patrons %]
+ <input type="hidden" name="id" value="[% p.id %]" />
+ [% END %]
+
+ <p/>
+
+ <input type="hidden" name="action" value="merge" />
+ <input id="merge-patrons" type="submit" value="Merge patrons" />
+ </form>
+ [% ELSIF action == 'merge' %]
+ <h4>Results</h4>
+
+ <p>
+ Patron records merged into <a href="moremember.pl?borrowernumber=[% results.keeper.id %]">[% results.keeper.firstname %] [% results.keeper.surname %] ([% results.keeper.cardnumber | html %])</a>
+ </p>
+
+ [% FOREACH pair IN results.merged.pairs %]
+ [% SET patron = pair.value.patron %]
+
+ <h5>[% patron.firstname %] [% patron.surname %] ([% patron.cardnumber %])</h5>
+
+ [% USE Dumper %]
+ [% FOREACH r IN pair.value.updated.pairs %]
+ [% SET name = r.key %]
+ [% SET count = r.value %]
+ [% IF count %]
+ <p>
+ [% count %] [% PROCESS display_names rs = name %] transferred.
+ [% IF name == 'Reserve' %]
+ <strong>It is advisable to check for and resolve duplicate holds due to merging.</strong>
+ [% END %]
+ </p>
+ [% END %]
+ [% END %]
+ [% END %]
+ [% END %]
+ </div>
+ </div>
+[% INCLUDE 'intranet-bottom.inc' %]
<div class="btn-group">
<button class="btn btn-default btn-xs list-remove" type="submit"><i class="fa fa-trash"></i> Remove selected</button>
</div>
+ |
+ <div class="btn-group">
+ <button class="btn btn-default btn-xs merge-patrons"><i class="fa fa-compress"></i> Merge selected patrons</button>
+ </div>
</div>
<table id="patron-list-table">
<tbody>
[% FOREACH p IN list.patron_list_patrons %]
<tr>
- <td><input type="checkbox" name="patrons_to_remove" value="[% p.patron_list_patron_id %]" /></td>
+ <td>
+ <input type="checkbox" name="patrons_to_remove" class="selection" value="[% p.patron_list_patron_id %]" />
+ <input type="hidden" id="borrowernumber_[% p.patron_list_patron_id %]" value="[% p.borrowernumber.id %]" />
+ </td>
<td>
<a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% p.borrowernumber.borrowernumber %]">
[% p.borrowernumber.cardnumber %]
</table>
<input type="hidden" name="patron_list_id" value="[% list.patron_list_id %]" />
- <button type="submit" class="btn btn-default btn-sm"><i class="fa fa-trash" aria-hidden="true"></i> Remove selected patrons</button>
+ <button type="submit" class="btn btn-default btn-sm list-remove"><i class="fa fa-trash" aria-hidden="true"></i> Remove selected patrons</button>
+ <button class="btn btn-default btn-sm merge-patrons" type="submit"><i class="fa fa-compress"></i> Merge selected patrons</button>
</form>
</div>
$("#add_patrons_by_barcode, #patron_search_line").show();
$("#add_patrons_by_search, #patron_barcodes_line, #patron_barcodes_submit").hide();
});
+
+ $('.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;
+ }
+ });
});
</script>
[% END %]
--- /dev/null
+#!/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 <http://www.gnu.org/licenses>.
+
+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;
use Modern::Perl;
-use Test::More tests => 17;
+use Test::More tests => 18;
use Test::Warn;
use C4::Context;
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 = $Koha::Patrons::RESULTSET_PATRON_ID_MAPPING;
+
+ 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();