Bug 9302: Add ability to merge patron records
[koha.git] / Koha / Patrons.pm
1 package Koha::Patrons;
2
3 # Copyright 2014 ByWater Solutions
4 # Copyright 2016 Koha Development Team
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it under the
9 # terms of the GNU General Public License as published by the Free Software
10 # Foundation; either version 3 of the License, or (at your option) any later
11 # version.
12 #
13 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
14 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License along
18 # with Koha; if not, write to the Free Software Foundation, Inc.,
19 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21 use Modern::Perl;
22
23 use Carp;
24
25 use Koha::Database;
26 use Koha::DateUtils;
27
28 use Koha::ArticleRequests;
29 use Koha::ArticleRequest::Status;
30 use Koha::Patron;
31
32 use base qw(Koha::Objects);
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
61 =head1 NAME
62
63 Koha::Patron - Koha Patron Object class
64
65 =head1 API
66
67 =head2 Class Methods
68
69 =cut
70
71 =head3 search_limited
72
73 my $patrons = Koha::Patrons->search_limit( $params, $attributes );
74
75 Returns all the patrons the logged in user is allowed to see
76
77 =cut
78
79 sub search_limited {
80     my ( $self, $params, $attributes ) = @_;
81
82     my $userenv = C4::Context->userenv;
83     my @restricted_branchcodes;
84     if ( $userenv and $userenv->{number} ) {
85         my $logged_in_user = Koha::Patrons->find( $userenv->{number} );
86         @restricted_branchcodes = $logged_in_user->libraries_where_can_see_patrons;
87     }
88     $params->{'me.branchcode'} = { -in => \@restricted_branchcodes } if @restricted_branchcodes;
89     return $self->search( $params, $attributes );
90 }
91
92 =head3 search_housebound_choosers
93
94 Returns all Patrons which are Housebound choosers.
95
96 =cut
97
98 sub search_housebound_choosers {
99     my ( $self ) = @_;
100     my $cho = $self->_resultset
101         ->search_related('housebound_role', {
102             housebound_chooser => 1,
103         })->search_related('borrowernumber');
104     return Koha::Patrons->_new_from_dbic($cho);
105 }
106
107 =head3 search_housebound_deliverers
108
109 Returns all Patrons which are Housebound deliverers.
110
111 =cut
112
113 sub search_housebound_deliverers {
114     my ( $self ) = @_;
115     my $del = $self->_resultset
116         ->search_related('housebound_role', {
117             housebound_deliverer => 1,
118         })->search_related('borrowernumber');
119     return Koha::Patrons->_new_from_dbic($del);
120 }
121
122 =head3 search_upcoming_membership_expires
123
124 my $patrons = Koha::Patrons->search_upcoming_membership_expires();
125
126 The 'before' and 'after' represent the number of days before/after the date
127 that is set by the preference MembershipExpiryDaysNotice.
128 If the pref is 14, before 2 and after 3 then you will get all expires
129 from 12 to 17 days.
130
131 =cut
132
133 sub search_upcoming_membership_expires {
134     my ( $self, $params ) = @_;
135     my $before = $params->{before} || 0;
136     my $after  = $params->{after} || 0;
137     delete $params->{before};
138     delete $params->{after};
139
140     my $days = C4::Context->preference("MembershipExpiryDaysNotice") || 0;
141     my $date_before = dt_from_string->add( days => $days - $before );
142     my $date_after = dt_from_string->add( days => $days + $after );
143     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
144
145     $params->{dateexpiry} = {
146         ">=" => $dtf->format_date( $date_before ),
147         "<=" => $dtf->format_date( $date_after ),
148     };
149     return $self->SUPER::search(
150         $params, { join => ['branchcode', 'categorycode'] }
151     );
152 }
153
154 =head3 guarantor
155
156 Returns a Koha::Patron object for this borrower's guarantor
157
158 =cut
159
160 sub guarantor {
161     my ( $self ) = @_;
162
163     return Koha::Patrons->find( $self->guarantorid() );
164 }
165
166 =head3 search_patrons_to_anonymise
167
168     my $patrons = Koha::Patrons->search_patrons_to_anonymise( { before => $older_than_date, [ library => $library ] } );
169
170 This method returns all patrons who has an issue history older than a given date.
171
172 =cut
173
174 sub search_patrons_to_anonymise {
175     my ( $class, $params ) = @_;
176     my $older_than_date = $params->{before};
177     my $library         = $params->{library};
178     $older_than_date = $older_than_date ? dt_from_string($older_than_date) : dt_from_string;
179     $library ||=
180       ( C4::Context->preference('IndependentBranches') && C4::Context->userenv && !C4::Context->IsSuperLibrarian() && C4::Context->userenv->{branch} )
181       ? C4::Context->userenv->{branch}
182       : undef;
183
184     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
185     my $rs = $class->_resultset->search(
186         {   returndate                  => { '<'   =>  $dtf->format_datetime($older_than_date), },
187             'old_issues.borrowernumber' => { 'not' => undef },
188             privacy                     => { '<>'  => 0 },                  # Keep forever
189             ( $library ? ( 'old_issues.branchcode' => $library ) : () ),
190         },
191         {   join     => ["old_issues"],
192             distinct => 1,
193         }
194     );
195     return Koha::Patrons->_new_from_dbic($rs);
196 }
197
198 =head3 anonymise_issue_history
199
200     Koha::Patrons->search->anonymise_issue_history( { [ before => $older_than_date ] } );
201
202 Anonymise issue history (old_issues) for all patrons older than the given date (optional).
203 To make sure all the conditions are met, the caller has the responsibility to
204 call search_patrons_to_anonymise to filter the Koha::Patrons set
205
206 =cut
207
208 sub anonymise_issue_history {
209     my ( $self, $params ) = @_;
210
211     my $older_than_date = $params->{before};
212
213     $older_than_date = dt_from_string $older_than_date if $older_than_date;
214
215     # The default of 0 does not work due to foreign key constraints
216     # The anonymisation should not fail quietly if AnonymousPatron is not a valid entry
217     # Set it to undef (NULL)
218     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
219     my $nb_rows = 0;
220     while ( my $patron = $self->next ) {
221         my $old_issues_to_anonymise = $patron->old_checkouts->search(
222         {
223             (
224                 $older_than_date
225                 ? ( returndate =>
226                       { '<' => $dtf->format_datetime($older_than_date) } )
227                 : ()
228             )
229         }
230         );
231         my $anonymous_patron = C4::Context->preference('AnonymousPatron') || undef;
232         $nb_rows += $old_issues_to_anonymise->update( { 'old_issues.borrowernumber' => $anonymous_patron } );
233     }
234     return $nb_rows;
235 }
236
237 =head3 merge
238
239     Koha::Patrons->search->merge( { keeper => $borrowernumber, patrons => \@borrowernumbers } );
240
241     This subroutine merges a list of patrons into another patron record. This is accomplished by finding
242     all related patron ids for the patrons to be merged in other tables and changing the ids to be that
243     of the keeper patron.
244
245 =cut
246
247 sub merge {
248     my ( $self, $params ) = @_;
249
250     my $keeper          = $params->{keeper};
251     my @borrowernumbers = @{ $params->{patrons} };
252
253     my $patron_to_keep = Koha::Patrons->find( $keeper );
254     return unless $patron_to_keep;
255
256     # Ensure the keeper isn't in the list of patrons to merge
257     @borrowernumbers = grep { $_ ne $keeper } @borrowernumbers;
258
259     my $schema = Koha::Database->new()->schema();
260
261     my $results;
262
263     foreach my $borrowernumber (@borrowernumbers) {
264         my $patron = Koha::Patrons->find( $borrowernumber );
265
266         next unless $patron;
267
268         # Unbless for safety, the patron will end up being deleted
269         $results->{merged}->{$borrowernumber}->{patron} = $patron->unblessed;
270
271         while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
272             my $rs = $schema->resultset($r)->search({ $field => $borrowernumber} );
273             $results->{merged}->{ $borrowernumber }->{updated}->{$r} = $rs->count();
274             $rs->update( { $field => $keeper });
275         }
276
277         $patron->delete();
278     }
279
280     $results->{keeper} = $patron_to_keep;
281
282     return $results;
283 }
284
285 =head3 type
286
287 =cut
288
289 sub _type {
290     return 'Borrower';
291 }
292
293 sub object_class {
294     return 'Koha::Patron';
295 }
296
297 =head1 AUTHOR
298
299 Kyle M Hall <kyle@bywatersolutions.com>
300
301 =cut
302
303 1;