View | Details | Raw Unified | Return to bug 9302
Collapse All | Expand All

(-)a/Koha/Patrons.pm (+76 lines)
Lines 21-26 package Koha::Patrons; Link Here
21
use Modern::Perl;
21
use Modern::Perl;
22
22
23
use Carp;
23
use Carp;
24
use Readonly;
24
25
25
use Koha::Database;
26
use Koha::Database;
26
use Koha::DateUtils;
27
use Koha::DateUtils;
Lines 31-36 use Koha::Patron; Link Here
31
32
32
use base qw(Koha::Objects);
33
use base qw(Koha::Objects);
33
34
35
Readonly our $RESULTSET_PATRON_ID_MAPPING => {
36
    Accountline          => 'borrowernumber',
37
    ArticleRequest       => 'borrowernumber',
38
    BorrowerAttribute    => 'borrowernumber',
39
    BorrowerDebarment    => 'borrowernumber',
40
    BorrowerFile         => 'borrowernumber',
41
    BorrowerModification => 'borrowernumber',
42
    ClubEnrollment       => 'borrowernumber',
43
    Issue                => 'borrowernumber',
44
    ItemsLastBorrower    => 'borrowernumber',
45
    Linktracker          => 'borrowernumber',
46
    Message              => 'borrowernumber',
47
    MessageQueue         => 'borrowernumber',
48
    OldIssue             => 'borrowernumber',
49
    OldReserve           => 'borrowernumber',
50
    Rating               => 'borrowernumber',
51
    Reserve              => 'borrowernumber',
52
    Review               => 'borrowernumber',
53
    Statistic            => 'borrowernumber',
54
    SearchHistory        => 'userid',
55
    Suggestion           => 'suggestedby',
56
    TagAll               => 'borrowernumber',
57
    Virtualshelfcontent  => 'borrowernumber',
58
    Virtualshelfshare    => 'borrowernumber',
59
    Virtualshelve        => 'owner',
60
};
61
34
=head1 NAME
62
=head1 NAME
35
63
36
Koha::Patron - Koha Patron Object class
64
Koha::Patron - Koha Patron Object class
Lines 183-188 sub anonymise_issue_history { Link Here
183
    $old_issues_to_anonymise->update( { 'old_issues.borrowernumber' => $anonymous_patron } );
211
    $old_issues_to_anonymise->update( { 'old_issues.borrowernumber' => $anonymous_patron } );
184
}
212
}
185
213
214
=head3 merge
215
216
    Koha::Patrons->search->merge( { keeper => $borrowernumber, patrons => \@borrowernumbers } );
217
218
    This subroutine merges a list of patrons into another patron record. This is accomplished by finding
219
    all related patron ids for the patrons to be merged in other tables and changing the ids to be that
220
    of the keeper patron.
221
222
=cut
223
224
sub merge {
225
    my ( $self, $params ) = @_;
226
227
    my $keeper          = $params->{keeper};
228
    my @borrowernumbers = @{ $params->{patrons} };
229
230
    my $patron_to_keep = Koha::Patrons->find( $keeper );
231
    return unless $patron_to_keep;
232
233
    # Ensure the keeper isn't in the list of patrons to merge
234
    @borrowernumbers = grep { $_ ne $keeper } @borrowernumbers;
235
236
    my $schema = Koha::Database->new()->schema();
237
238
    my $results;
239
240
    foreach my $borrowernumber (@borrowernumbers) {
241
        my $patron = Koha::Patrons->find( $borrowernumber );
242
243
        next unless $patron;
244
245
        # Unbless for safety, the patron will end up being deleted
246
        $results->{merged}->{$borrowernumber}->{patron} = $patron->unblessed;
247
248
        while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
249
            my $rs = $schema->resultset($r)->search({ $field => $borrowernumber} );
250
            $results->{merged}->{ $borrowernumber }->{updated}->{$r} = $rs->count();
251
            $rs->update( { $field => $keeper });
252
        }
253
254
        $patron->delete();
255
    }
256
257
    $results->{keeper} = $patron_to_keep;
258
259
    return $results;
