Bug 17585: Add the Koha::Patron->get_account_lines method
[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
97 =head3 category
98
99 my $patron_category = $patron->category
100
101 Return the patron category for this patron
102
103 =cut
104
105 sub category {
106     my ( $self ) = @_;
107     return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
108 }
109
110 =head3 guarantor
111
112 Returns a Koha::Patron object for this patron's guarantor
113
114 =cut
115
116 sub guarantor {
117     my ( $self ) = @_;
118
119     return unless $self->guarantorid();
120
121     return Koha::Patrons->find( $self->guarantorid() );
122 }
123
124 sub image {
125     my ( $self ) = @_;
126
127     return Koha::Patron::Images->find( $self->borrowernumber )
128 }
129
130 sub library {
131     my ( $self ) = @_;
132     return Koha::Library->_new_from_dbic($self->_result->branchcode)
133 }
134
135 =head3 guarantees
136
137 Returns the guarantees (list of Koha::Patron) of this patron
138
139 =cut
140
141 sub guarantees {
142     my ( $self ) = @_;
143
144     return Koha::Patrons->search( { guarantorid => $self->borrowernumber } );
145 }
146
147 =head3 housebound_profile
148
149 Returns the HouseboundProfile associated with this patron.
150
151 =cut
152
153 sub housebound_profile {
154     my ( $self ) = @_;
155     my $profile = $self->_result->housebound_profile;
156     return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
157         if ( $profile );
158     return;
159 }
160
161 =head3 housebound_role
162
163 Returns the HouseboundRole associated with this patron.
164
165 =cut
166
167 sub housebound_role {
168     my ( $self ) = @_;
169
170     my $role = $self->_result->housebound_role;
171     return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
172     return;
173 }
174
175 =head3 siblings
176
177 Returns the siblings of this patron.
178
179 =cut
180
181 sub siblings {
182     my ( $self ) = @_;
183
184     my $guarantor = $self->guarantor;
185
186     return unless $guarantor;
187
188     return Koha::Patrons->search(
189         {
190             guarantorid => {
191                 '!=' => undef,
192                 '=' => $guarantor->id,
193             },
194             borrowernumber => {
195                 '!=' => $self->borrowernumber,
196             }
197         }
198     );
199 }
200
201 =head3 wants_check_for_previous_checkout
202
203     $wants_check = $patron->wants_check_for_previous_checkout;
204
205 Return 1 if Koha needs to perform PrevIssue checking, else 0.
206
207 =cut
208
209 sub wants_check_for_previous_checkout {
210     my ( $self ) = @_;
211     my $syspref = C4::Context->preference("checkPrevCheckout");
212
213     # Simple cases
214     ## Hard syspref trumps all
215     return 1 if ($syspref eq 'hardyes');
216     return 0 if ($syspref eq 'hardno');
217     ## Now, patron pref trumps all
218     return 1 if ($self->checkprevcheckout eq 'yes');
219     return 0 if ($self->checkprevcheckout eq 'no');
220
221     # More complex: patron inherits -> determine category preference
222     my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
223     return 1 if ($checkPrevCheckoutByCat eq 'yes');
224     return 0 if ($checkPrevCheckoutByCat eq 'no');
225
226     # Finally: category preference is inherit, default to 0
227     if ($syspref eq 'softyes') {
228         return 1;
229     } else {
230         return 0;
231     }
232 }
233
234 =head3 do_check_for_previous_checkout
235
236     $do_check = $patron->do_check_for_previous_checkout($item);
237
238 Return 1 if the bib associated with $ITEM has previously been checked out to
239 $PATRON, 0 otherwise.
240
241 =cut
242
243 sub do_check_for_previous_checkout {
244     my ( $self, $item ) = @_;
245
246     # Find all items for bib and extract item numbers.
247     my @items = Koha::Items->search({biblionumber => $item->{biblionumber}});
248     my @item_nos;
249     foreach my $item (@items) {
250         push @item_nos, $item->itemnumber;
251     }
252
253     # Create (old)issues search criteria
254     my $criteria = {
255         borrowernumber => $self->borrowernumber,
256         itemnumber => \@item_nos,
257     };
258
259     # Check current issues table
260     my $issues = Koha::Issues->search($criteria);
261     return 1 if $issues->count; # 0 || N
262
263     # Check old issues table
264     my $old_issues = Koha::OldIssues->search($criteria);
265     return $old_issues->count;  # 0 || N
266 }
267
268 =head2 is_debarred
269
270 my $debarment_expiration = $patron->is_debarred;
271
272 Returns the date a patron debarment will expire, or undef if the patron is not
273 debarred
274
275 =cut
276
277 sub is_debarred {
278     my ($self) = @_;
279
280     return unless $self->debarred;
281     return $self->debarred
282       if $self->debarred =~ '^9999'
283       or dt_from_string( $self->debarred ) > dt_from_string;
284     return;
285 }
286
287 =head2 is_expired
288
289 my $is_expired = $patron->is_expired;
290
291 Returns 1 if the patron is expired or 0;
292
293 =cut
294
295 sub is_expired {
296     my ($self) = @_;
297     return 0 unless $self->dateexpiry;
298     return 0 if $self->dateexpiry eq '0000-00-00';
299     return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
300     return 0;
301 }
302
303 =head2 is_going_to_expire
304
305 my $is_going_to_expire = $patron->is_going_to_expire;
306
307 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
308
309 =cut
310
311 sub is_going_to_expire {
312     my ($self) = @_;
313
314     my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
315
316     return 0 unless $delay;
317     return 0 unless $self->dateexpiry;
318     return 0 if $self->dateexpiry eq '0000-00-00';
319     return 1 if dt_from_string( $self->dateexpiry )->subtract( days => $delay ) < dt_from_string->truncate( to => 'day' );
320     return 0;
321 }
322
323 =head2 update_password
324
325 my $updated = $patron->update_password( $userid, $password );
326
327 Update the userid and the password of a patron.
328 If the userid already exists, returns and let DBIx::Class warns
329 This will add an entry to action_logs if BorrowersLog is set.
330
331 =cut
332
333 sub update_password {
334     my ( $self, $userid, $password ) = @_;
335     eval { $self->userid($userid)->store; };
336     return if $@; # Make sure the userid is not already in used by another patron
337     $self->password($password)->store;
338     logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
339     return 1;
340 }
341
342 =head3 renew_account
343
344 my $new_expiry_date = $patron->renew_account
345
346 Extending the subscription to the expiry date.
347
348 =cut
349
350 sub renew_account {
351     my ($self) = @_;
352     my $date;
353     if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
354         $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
355     } else {
356         $date =
357             C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
358             ? dt_from_string( $self->dateexpiry )
359             : dt_from_string;
360     }
361     my $expiry_date = $self->category->get_expiry_date($date);
362
363     $self->dateexpiry($expiry_date)->store;
364
365     $self->add_enrolment_fee_if_needed;
366
367     logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
368     return dt_from_string( $expiry_date )->truncate( to => 'day' );
369 }
370
371 =head2 has_overdues
372
373 my $has_overdues = $patron->has_overdues;
374
375 Returns the number of patron's overdues
376
377 =cut
378
379 sub has_overdues {
380     my ($self) = @_;
381     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
382     return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
383 }
384
385 =head2 track_login
386
387     $patron->track_login;
388     $patron->track_login({ force => 1 });
389
390     Tracks a (successful) login attempt.
391     The preference TrackLastPatronActivity must be enabled. Or you
392     should pass the force parameter.
393
394 =cut
395
396 sub track_login {
397     my ( $self, $params ) = @_;
398     return if
399         !$params->{force} &&
400         !C4::Context->preference('TrackLastPatronActivity');
401     $self->lastseen( dt_from_string() )->store;
402 }
403
404 =head2 move_to_deleted
405
406 my $is_moved = $patron->move_to_deleted;
407
408 Move a patron to the deletedborrowers table.
409 This can be done before deleting a patron, to make sure the data are not completely deleted.
410
411 =cut
412
413 sub move_to_deleted {
414     my ($self) = @_;
415     my $patron_infos = $self->unblessed;
416     return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
417 }
418
419 =head3 article_requests
420
421 my @requests = $borrower->article_requests();
422 my $requests = $borrower->article_requests();
423
424 Returns either a list of ArticleRequests objects,
425 or an ArtitleRequests object, depending on the
426 calling context.
427
428 =cut
429
430 sub article_requests {
431     my ( $self ) = @_;
432
433     $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
434
435     return $self->{_article_requests};
436 }
437
438 =head3 article_requests_current
439
440 my @requests = $patron->article_requests_current
441
442 Returns the article requests associated with this patron that are incomplete
443
444 =cut
445
446 sub article_requests_current {
447     my ( $self ) = @_;
448
449     $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
450         {
451             borrowernumber => $self->id(),
452             -or          => [
453                 { status => Koha::ArticleRequest::Status::Pending },
454                 { status => Koha::ArticleRequest::Status::Processing }
455             ]
456         }
457     );
458
459     return $self->{_article_requests_current};
460 }
461
462 =head3 article_requests_finished
463
464 my @requests = $biblio->article_requests_finished
465
466 Returns the article requests associated with this patron that are completed
467
468 =cut
469
470 sub article_requests_finished {
471     my ( $self, $borrower ) = @_;
472
473     $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
474         {
475             borrowernumber => $self->id(),
476             -or          => [
477                 { status => Koha::ArticleRequest::Status::Completed },
478                 { status => Koha::ArticleRequest::Status::Canceled }
479             ]
480         }
481     );
482
483     return $self->{_article_requests_finished};
484 }
485
486 =head3 add_enrolment_fee_if_needed
487
488 my $enrolment_fee = $patron->add_enrolment_fee_if_needed;
489
490 Add enrolment fee for a patron if needed.
491
492 =cut
493
494 sub add_enrolment_fee_if_needed {
495     my ($self) = @_;
496     my $enrolment_fee = $self->category->enrolmentfee;
497     if ( $enrolment_fee && $enrolment_fee > 0 ) {
498         # insert fee in patron debts
499         C4::Accounts::manualinvoice( $self->borrowernumber, '', '', 'A', $enrolment_fee );
500     }
501     return $enrolment_fee || 0;
502 }
503
504 =head3 get_overdues
505
506 my $overdue_items = $patron->get_overdues
507
508 Return the overdued items
509
510 =cut
511
512 sub get_overdues {
513     my ($self) = @_;
514     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
515     my $issues = Koha::Issues->search(
516         {
517             'me.borrowernumber' => $self->borrowernumber,
518             'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
519         },
520         {
521             prefetch => { item => { biblio => 'biblioitems' } },
522         }
523     );
524     return $issues;
525 }
526
527 =head3 get_age
528
529 my $age = $patron->get_age
530
531 Return the age of the patron
532
533 =cut
534
535 sub get_age {
536     my ($self)    = @_;
537     my $today_str = dt_from_string->strftime("%Y-%m-%d");
538     my $dob_str   = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
539
540     my ( $dob_y,   $dob_m,   $dob_d )   = split /-/, $dob_str;
541     my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
542
543     my $age = $today_y - $dob_y;
544     if ( $dob_m . $dob_d > $today_m . $today_d ) {
545         $age--;
546     }
547
548     return $age;
549 }
550
551 =head3 get_account_lines
552
553 my $fines = $patron->get_account_lines
554
555 =cut
556
557 sub get_account_lines {
558     my ($self) = @_;
559     my $account_lines = $self->_result->accountlines;
560     return Koha::Account::Lines->_new_from_dbic($account_lines);
561 }
562
563 =head3 type
564
565 =cut
566
567 sub _type {
568     return 'Borrower';
569 }
570
571 =head1 AUTHOR
572
573 Kyle M Hall <kyle@bywatersolutions.com>
574 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
575
576 =cut
577
578 1;