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

(-)a/Koha/Patrons.pm (+75 lines)
Lines 31-36 use Koha::Patron; Link Here
31
31
32
use base qw(Koha::Objects);
32
use base qw(Koha::Objects);
33
33
34
our $RESULTSET_PATRON_ID_MAPPING => {
35
    Accountline          => 'borrowernumber',
36
    ArticleRequest       => 'borrowernumber',
37
    BorrowerAttribute    => 'borrowernumber',
38
    BorrowerDebarment    => 'borrowernumber',
39
    BorrowerFile         => 'borrowernumber',
40
    BorrowerModification => 'borrowernumber',
41
    ClubEnrollment       => 'borrowernumber',
42
    Issue                => 'borrowernumber',
43
    ItemsLastBorrower    => 'borrowernumber',
44
    Linktracker          => 'borrowernumber',
45
    Message              => 'borrowernumber',
46
    MessageQueue         => 'borrowernumber',
47
    OldIssue             => 'borrowernumber',
48
    OldReserve           => 'borrowernumber',
49
    Rating               => 'borrowernumber',
50
    Reserve              => 'borrowernumber',
51
    Review               => 'borrowernumber',
52
    Statistic            => 'borrowernumber',
53
    SearchHistory        => 'userid',
54
    Suggestion           => 'suggestedby',
55
    TagAll               => 'borrowernumber',
56
    Virtualshelfcontent  => 'borrowernumber',
57
    Virtualshelfshare    => 'borrowernumber',
58
    Virtualshelve        => 'owner',
59
};
60
34
=head1 NAME
61
=head1 NAME
35
62
36
Koha::Patron - Koha Patron Object class
63
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 } );
210
    $old_issues_to_anonymise->update( { 'old_issues.borrowernumber' => $anonymous_patron } );
184
}
211
}
185
212
213
=head3 merge
214
215
    Koha::Patrons->search->merge( { keeper => $borrowernumber, patrons => \@borrowernumbers } );
216
217
    This subroutine merges a list of patrons into another patron record. This is accomplished by finding
218
    all related patron ids for the patrons to be merged in other tables and changing the ids to be that
219
    of the keeper patron.
220
221
=cut
222
223
sub merge {
224
    my ( $self, $params ) = @_;
225
226
    my $keeper          = $params->{keeper};
227
    my @borrowernumbers = @{ $params->{patrons} };
228
229
    my $patron_to_keep = Koha::Patrons->find( $keeper );
230
    return unless $patron_to_keep;
231
232
    # Ensure the keeper isn't in the list of patrons to merge
233
    @borrowernumbers = grep { $_ ne $keeper } @borrowernumbers;
234
235
    my $schema = Koha::Database->new()->schema();
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(%$RESULTSET_PATRON_ID_MAPPING)) {
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 | html %]'[% END %]</span></h3>
377
              <h3>Patrons found for: <span id="searchpattern">[% IF searchmember %] for '[% searchmember | html %]'[% 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