260
}
261
186
=head3 type
262
=head3 type
187
263
188
=cut
264
=cut
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/members/member.tt (-2 / +29 lines)
Lines 11-16 Link Here
11
<script type="text/javascript">
11
<script type="text/javascript">
12
//<![CDATA[
12
//<![CDATA[
13
$(document).ready(function() {
13
$(document).ready(function() {
14
    $('#merge-patrons').prop('disabled', true);
15
    $('#memberresultst').on('change', 'input.selection', function() {
16
        if ( $('.selection:checked').length > 1 ) {
17
            $('#merge-patrons').prop('disabled', false);
18
        } else {
19
            $('#merge-patrons').prop('disabled', true);
20
        }
21
    });
22
    $('#merge-patrons').on('click', function() {
23
        var merge_patrons_url = 'merge-patrons.pl?' + $('.selection:checked')
24
            .map(function() {
25
                return "id=" + $(this).val()
26
            }).get().join('&');
27
28
        window.location.href = merge_patrons_url;
29
    });
30
14
    $('#add_to_patron_list_submit').prop('disabled', true);
31
    $('#add_to_patron_list_submit').prop('disabled', true);
15
    $('#new_patron_list').hide();
32
    $('#new_patron_list').hide();
16
33
Lines 359-367 function filterByFirstLetterSurname(letter) { Link Here
359
            <div id="searchheader">
376
            <div id="searchheader">
360
              <h3>Patrons found for: <span id="searchpattern">[% IF searchmember %] for '[% searchmember %]'[% END %]</span></h3>
377
              <h3>Patrons found for: <span id="searchpattern">[% IF searchmember %] for '[% searchmember %]'[% END %]</span></h3>
361
            </div>
378
            </div>
362
            [% IF CAN_user_tools_manage_patron_lists %]
379
            [% IF CAN_user_tools_manage_patron_lists || CAN_user_borrowers %]
363
              <div id="searchheader">
380
              <div id="searchheader">
364
                  <div>
381
                  <div>
382
                    [% IF CAN_user_tools_manage_patron_lists %]
365
                      <a href="#" id="select_all"><i class="fa fa-check"></i> Select all</a>
383
                      <a href="#" id="select_all"><i class="fa fa-check"></i> Select all</a>
366
                      |
384
                      |
367
                      <a href="#" id="clear_all"><i class="fa fa-remove"></i> Clear all</a>
385
                      <a href="#" id="clear_all"><i class="fa fa-remove"></i> Clear all</a>
Lines 385-392 function filterByFirstLetterSurname(letter) { Link Here
385
403
386
                          <input id="add_to_patron_list_submit" type="submit" class="submit" value="Save">
404
                          <input id="add_to_patron_list_submit" type="submit" class="submit" value="Save">
387
                      </span>
405
                      </span>
406
                    [% END %]
407
408
                    [% IF CAN_user_tools_manage_patron_lists && CAN_user_borrowers %]
409
                        |
410
                    [% END %]
411
412
                    [% IF CAN_user_borrowers %]
413
                          <button id="merge-patrons" type="submit">Merge selected patrons</button>
414
                    [% END %]
388
                  </div>
415
                  </div>
389
              </div>
416
                </div>
390
            [% END %]
417
            [% END %]
391
418
392
            <table id="memberresultst">
419
            <table id="memberresultst">
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/members/merge-patrons.tt (+133 lines)
Line 0 Link Here
1
[% USE Branches %]
2
[% USE Categories %]
3
[% USE KohaDates %]
4
[% INCLUDE 'doc-head-open.inc' %]
5
<title>Koha &rsaquo; Patrons &rsaquo; Merge patron records</title>
6
[% INCLUDE 'doc-head-close.inc' %]
7
8
<script type="text/javascript">
9
//<![CDATA[
10
$(document).ready(function() {
11
    $('#merge-patrons').prop('disabled', true);
12
    $('#patron-merge-table').on('change', 'input', function() {
13
        if ( $('.keeper:checked').length > 0 ) {
14
            $('#merge-patrons').prop('disabled', false);
15
        } else {
16
            $('#merge-patrons').prop('disabled', true);
17
        }
18
    });
19
});
20
//]]>
21
</script>
22
23
</head>
24
<body id="pat_merge" class="pat">
25
[% INCLUDE 'header.inc' %]
26
[% INCLUDE 'patron-search.inc' %]
27
28
[% BLOCK display_names %]
29
	[% SWITCH rs %]
30
		[% CASE 'Accountline'           %]account lines
31
		[% CASE 'ArticleRequest'        %]article requests
32
		[% CASE 'BorrowerAttribute'     %]extended patron attributes
33
		[% CASE 'BorrowerDebarment'     %]patron restrictions
34
		[% CASE 'BorrowerFile'          %]patrons files
35
		[% CASE 'BorrowerModification'  %]patron modification requests
36
		[% CASE 'ClubEnrollment'        %]club enrollments
37
		[% CASE 'Issue'                 %]checkouts
38
		[% CASE 'ItemsLastBorrower'     %]marks as last borrower of item
39
		[% CASE 'Linktracker'           %]tracked link clicks
40
		[% CASE 'Message'               %]patron messages
41
		[% CASE 'MessageQueue'          %]patron notices
42
		[% CASE 'OldIssue'              %]previous checkouts
43
		[% CASE 'OldReserve'            %]filled holds
44
		[% CASE 'Rating'                %]ratings
45
		[% CASE 'Reserve'               %]current holds
46
		[% CASE 'Review'                %]reviews
47
		[% CASE 'Statistic'             %]statistics
48
		[% CASE 'SearchHistory'         %]historical searches
49
		[% CASE 'Suggestion'            %]purchase suggestions
50
		[% CASE 'TagAll'                %]tags
51
		[% CASE 'Virtualshelfcontent'   %]list items
52
		[% CASE 'Virtualshelfshare'     %]list shares
53
		[% CASE 'Virtualshelve'         %]lists
54
		[% CASE %][% rs %]
55
	[% END %]
56
[% END %]
57
58
<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; <a href="/cgi-bin/koha/members/members-home.pl">Patrons</a> &rsaquo; Merge patron records</div>
59
60
<div id="doc2" class="yui-t7">
61
   <div id="bd">
62
        <div id="yui-main">
63
            <h3>Merge patron records</h3>
64
65
            [% IF action == 'show' %]
66
                <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>
67
                <form type="post" action="merge-patrons.pl">
68
                    <table id="patron-merge-table" class="datatable">
69
                        <thead>
70
                            <tr>
71
                                <th>&nbsp;</th>
72
                                <th>Card</th>
73
                                <th>Name</th>
74
                                <th>Date of birth</th>
75
                                <th>Category</th>
76
                                <th>Library</th>
77
                                <th>Expires on</th>
78
                            </tr>
79
                        </thead>
80
81
                        <tbody>
82
                            [% FOREACH p IN patrons %]
83
                                <tr>
84
                                    <td><input class='keeper' type='radio' name='keeper' value='[% p.id %]' /></td>
85
                                    <td>[% p.cardnumber | html %]</td>
86
                                    <td>[% p.firstname | html %] [% p.surname | html %]</td>
87
                                    <td>[% p.dateofbirth | $KohaDates %]</td>
88
                                    <td>[% Categories.GetName( p.categorycode ) %] ([% p.categorycode %])</td>
89
                                    <td>[% Branches.GetName( p.branchcode ) %]</td>
90
                                    <td>[% p.dateexpiry | $KohaDates %]</td>
91
                            [% END %]
92
                        </tbody>
93
                    </table>
94
95
                    [% FOREACH p IN patrons %]
96
                        <input type="hidden" name="id" value="[% p.id %]" />
97
                    [% END %]
98
99
                    <p/>
100
101
                    <input type="hidden" name="action" value="merge" />
102
                    <input id="merge-patrons" type="submit" value="Merge patrons" />
103
                </form>
104
            [% ELSIF action == 'merge' %]
105
                <h4>Results</h4>
106
107
                <p>
108
                    Patron records merged into <a href="moremember.pl?borrowernumber=[% results.keeper.id %]">[% results.keeper.firstname %] [% results.keeper.surname %] ([% results.keeper.cardnumber | html %])</a>
109
                </p>
110
111
                [% FOREACH pair IN results.merged.pairs %]
112
                    [% SET patron = pair.value.patron %]
113
114
                    <h5>[% patron.firstname %] [% patron.surname %] ([% patron.cardnumber %])</h5>
115
116
                    [% USE Dumper %]
117
                    [% FOREACH r IN pair.value.updated.pairs %]
118
                        [% SET name = r.key %]
119
                        [% SET count = r.value %]
120
						[% IF count %]
121
							<p>
122
								[% count %] [% PROCESS display_names rs = name %] transferred.
123
								[% IF name == 'Reserve' %]
124
									<strong>It is advisable to check for and resolve duplicate holds due to merging.</strong>
125
								[% END %]
126
							</p>
127
						[% END %]
128
                    [% END %]
129
                [% END %]
130
            [% END %]
131
        </div>
132
    </div>
133
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/patron_lists/list.tt (-4 / +34 lines)
Lines 45-53 $(document).ready(function() { Link Here
45
        });
45
        });
46
    });
46
    });
