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 186-191 sub anonymise_issue_history { Link Here
186
    return $nb_rows;
213
    return $nb_rows;
187
}
214
}
188
215
216
=head3 merge
217
218
    Koha::Patrons->search->merge( { keeper => $borrowernumber, patrons => \@borrowernumbers } );
219
220
    This subroutine merges a list of patrons into another patron record. This is accomplished by finding
221
    all related patron ids for the patrons to be merged in other tables and changing the ids to be that
222
    of the keeper patron.
223
224
=cut
225
226
sub merge {
227
    my ( $self, $params ) = @_;
228
229
    my $keeper          = $params->{keeper};
230
    my @borrowernumbers = @{ $params->{patrons} };
231
232
    my $patron_to_keep = Koha::Patrons->find( $keeper );
233
    return unless $patron_to_keep;
234
235
    # Ensure the keeper isn't in the list of patrons to merge
236
    @borrowernumbers = grep { $_ ne $keeper } @borrowernumbers;
237
238
    my $schema = Koha::Database->new()->schema();
239
240
    my $results;
241
242
    foreach my $borrowernumber (@borrowernumbers) {
243
        my $patron = Koha::Patrons->find( $borrowernumber );
244
245
        next unless $patron;
246
247
        # Unbless for safety, the patron will end up being deleted
248
        $results->{merged}->{$borrowernumber}->{patron} = $patron->unblessed;
249
250
        while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
251
            my $rs = $schema->resultset($r)->search({ $field => $borrowernumber} );
252
            $results->{merged}->{ $borrowernumber }->{updated}->{$r} = $rs->count();
253
            $rs->update( { $field => $keeper });
254
        }
255
256
        $patron->delete();
257
    }
258
259
    $results->{keeper} = $patron_to_keep;
260
261
    return $results;
262
}
263
189
=head3 type
264
=head3 type
190
265
191
=cut
266
=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 (-2 / +32 lines)
Lines 48-53 Link Here
48
                <div class="btn-group">
48
                <div class="btn-group">
49
                    <button class="btn btn-default btn-xs list-remove" type="submit"><i class="fa fa-trash"></i> Remove selected</button>
49
                    <button class="btn btn-default btn-xs list-remove" type="submit"><i class="fa fa-trash"></i> Remove selected</button>
50
                </div>
50
                </div>
51
                |
52
                <div class="btn-group">
53
                    <button class="btn btn-default btn-xs merge-patrons"><i class="fa fa-compress"></i> Merge selected patrons</button>
54
                </div>
51
            </div>
55
            </div>
52
56
53
            <table id="patron-list-table">
57
            <table id="patron-list-table">
Lines 68-74 Link Here
68
                <tbody>
72
                <tbody>
69
                    [% FOREACH p IN list.patron_list_patrons %]
73
                    [% FOREACH p IN list.patron_list_patrons %]
70
                        <tr>
74
                        <tr>
71
                            <td><input type="checkbox" name="patrons_to_remove" value="[% p.patron_list_patron_id %]" /></td>
75
                            <td>
76
                                <input type="checkbox" name="patrons_to_remove" class="selection" value="[% p.patron_list_patron_id %]" />
77
                                <input type="hidden" id="borrowernumber_[% p.patron_list_patron_id %]" value="[% p.borrowernumber.id %]" />
78
                            </td>
72
                            <td>
79
                            <td>
73
                                <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% p.borrowernumber.borrowernumber %]">
80
                                <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% p.borrowernumber.borrowernumber %]">
74
                                    [% p.borrowernumber.cardnumber %]
81
                                    [% p.borrowernumber.cardnumber %]
Lines 93-99 Link Here
93
            </table>
100
            </table>
94
101
95
            <input type="hidden" name="patron_list_id" value="[% list.patron_list_id %]" />
102
            <input type="hidden" name="patron_list_id" value="[% list.patron_list_id %]" />
96
            <button type="submit" class="btn btn-default btn-sm"><i class="fa fa-trash" aria-hidden="true"></i> Remove selected patrons</button>
