Bug 17443: Make possible to renew patron by later of expiry and current date
[koha.git] / Koha / Patron.pm
1 package Koha::Patron;
2
3 # Copyright ByWater Solutions 2014
4 # Copyright PTFS Europe 2016
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 C4::Context;
26 use C4::Log;
27 use Koha::Database;
28 use Koha::DateUtils;
29 use Koha::Holds;
30 use Koha::Issues;
31 use Koha::OldIssues;
32 use Koha::Patron::Categories;
33 use Koha::Patron::HouseboundProfile;
34 use Koha::Patron::HouseboundRole;
35 use Koha::Patron::Images;
36 use Koha::Patrons;
37 use Koha::Virtualshelves;
38
39 use base qw(Koha::Object);
40
41 =head1 NAME
42
43 Koha::Patron - Koha Patron Object class
44
45 =head1 API
46
47 =head2 Class Methods
48
49 =cut
50
51 =head3 delete
52
53 $patron->delete
54
55 Delete patron's holds, lists and finally the patron.
56
57 Lists owned by the borrower are deleted, but entries from the borrower to
58 other lists are kept.
59
60 =cut
61
62 sub delete {
63     my ($self) = @_;
64
65     my $deleted;
66     $self->_result->result_source->schema->txn_do(
67         sub {
68             # Delete Patron's holds
69             # FIXME Should be $patron->get_holds
70             $_->delete for Koha::Holds->search( { borrowernumber => $self->borrowernumber } );
71
72             # Delete all lists and all shares of this borrower
73             # Consistent with the approach Koha uses on deleting individual lists
74             # Note that entries in virtualshelfcontents added by this borrower to
75             # lists of others will be handled by a table constraint: the borrower
76             # is set to NULL in those entries.
77             # NOTE:
78             # We could handle the above deletes via a constraint too.
79             # But a new BZ report 11889 has been opened to discuss another approach.
80             # Instead of deleting we could also disown lists (based on a pref).
81             # In that way we could save shared and public lists.
82             # The current table constraints support that idea now.
83             # This pref should then govern the results of other routines/methods such as
84             # Koha::Virtualshelf->new->delete too.
85             # FIXME Could be $patron->get_lists
86             $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
87
88             $deleted = $self->SUPER::delete;
89
90             logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
91         }
92     );
93     return $deleted;
94 }
95
96 =head3 guarantor
97
98 Returns a Koha::Patron object for this patron's guarantor
99
100 =cut
101
102 sub guarantor {
103     my ( $self ) = @_;
104
105     return unless $self->guarantorid();
106
107     return Koha::Patrons->find( $self->guarantorid() );
108 }
109
110 sub image {
111     my ( $self ) = @_;
112
113     return Koha::Patron::Images->find( $self->borrowernumber )
114 }
115
116 =head3 guarantees
117
118 Returns the guarantees (list of Koha::Patron) of this patron
119
120 =cut
121
122 sub guarantees {
123     my ( $self ) = @_;
124
125     return Koha::Patrons->search( { guarantorid => $self->borrowernumber } );
126 }
127
128 =head3 housebound_profile
129
130 Returns the HouseboundProfile associated with this patron.
131
132 =cut
133
134 sub housebound_profile {
135     my ( $self ) = @_;
136     my $profile = $self->_result->housebound_profile;
137     return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
138         if ( $profile );
139     return;
140 }
141
142 =head3 housebound_role
143
144 Returns the HouseboundRole associated with this patron.
145
146 =cut
147
148 sub housebound_role {
149     my ( $self ) = @_;
150
151     my $role = $self->_result->housebound_role;
152     return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
153     return;
154 }
155
156 =head3 siblings
157
158 Returns the siblings of this patron.
159
160 =cut
161
162 sub siblings {
163     my ( $self ) = @_;
164
165     my $guarantor = $self->guarantor;
166
167     return unless $guarantor;
168
169     return Koha::Patrons->search(
170         {
171             guarantorid => {
172                 '!=' => undef,
173                 '=' => $guarantor->id,
174             },
175             borrowernumber => {
176                 '!=' => $self->borrowernumber,
177             }
178         }
179     );
180 }
181
182 =head3 wants_check_for_previous_checkout
183
184     $wants_check = $patron->wants_check_for_previous_checkout;
185
186 Return 1 if Koha needs to perform PrevIssue checking, else 0.
187
188 =cut
189
190 sub wants_check_for_previous_checkout {
191     my ( $self ) = @_;
192     my $syspref = C4::Context->preference("checkPrevCheckout");
193
194     # Simple cases
195     ## Hard syspref trumps all
196     return 1 if ($syspref eq 'hardyes');
197     return 0 if ($syspref eq 'hardno');
198     ## Now, patron pref trumps all
199     return 1 if ($self->checkprevcheckout eq 'yes');
200     return 0 if ($self->checkprevcheckout eq 'no');
201
202     # More complex: patron inherits -> determine category preference
203     my $checkPrevCheckoutByCat = Koha::Patron::Categories
204         ->find($self->categorycode)->checkprevcheckout;
205     return 1 if ($checkPrevCheckoutByCat eq 'yes');
206     return 0 if ($checkPrevCheckoutByCat eq 'no');
207
208     # Finally: category preference is inherit, default to 0
209     if ($syspref eq 'softyes') {
210         return 1;
211     } else {
212         return 0;
213     }
214 }
215
216 =head3 do_check_for_previous_checkout
217
218     $do_check = $patron->do_check_for_previous_checkout($item);
219
220 Return 1 if the bib associated with $ITEM has previously been checked out to
221 $PATRON, 0 otherwise.
222
223 =cut
224
225 sub do_check_for_previous_checkout {
226     my ( $self, $item ) = @_;
227
228     # Find all items for bib and extract item numbers.
229     my @items = Koha::Items->search({biblionumber => $item->{biblionumber}});
230     my @item_nos;
231     foreach my $item (@items) {
232         push @item_nos, $item->itemnumber;
233     }
234
235     # Create (old)issues search criteria
236     my $criteria = {
237         borrowernumber => $self->borrowernumber,
238         itemnumber => \@item_nos,
239     };
240
241     # Check current issues table
242     my $issues = Koha::Issues->search($criteria);
243     return 1 if $issues->count; # 0 || N
244
245     # Check old issues table
246     my $old_issues = Koha::OldIssues->search($criteria);
247     return $old_issues->count;  # 0 || N
248 }
249
250 =head2 is_debarred
251
252 my $debarment_expiration = $patron->is_debarred;
253
254 Returns the date a patron debarment will expire, or undef if the patron is not
255 debarred
256
257 =cut
258
259 sub is_debarred {
260     my ($self) = @_;
261
262     return unless $self->debarred;
263     return $self->debarred
264       if $self->debarred =~ '^9999'
265       or dt_from_string( $self->debarred ) > dt_from_string;
266     return;
267 }
268
269 =head2 update_password
270
271 my $updated = $patron->update_password( $userid, $password );
272
273 Update the userid and the password of a patron.
274 If the userid already exists, returns and let DBIx::Class warns
275 This will add an entry to action_logs if BorrowersLog is set.
276
277 =cut
278
279 sub update_password {
280     my ( $self, $userid, $password ) = @_;
281     eval { $self->userid($userid)->store; };
282     return if $@; # Make sure the userid is not already in used by another patron
283     $self->password($password)->store;
284     logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
285     return 1;
286 }
287
288 =head3 renew_account
289
290 my $new_expiry_date = $patron->renew_account
291
292 Extending the subscription to the expiry date.
293
294 =cut
295
296 sub renew_account {
297     my ($self) = @_;
298     my $date;
299     if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
300         $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
301     } else {
302         $date =
303             C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
304             ? dt_from_string( $self->dateexpiry )
305             : dt_from_string;
306     }
307     my $patron_category = Koha::Patron::Categories->find( $self->categorycode );    # FIXME Should be $self->category
308     my $expiry_date     = $patron_category->get_expiry_date($date);
309
310     $self->dateexpiry($expiry_date)->store;
311
312     $self->add_enrolment_fee_if_needed;
313
314     logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
315     return dt_from_string( $expiry_date )->truncate( to => 'day' );
316 }
317
318 =head2 has_overdues
319
320 my $has_overdues = $patron->has_overdues;
321
322 Returns the number of patron's overdues
323
324 =cut
325
326 sub has_overdues {
327     my ($self) = @_;
328     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
329     return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
330 }
331
332 =head2 track_login
333
334     $patron->track_login;
335     $patron->track_login({ force => 1 });
336
337     Tracks a (successful) login attempt.
338     The preference TrackLastPatronActivity must be enabled. Or you
339     should pass the force parameter.
340
341 =cut
342
343 sub track_login {
344     my ( $self, $params ) = @_;
345     return if
346         !$params->{force} &&
347         !C4::Context->preference('TrackLastPatronActivity');
348     $self->lastseen( dt_from_string() )->store;
349 }
350
351 =head2 move_to_deleted
352
353 my $is_moved = $patron->move_to_deleted;
354
355 Move a patron to the deletedborrowers table.
356 This can be done before deleting a patron, to make sure the data are not completely deleted.
357
358 =cut
359
360 sub move_to_deleted {
361     my ($self) = @_;
362     my $patron_infos = $self->unblessed;
363     return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
364 }
365
366 =head3 article_requests
367
368 my @requests = $borrower->article_requests();
369 my $requests = $borrower->article_requests();
370
371 Returns either a list of ArticleRequests objects,
372 or an ArtitleRequests object, depending on the
373 calling context.
374
375 =cut
376
377 sub article_requests {
378     my ( $self ) = @_;
379
380     $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
381
382     return $self->{_article_requests};
383 }
384
385 =head3 article_requests_current
386
387 my @requests = $patron->article_requests_current
388
389 Returns the article requests associated with this patron that are incomplete
390
391 =cut
392
393 sub article_requests_current {
394     my ( $self ) = @_;
395
396     $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
397         {
398             borrowernumber => $self->id(),
399             -or          => [
400                 { status => Koha::ArticleRequest::Status::Pending },
401                 { status => Koha::ArticleRequest::Status::Processing }
402             ]
403         }
404     );
405
406     return $self->{_article_requests_current};
407 }
408
409 =head3 article_requests_finished
410
411 my @requests = $biblio->article_requests_finished
412
413 Returns the article requests associated with this patron that are completed
414
415 =cut
416
417 sub article_requests_finished {
418     my ( $self, $borrower ) = @_;
419
420     $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
421         {
422             borrowernumber => $self->id(),
423             -or          => [
424                 { status => Koha::ArticleRequest::Status::Completed },
425                 { status => Koha::ArticleRequest::Status::Canceled }
426             ]
427         }
428     );
429
430     return $self->{_article_requests_finished};
431 }
432
433 =head3 add_enrolment_fee_if_needed
434
435 my $enrolment_fee = $patron->add_enrolment_fee_if_needed;
436
437 Add enrolment fee for a patron if needed.
438
439 =cut
440
441 sub add_enrolment_fee_if_needed {
442     my ($self) = @_;
443     my $patron_category = Koha::Patron::Categories->find( $self->categorycode );
444     my $enrolment_fee = $patron_category->enrolmentfee;
445     if ( $enrolment_fee && $enrolment_fee > 0 ) {
446         # insert fee in patron debts
447         C4::Accounts::manualinvoice( $self->borrowernumber, '', '', 'A', $enrolment_fee );
448     }
449     return $enrolment_fee || 0;
450 }
451
452 =head3 type
453
454 =cut
455
456 sub _type {
457     return 'Borrower';
458 }
459
460 =head1 AUTHOR
461
462 Kyle M Hall <kyle@bywatersolutions.com>
463 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
464
465 =cut
466
467 1;