47
47
48
    $("#remove_patrons").submit(function(){
48
    $('.list-remove').on('click', function(){
49
        var checkedItems = $("input:checked");
49
        var checkedItems = $("input:checked");
50
        if ($(checkedItems).size() == 0) {
50
        if ($(checkedItems).length == 0) {
51
            alert(_("You must select one or more patrons to remove"));
51
            alert(_("You must select one or more patrons to remove"));
52
            return false;
52
            return false;
53
        }
53
        }
Lines 59-64 $(document).ready(function() { Link Here
59
            return false;
59
            return false;
60
        }
60
        }
61
    });
61
    });
62
63
    $('.merge-patrons').on('click', function() {
64
        var checkedItems = $("input:checked");
65
        if ($(checkedItems).length < 2) {
66
            alert(_("You must select one or more patrons to remove"));
67
            return false;
68
        }
69
        $(checkedItems).parents('tr').addClass("warn");
70
        if (confirm(_("Are you sure you want to remove the selected patrons?"))) {
71
            var merge_patrons_url = '/cgi-bin/koha/members/merge-patrons.pl?' +
72
                $('.selection:checked')
73
                .map(function() {
74
                    return "id=" + $( '#borrowernumber_' + $(this).val() ).val()
75
                }).get().join('&');
76
77
            window.location.href = merge_patrons_url;
78
            return false;
79
        } else {
80
            $(checkedItems).parents('tr').removeClass("warn");
81
            return false;
82
        }
83
    });
