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

(-)a/Koha/Patrons.pm (+75 lines)
Lines 183-188 sub anonymise_issue_history { Link Here
183
    $old_issues_to_anonymise->update( { 'old_issues.borrowernumber' => $anonymous_patron } );
183
    $old_issues_to_anonymise->update( { 'old_issues.borrowernumber' => $anonymous_patron } );
184
}
184
}
185
185
186
=head3 merge
187
188
    Koha::Patrons->search->merge( { keeper => $borrowernumber, patrons => \@borrowernumbers } );
189
190
    This subroutine merges a list of patrons into another patron record. This is accomplished by finding
191
    all related patron ids for the patrons to be merged in other tables and changing the ids to be that
192
    of the keeper patron.
193
194
=cut
195
196
sub merge {
197
    my ( $self, $params ) = @_;
198
199
    my $keeper          = $params->{keeper};
200
    my @borrowernumbers = @{ $params->{patrons} };
201
202
    my $patron_to_keep = Koha::Patrons->find( $keeper );
203
    return unless $patron_to_keep;
204
205
    # Ensure the keeper isn't in the list of patrons to merge
206
    @borrowernumbers = grep { $_ ne $keeper } @borrowernumbers;
207
208
    my $schema = Koha::Database->new()->schema();
209
210
    my $resultsets = {
211
        Accountline          => 'borrowernumber',
212
        ArticleRequest       => 'borrowernumber',
213
        BorrowerAttribute    => 'borrowernumber',
214
        BorrowerDebarment    => 'borrowernumber',
215
        BorrowerFile         => 'borrowernumber',
216
        BorrowerModification => 'borrowernumber',
217
        ClubEnrollment       => 'borrowernumber',
218
        Issue                => 'borrowernumber',
219
        ItemsLastBorrower    => 'borrowernumber',
220
        Linktracker          => 'borrowernumber',
221
        Message              => 'borrowernumber',
222
        MessageQueue         => 'borrowernumber',
223
        OldIssue             => 'borrowernumber',
224
        OldReserve           => 'borrowernumber',
225
        Rating               => 'borrowernumber',
226
        Reserve              => 'borrowernumber',
227
        Review               => 'borrowernumber',
228
        Statistic            => 'borrowernumber',
229
        SearchHistory        => 'userid',
230
        Suggestion           => 'suggestedby',
231
        TagAll               => 'borrowernumber',
232
        Virtualshelfcontent  => 'borrowernumber',
233
        Virtualshelfshare    => 'borrowernumber',
234
        Virtualshelve        => 'owner',
235
    };
236
237
    my $results;
238
239
    foreach my $borrowernumber (@borrowernumbers) {
240
        my $patron = Koha::Patrons->find( $borrowernumber );
241
242
        next unless $patron;
243
244
        # Unbless for safety, the patron will end up being deleted
245
        $results->{merged}->{$borrowernumber}->{patron} = $patron->unblessed;
246
247
        while (my ($r, $field) = each(%$resultsets)) {
248
            my $rs = $schema->resultset($r)->search({ $field => $borrowernumber} );
249
            $results->{merged}->{ $borrowernumber }->{updated}->{$r} = $rs->count();
250
            $rs->update( { $field => $keeper });
251
        }
252
253
        $patron->delete();
254
    }
255
256
    $results->{keeper} = $patron_to_keep;
257
258
    return $results;
259
}
260
186
=head3 type
261
=head3 type
187
262
188
=cut
263
=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 / +72 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 = {
112
        Accountline          => 'borrowernumber',
113
        ArticleRequest       => 'borrowernumber',
114
        BorrowerAttribute    => 'borrowernumber',
115
        BorrowerDebarment    => 'borrowernumber',
116
        BorrowerFile         => 'borrowernumber',
117
        BorrowerModification => 'borrowernumber',
118
        ClubEnrollment       => 'borrowernumber',
119
        Issue                => 'borrowernumber',
120
        ItemsLastBorrower    => 'borrowernumber',
121
        Linktracker          => 'borrowernumber',
122
        Message              => 'borrowernumber',
123
        MessageQueue         => 'borrowernumber',
124
        OldIssue             => 'borrowernumber',
125
        OldReserve           => 'borrowernumber',
126
        Rating               => 'borrowernumber',
127
        Reserve              => 'borrowernumber',
128
        Review               => 'borrowernumber',
129
        Statistic            => 'borrowernumber',
130
        SearchHistory        => 'userid',
131
        Suggestion           => 'suggestedby',
132
        TagAll               => 'borrowernumber',
133
        Virtualshelfcontent  => 'borrowernumber',
134
        Virtualshelfshare    => 'borrowernumber',
135
        Virtualshelve        => 'owner',
136
    };
137
138
    my $keeper  = $builder->build( { source => 'Borrower' } )->{borrowernumber};
139
    my $loser_1 = $builder->build( { source => 'Borrower' } )->{borrowernumber};
140
    my $loser_2 = $builder->build( { source => 'Borrower' } )->{borrowernumber};
141
142
    while (my ($r, $field) = each(%$resultsets)) {
143
        $builder->build( { source => $r, value => { $field => $keeper } } );
144
        $builder->build( { source => $r, value => { $field => $loser_1 } } );
145
        $builder->build( { source => $r, value => { $field => $loser_2 } } );
146
147
        my $keeper_rs =
148
          $schema->resultset($r)->search( { $field => $keeper } );
149
        is( $keeper_rs->count(), 1, "Found 1 $r rows for keeper" );
150
151
        my $loser_1_rs =
152
          $schema->resultset($r)->search( { $field => $loser_1 } );
153
        is( $loser_1_rs->count(), 1, "Found 1 $r rows for loser_1" );
154
155
        my $loser_2_rs =
156
          $schema->resultset($r)->search( { $field => $loser_2 } );
157
        is( $loser_2_rs->count(), 1, "Found 1 $r rows for loser_2" );
158
    }
159
160
    my $results = Koha::Patrons->merge(
161
        {
162
            keeper  => $keeper,
163
            patrons => [ $loser_1, $loser_2 ],
164
        }
165
    );
166
167
    while (my ($r, $field) = each(%$resultsets)) {
168
        my $keeper_rs =
169
          $schema->resultset($r)->search( {$field => $keeper } );
170
        is( $keeper_rs->count(), 3, "Found 2 $r rows for keeper" );
171
    }
172
173
	is( Koha::Patrons->find($loser_1), undef, 'Loser 1 has been deleted' );
174
	is( Koha::Patrons->find($loser_2), undef, 'Loser 2 has been deleted' );
175
};
176
106
$schema->storage->txn_rollback();
177
$schema->storage->txn_rollback();
107
178
108
- 

Return to bug 9302