103
            <button type="submit" class="btn btn-default btn-sm list-remove"><i class="fa fa-trash" aria-hidden="true"></i> Remove selected patrons</button>
104
            <button class="btn btn-default btn-sm merge-patrons" type="submit"><i class="fa fa-compress"></i> Merge selected patrons</button>
97
        </form>
105
        </form>
98
106
99
            </div>
107
            </div>
Lines 159-164 Link Here
159
                    return false;
167
                    return false;
160
                }
168
                }
161
            });
169
            });
170
171
            $('.merge-patrons').on('click', function() {
172
                var checkedItems = $("input:checked");
173
                if ($(checkedItems).length < 2) {
174
                    alert(_("You must select one or more patrons to remove"));
175
                    return false;
176
                }
177
                $(checkedItems).parents('tr').addClass("warn");
178
                if (confirm(_("Are you sure you want to remove the selected patrons?"))) {
179
                    var merge_patrons_url = '/cgi-bin/koha/members/merge-patrons.pl?' +
180
                        $('.selection:checked')
181
                        .map(function() {
182
                            return "id=" + $( '#borrowernumber_' + $(this).val() ).val()
183
                        }).get().join('&');
184
185
                    window.location.href = merge_patrons_url;
186
                    return false;
187
                } else {
188
                    $(checkedItems).parents('tr').removeClass("warn");
189
                    return false;
190
                }
191
            });
162
        });
192
        });
163
    </script>
193
    </script>
164
[% END %]
194
[% END %]
(-)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 104-108 foreach my $b ( $patrons->as_list() ) { Link Here
104
    is( $b->categorycode(), $categorycode, "Iteration returns a patron object" );
104
    is( $b->categorycode(), $categorycode, "Iteration returns a patron object" );
105
}
105
}
106
106
107
subtest 'Test Koha::Patrons::merge' => sub {
108
    plan tests => 98;
109
110
    my $schema = Koha::Database->new()->schema();
111
112
    my $resultsets = $Koha::Patrons::RESULTSET_PATRON_ID_MAPPING;
113
114
    my $keeper  = $builder->build( { source => 'Borrower' } )->{borrowernumber};
115
    my $loser_1 = $builder->build( { source => 'Borrower' } )->{borrowernumber};
116
    my $loser_2 = $builder->build( { source => 'Borrower' } )->{borrowernumber};
117
118
    while (my ($r, $field) = each(%$resultsets)) {
119
        $builder->build( { source => $r, value => { $field => $keeper } } );
120
        $builder->build( { source => $r, value => { $field => $loser_1 } } );
121
        $builder->build( { source => $r, value => { $field => $loser_2 } } );
122
123
        my $keeper_rs =
124
          $schema->resultset($r)->search( { $field => $keeper } );
125
        is( $keeper_rs->count(), 1, "Found 1 $r rows for keeper" );
126
127
        my $loser_1_rs =
128
          $schema->resultset($r)->search( { $field => $loser_1 } );
129
        is( $loser_1_rs->count(), 1, "Found 1 $r rows for loser_1" );
130
131
        my $loser_2_rs =
132
          $schema->resultset($r)->search( { $field => $loser_2 } );
133
        is( $loser_2_rs->count(), 1, "Found 1 $r rows for loser_2" );
134
    }
135
136
    my $results = Koha::Patrons->merge(
137
        {
138
            keeper  => $keeper,
139
            patrons => [ $loser_1, $loser_2 ],
140
        }
141
    );
142
143
    while (my ($r, $field) = each(%$resultsets)) {
144
        my $keeper_rs =
145
          $schema->resultset($r)->search( {$field => $keeper } );
146
        is( $keeper_rs->count(), 3, "Found 2 $r rows for keeper" );
147
    }
148
149
    is( Koha::Patrons->find($loser_1), undef, 'Loser 1 has been deleted' );
150
    is( Koha::Patrons->find($loser_2), undef, 'Loser 2 has been deleted' );
151
};
152
107
$schema->storage->txn_rollback();
153
$schema->storage->txn_rollback();
108
154
109
- 

Return to bug 9302