62
});
84
});
63
85
64
//]]>
86
//]]>
Lines 107-112 $(document).ready(function() { Link Here
107
                <div class="btn-group">
129
                <div class="btn-group">
108
                    <button class="btn btn-default btn-xs list-remove" type="submit"><i class="fa fa-trash"></i> Remove selected</button>
130
                    <button class="btn btn-default btn-xs list-remove" type="submit"><i class="fa fa-trash"></i> Remove selected</button>
109
                </div>
131
                </div>
132
                |
133
                <div class="btn-group">
134
                    <button class="btn btn-default btn-xs merge-patrons"><i class="fa fa-compress"></i> Merge selected patrons</button>
135
                </div>
110
            </div>
136
            </div>
111
137
112
            <table id="patron-list-table">
138
            <table id="patron-list-table">
Lines 127-133 $(document).ready(function() { Link Here
127
                <tbody>
153
                <tbody>
128
                    [% FOREACH p IN list.patron_list_patrons %]
154
                    [% FOREACH p IN list.patron_list_patrons %]
129
                        <tr>
155
                        <tr>
130
                            <td><input type="checkbox" name="patrons_to_remove" value="[% p.patron_list_patron_id %]" /></td>
156
                            <td>
157
                                <input type="checkbox" name="patrons_to_remove" class="selection" value="[% p.patron_list_patron_id %]" />
158
                                <input type="hidden" id="borrowernumber_[% p.patron_list_patron_id %]" value="[% p.borrowernumber.id %]" />
159
                            </td>
131
                            <td>
160
                            <td>
132
                                <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% p.borrowernumber.borrowernumber %]">
161
                                <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% p.borrowernumber.borrowernumber %]">
133
                                    [% p.borrowernumber.cardnumber %]
162
                                    [% p.borrowernumber.cardnumber %]
Lines 152-158 $(document).ready(function() { Link Here
152
            </table>
181
            </table>
153
182
154
            <input type="hidden" name="patron_list_id" value="[% list.patron_list_id %]" />
183
            <input type="hidden" name="patron_list_id" value="[% list.patron_list_id %]" />
155
            <button type="submit" class="btn btn-default btn-sm"><i class="fa fa-trash" aria-hidden="true"></i> Remove selected patrons</button>
184
            <button type="submit" class="btn btn-default btn-sm list-remove"><i class="fa fa-trash" aria-hidden="true"></i> Remove selected patrons</button>
185
            <button class="btn btn-default btn-sm merge-patrons" type="submit"><i class="fa fa-compress"></i> Merge selected patrons</button>
156
        </form>
186
        </form>
157
187
158
            </div>
188
            </div>
(-)a/members/merge-patrons.pl (+58 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# Copyright ByWater Solutions 2017
4
# This file is part of Koha.
5
#
6
# Koha is free software; you can redistribute it and/or modify it
7
# under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 3 of the License, or
9
# (at your option) any later version.
10
#
11
# Koha is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with Koha; if not, see <http://www.gnu.org/licenses>.
18
19
use Modern::Perl;
20
21
use CGI qw ( -utf8 );
22
23
use C4::Auth;
24
use C4::Output;
25
use C4::Context;
26
use Koha::Patrons;
27
28
my $cgi = new CGI;
29
30
my ( $template, $loggedinuser, $cookie, $flags ) = get_template_and_user(
31
    {
32
        template_name   => "members/merge-patrons.tt",
33
        query           => $cgi,
34
        type            => "intranet",
35
        authnotrequired => 0,
36
        flagsrequired   => { borrowers => 1 },
37
        debug           => 1,
38
    }
39
);
40
41
my $action = $cgi->param('action') || 'show';
42
my @ids = $cgi->multi_param('id');
43
44
if ( $action eq 'show' ) {
45
    my $patrons =
46
      Koha::Patrons->search( { borrowernumber => { -in => \@ids } } );
47
    $template->param( patrons => $patrons );
48
} elsif ( $action eq 'merge' ) {
49
    my $keeper = $cgi->param('keeper');
50
    my $results = Koha::Patrons->merge( { keeper => $keeper, patrons => \@ids } );
51
    $template->param( results => $results );
52
}
53
54
$template->param( action => $action );
55
56
output_html_with_http_headers $cgi, $cookie, $template->output;
57
58
1;
(-)a/t/db_dependent/Patrons.t (-2 / +47 lines)
Lines 17-23 Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::More tests => 17;
20
use Test::More tests => 18;
21
use Test::Warn;
21
use Test::Warn;
22
22
23
use C4::Context;
23
use C4::Context;
Lines 103-107 foreach my $b ( $patrons->as_list() ) { Link Here
103
    is( $b->categorycode(), $categorycode, "Iteration returns a patron object" );
103
    is( $b->categorycode(), $categorycode, "Iteration returns a patron object" );
104
}
104
}
105
105
106
subtest 'Test Koha::Patrons::merge' => sub {
107
    plan tests => 98;
108
109
    my $schema = Koha::Database->new()->schema();
110
111
    my $resultsets = $Koha::Patrons::RESULTSET_PATRON_ID_MAPPING;
112
113
    my $keeper  = $builder->build( { source => 'Borrower' } )->{borrowernumber};
114
    my $loser_1 = $builder->build( { source => 'Borrower' } )->{borrowernumber};
115
    my $loser_2 = $builder->build( { source => 'Borrower' } )->{borrowernumber};
116
117
    while (my ($r, $field) = each(%$resultsets)) {
118
        $builder->build( { source => $r, value => { $field => $keeper } } );
119
        $builder->build( { source => $r, value => { $field => $loser_1 } } );
120
        $builder->build( { source => $r, value => { $field => $loser_2 } } );
121
122
        my $keeper_rs =
123
          $schema->resultset($r)->search( { $field => $keeper } );
124
        is( $keeper_rs->count(), 1, "Found 1 $r rows for keeper" );
125
126
        my $loser_1_rs =
127
          $schema->resultset($r)->search( { $field => $loser_1 } );
128
        is( $loser_1_rs->count(), 1, "Found 1 $r rows for loser_1" );
129
130
        my $loser_2_rs =
131
          $schema->resultset($r)->search( { $field => $loser_2 } );
132
        is( $loser_2_rs->count(), 1, "Found 1 $r rows for loser_2" );
133
    }
134
135
    my $results = Koha::Patrons->merge(
136
        {
137
            keeper  => $keeper,
138
            patrons => [ $loser_1, $loser_2 ],
139
        }
140
    );
141
142
    while (my ($r, $field) = each(%$resultsets)) {
143
        my $keeper_rs =
144
          $schema->resultset($r)->search( {$field => $keeper } );
145
        is( $keeper_rs->count(), 3, "Found 2 $r rows for keeper" );
146
    }
147
148
	is( Koha::Patrons->find($loser_1), undef, 'Loser 1 has been deleted' );
149
	is( Koha::Patrons->find($loser_2), undef, 'Loser 2 has been deleted' );
150
};
151
106
$schema->storage->txn_rollback();
152
$schema->storage->txn_rollback();
107
153
108
- 

Return to bug 9302