Bug 16497: Add /api/v1/libraries
[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 use List::MoreUtils qw( uniq );
25 use JSON qw( to_json );
26 use Text::Unaccent qw( unac_string );
27
28 use C4::Accounts;
29 use C4::Context;
30 use C4::Log;
31 use Koha::AuthUtils;
32 use Koha::Checkouts;
33 use Koha::Database;
34 use Koha::DateUtils;
35 use Koha::Exceptions::Password;
36 use Koha::Holds;
37 use Koha::Old::Checkouts;
38 use Koha::Patron::Categories;
39 use Koha::Patron::HouseboundProfile;
40 use Koha::Patron::HouseboundRole;
41 use Koha::Patron::Images;
42 use Koha::Patrons;
43 use Koha::Virtualshelves;
44 use Koha::Club::Enrollments;
45 use Koha::Account;
46 use Koha::Subscription::Routinglists;
47
48 use base qw(Koha::Object);
49
50 our $RESULTSET_PATRON_ID_MAPPING = {
51     Accountline          => 'borrowernumber',
52     Aqbasketuser         => 'borrowernumber',
53     Aqbudget             => 'budget_owner_id',
54     Aqbudgetborrower     => 'borrowernumber',
55     ArticleRequest       => 'borrowernumber',
56     BorrowerAttribute    => 'borrowernumber',
57     BorrowerDebarment    => 'borrowernumber',
58     BorrowerFile         => 'borrowernumber',
59     BorrowerModification => 'borrowernumber',
60     ClubEnrollment       => 'borrowernumber',
61     Issue                => 'borrowernumber',
62     ItemsLastBorrower    => 'borrowernumber',
63     Linktracker          => 'borrowernumber',
64     Message              => 'borrowernumber',
65     MessageQueue         => 'borrowernumber',
66     OldIssue             => 'borrowernumber',
67     OldReserve           => 'borrowernumber',
68     Rating               => 'borrowernumber',
69     Reserve              => 'borrowernumber',
70     Review               => 'borrowernumber',
71     SearchHistory        => 'userid',
72     Statistic            => 'borrowernumber',
73     Suggestion           => 'suggestedby',
74     TagAll               => 'borrowernumber',
75     Virtualshelfcontent  => 'borrowernumber',
76     Virtualshelfshare    => 'borrowernumber',
77     Virtualshelve        => 'owner',
78 };
79
80 =head1 NAME
81
82 Koha::Patron - Koha Patron Object class
83
84 =head1 API
85
86 =head2 Class Methods
87
88 =cut
89
90 =head3 new
91
92 =cut
93
94 sub new {
95     my ( $class, $params ) = @_;
96
97     return $class->SUPER::new($params);
98 }
99
100 =head3 fixup_cardnumber
101
102 Autogenerate next cardnumber from highest value found in database
103
104 =cut
105
106 sub fixup_cardnumber {
107     my ( $self ) = @_;
108     my $max = Koha::Patrons->search({
109         cardnumber => {-regexp => '^-?[0-9]+$'}
110     }, {
111         select => \'CAST(cardnumber AS SIGNED)',
112         as => ['cast_cardnumber']
113     })->_resultset->get_column('cast_cardnumber')->max;
114     $self->cardnumber(($max || 0) +1);
115 }
116
117 =head3 trim_whitespace
118
119 trim whitespace from data which has some non-whitespace in it.
120 Could be moved to Koha::Object if need to be reused
121
122 =cut
123
124 sub trim_whitespaces {
125     my( $self ) = @_;
126
127     my $schema  = Koha::Database->new->schema;
128     my @columns = $schema->source($self->_type)->columns;
129
130     for my $column( @columns ) {
131         my $value = $self->$column;
132         if ( defined $value ) {
133             $value =~ s/^\s*|\s*$//g;
134             $self->$column($value);
135         }
136     }
137     return $self;
138 }
139
140 =head3 plain_text_password
141
142 $patron->plain_text_password( $password );
143
144 stores a copy of the unencrypted password in the object
145 for use in code before encrypting for db
146
147 =cut
148
149 sub plain_text_password {
150     my ( $self, $password ) = @_;
151     if ( $password ) {
152         $self->{_plain_text_password} = $password;
153         return $self;
154     }
155     return $self->{_plain_text_password}
156         if $self->{_plain_text_password};
157
158     return;
159 }
160
161 =head3 store
162
163 Patron specific store method to cleanup record
164 and do other necessary things before saving
165 to db
166
167 =cut
168
169 sub store {
170     my ($self) = @_;
171
172     $self->_result->result_source->schema->txn_do(
173         sub {
174             if (
175                 C4::Context->preference("autoMemberNum")
176                 and ( not defined $self->cardnumber
177                     or $self->cardnumber eq '' )
178               )
179             {
180                 # Warning: The caller is responsible for locking the members table in write
181                 # mode, to avoid database corruption.
182                 # We are in a transaction but the table is not locked
183                 $self->fixup_cardnumber;
184             }
185
186             unless( $self->category->in_storage ) {
187                 Koha::Exceptions::Object::FKConstraint->throw(
188                     broken_fk => 'categorycode',
189                     value     => $self->categorycode,
190                 );
191             }
192
193             $self->trim_whitespaces;
194
195             unless ( $self->in_storage ) {    #AddMember
196
197                 # Generate a valid userid/login if needed
198                 $self->generate_userid
199                   if not $self->userid or not $self->has_valid_userid;
200
201                 # Add expiration date if it isn't already there
202                 unless ( $self->dateexpiry ) {
203                     $self->dateexpiry( $self->category->get_expiry_date );
204                 }
205
206                 # Add enrollment date if it isn't already there
207                 unless ( $self->dateenrolled ) {
208                     $self->dateenrolled(dt_from_string);
209                 }
210
211                 # Set the privacy depending on the patron's category
212                 my $default_privacy = $self->category->default_privacy || q{};
213                 $default_privacy =
214                     $default_privacy eq 'default' ? 1
215                   : $default_privacy eq 'never'   ? 2
216                   : $default_privacy eq 'forever' ? 0
217                   :                                                   undef;
218                 $self->privacy($default_privacy);
219
220
221                 # Make a copy of the plain text password for later use
222                 $self->plain_text_password( $self->password );
223
224                 # Create a disabled account if no password provided
225                 $self->password( $self->password
226                     ? Koha::AuthUtils::hash_password( $self->password )
227                     : '!' );
228
229                 $self->borrowernumber(undef);
230
231                 $self = $self->SUPER::store;
232
233                 $self->add_enrolment_fee_if_needed;
234
235                 logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
236                   if C4::Context->preference("BorrowersLog");
237             }
238             else {    #ModMember
239
240                 my $self_from_storage = $self->get_from_storage;
241                 # FIXME We should not deal with that here, callers have to do this job
242                 # Moved from ModMember to prevent regressions
243                 unless ( $self->userid ) {
244                     my $stored_userid = $self_from_storage->userid;
245                     $self->userid($stored_userid);
246                 }
247
248                 # Password must be updated using $self->update_password
249                 $self->password($self_from_storage->password);
250
251                 if ( C4::Context->preference('FeeOnChangePatronCategory')
252                     and $self->category->categorycode ne
253                     $self_from_storage->category->categorycode )
254                 {
255                     $self->add_enrolment_fee_if_needed;
256                 }
257
258                 my $borrowers_log = C4::Context->preference("BorrowersLog");
259                 my $previous_cardnumber = $self_from_storage->cardnumber;
260                 if ($borrowers_log
261                     && ( !defined $previous_cardnumber
262                         || $previous_cardnumber ne $self->cardnumber )
263                     )
264                 {
265                     logaction(
266                         "MEMBERS",
267                         "MODIFY",
268                         $self->borrowernumber,
269                         to_json(
270                             {
271                                 cardnumber_replaced => {
272                                     previous_cardnumber => $previous_cardnumber,
273                                     new_cardnumber      => $self->cardnumber,
274                                 }
275                             },
276                             { utf8 => 1, pretty => 1 }
277                         )
278                     );
279                 }
280
281                 logaction( "MEMBERS", "MODIFY", $self->borrowernumber,
282                     "UPDATE (executed w/ arg: " . $self->borrowernumber . ")" )
283                   if $borrowers_log;
284
285                 $self = $self->SUPER::store;
286             }
287         }
288     );
289     return $self;
290 }
291
292 =head3 delete
293
294 $patron->delete
295
296 Delete patron's holds, lists and finally the patron.
297
298 Lists owned by the borrower are deleted, but entries from the borrower to
299 other lists are kept.
300
301 =cut
302
303 sub delete {
304     my ($self) = @_;
305
306     my $deleted;
307     $self->_result->result_source->schema->txn_do(
308         sub {
309             # Delete Patron's holds
310             $self->holds->delete;
311
312             # Delete all lists and all shares of this borrower
313             # Consistent with the approach Koha uses on deleting individual lists
314             # Note that entries in virtualshelfcontents added by this borrower to
315             # lists of others will be handled by a table constraint: the borrower
316             # is set to NULL in those entries.
317             # NOTE:
318             # We could handle the above deletes via a constraint too.
319             # But a new BZ report 11889 has been opened to discuss another approach.
320             # Instead of deleting we could also disown lists (based on a pref).
321             # In that way we could save shared and public lists.
322             # The current table constraints support that idea now.
323             # This pref should then govern the results of other routines/methods such as
324             # Koha::Virtualshelf->new->delete too.
325             # FIXME Could be $patron->get_lists
326             $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
327
328             $deleted = $self->SUPER::delete;
329
330             logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
331         }
332     );
333     return $deleted;
334 }
335
336
337 =head3 category
338
339 my $patron_category = $patron->category
340
341 Return the patron category for this patron
342
343 =cut
344
345 sub category {
346     my ( $self ) = @_;
347     return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
348 }
349
350 =head3 guarantor
351
352 Returns a Koha::Patron object for this patron's guarantor
353
354 =cut
355
356 sub guarantor {
357     my ( $self ) = @_;
358
359     return unless $self->guarantorid();
360
361     return Koha::Patrons->find( $self->guarantorid() );
362 }
363
364 sub image {
365     my ( $self ) = @_;
366
367     return scalar Koha::Patron::Images->find( $self->borrowernumber );
368 }
369
370 sub library {
371     my ( $self ) = @_;
372     return Koha::Library->_new_from_dbic($self->_result->branchcode);
373 }
374
375 =head3 guarantees
376
377 Returns the guarantees (list of Koha::Patron) of this patron
378
379 =cut
380
381 sub guarantees {
382     my ( $self ) = @_;
383
384     return Koha::Patrons->search( { guarantorid => $self->borrowernumber }, { order_by => { -asc => ['surname','firstname'] } } );
385 }
386
387 =head3 housebound_profile
388
389 Returns the HouseboundProfile associated with this patron.
390
391 =cut
392
393 sub housebound_profile {
394     my ( $self ) = @_;
395     my $profile = $self->_result->housebound_profile;
396     return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
397         if ( $profile );
398     return;
399 }
400
401 =head3 housebound_role
402
403 Returns the HouseboundRole associated with this patron.
404
405 =cut
406
407 sub housebound_role {
408     my ( $self ) = @_;
409
410     my $role = $self->_result->housebound_role;
411     return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
412     return;
413 }
414
415 =head3 siblings
416
417 Returns the siblings of this patron.
418
419 =cut
420
421 sub siblings {
422     my ( $self ) = @_;
423
424     my $guarantor = $self->guarantor;
425
426     return unless $guarantor;
427
428     return Koha::Patrons->search(
429         {
430             guarantorid => {
431                 '!=' => undef,
432                 '=' => $guarantor->id,
433             },
434             borrowernumber => {
435                 '!=' => $self->borrowernumber,
436             }
437         }
438     );
439 }
440
441 =head3 merge_with
442
443     my $patron = Koha::Patrons->find($id);
444     $patron->merge_with( \@patron_ids );
445
446     This subroutine merges a list of patrons into the patron record. This is accomplished by finding
447     all related patron ids for the patrons to be merged in other tables and changing the ids to be that
448     of the keeper patron.
449
450 =cut
451
452 sub merge_with {
453     my ( $self, $patron_ids ) = @_;
454
455     my @patron_ids = @{ $patron_ids };
456
457     # Ensure the keeper isn't in the list of patrons to merge
458     @patron_ids = grep { $_ ne $self->id } @patron_ids;
459
460     my $schema = Koha::Database->new()->schema();
461
462     my $results;
463
464     $self->_result->result_source->schema->txn_do( sub {
465         foreach my $patron_id (@patron_ids) {
466             my $patron = Koha::Patrons->find( $patron_id );
467
468             next unless $patron;
469
470             # Unbless for safety, the patron will end up being deleted
471             $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
472
473             while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
474                 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
475                 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
476                 $rs->update({ $field => $self->id });
477             }
478
479             $patron->move_to_deleted();
480             $patron->delete();
481         }
482     });
483
484     return $results;
485 }
486
487
488
489 =head3 wants_check_for_previous_checkout
490
491     $wants_check = $patron->wants_check_for_previous_checkout;
492
493 Return 1 if Koha needs to perform PrevIssue checking, else 0.
494
495 =cut
496
497 sub wants_check_for_previous_checkout {
498     my ( $self ) = @_;
499     my $syspref = C4::Context->preference("checkPrevCheckout");
500
501     # Simple cases
502     ## Hard syspref trumps all
503     return 1 if ($syspref eq 'hardyes');
504     return 0 if ($syspref eq 'hardno');
505     ## Now, patron pref trumps all
506     return 1 if ($self->checkprevcheckout eq 'yes');
507     return 0 if ($self->checkprevcheckout eq 'no');
508
509     # More complex: patron inherits -> determine category preference
510     my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
511     return 1 if ($checkPrevCheckoutByCat eq 'yes');
512     return 0 if ($checkPrevCheckoutByCat eq 'no');
513
514     # Finally: category preference is inherit, default to 0
515     if ($syspref eq 'softyes') {
516         return 1;
517     } else {
518         return 0;
519     }
520 }
521
522 =head3 do_check_for_previous_checkout
523
524     $do_check = $patron->do_check_for_previous_checkout($item);
525
526 Return 1 if the bib associated with $ITEM has previously been checked out to
527 $PATRON, 0 otherwise.
528
529 =cut
530
531 sub do_check_for_previous_checkout {
532     my ( $self, $item ) = @_;
533
534     # Find all items for bib and extract item numbers.
535     my @items = Koha::Items->search({biblionumber => $item->{biblionumber}});
536     my @item_nos;
537     foreach my $item (@items) {
538         push @item_nos, $item->itemnumber;
539     }
540
541     # Create (old)issues search criteria
542     my $criteria = {
543         borrowernumber => $self->borrowernumber,
544         itemnumber => \@item_nos,
545     };
546
547     # Check current issues table
548     my $issues = Koha::Checkouts->search($criteria);
549     return 1 if $issues->count; # 0 || N
550
551     # Check old issues table
552     my $old_issues = Koha::Old::Checkouts->search($criteria);
553     return $old_issues->count;  # 0 || N
554 }
555
556 =head3 is_debarred
557
558 my $debarment_expiration = $patron->is_debarred;
559
560 Returns the date a patron debarment will expire, or undef if the patron is not
561 debarred
562
563 =cut
564
565 sub is_debarred {
566     my ($self) = @_;
567
568     return unless $self->debarred;
569     return $self->debarred
570       if $self->debarred =~ '^9999'
571       or dt_from_string( $self->debarred ) > dt_from_string;
572     return;
573 }
574
575 =head3 is_expired
576
577 my $is_expired = $patron->is_expired;
578
579 Returns 1 if the patron is expired or 0;
580
581 =cut
582
583 sub is_expired {
584     my ($self) = @_;
585     return 0 unless $self->dateexpiry;
586     return 0 if $self->dateexpiry =~ '^9999';
587     return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
588     return 0;
589 }
590
591 =head3 is_going_to_expire
592
593 my $is_going_to_expire = $patron->is_going_to_expire;
594
595 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
596
597 =cut
598
599 sub is_going_to_expire {
600     my ($self) = @_;
601
602     my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
603
604     return 0 unless $delay;
605     return 0 unless $self->dateexpiry;
606     return 0 if $self->dateexpiry =~ '^9999';
607     return 1 if dt_from_string( $self->dateexpiry )->subtract( days => $delay ) < dt_from_string->truncate( to => 'day' );
608     return 0;
609 }
610
611 =head3 update_password
612
613 my $updated = $patron->update_password( $userid, $password );
614
615 Update the userid and the password of a patron.
616 If the userid already exists, returns and let DBIx::Class warns
617 This will add an entry to action_logs if BorrowersLog is set.
618
619 =cut
620
621 sub update_password {
622     my ( $self, $userid, $password ) = @_;
623     eval { $self->userid($userid)->store; };
624     return if $@; # Make sure the userid is not already in used by another patron
625
626     return 0 if $password eq '****' or $password eq '';
627
628     my $digest = Koha::AuthUtils::hash_password($password);
629     $self->update(
630         {
631             password       => $digest,
632             login_attempts => 0,
633         }
634     );
635
636     logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
637     return $digest;
638 }
639
640 =head3 set_password
641
642     $patron->set_password({ password => $plain_text_password [, skip_validation => 1 ] });
643
644 Set the patron's password.
645
646 =head4 Exceptions
647
648 The passed string is validated against the current password enforcement policy.
649 Validation can be skipped by passing the I<skip_validation> parameter.
650
651 Exceptions are thrown if the password is not good enough.
652
653 =over 4
654
655 =item Koha::Exceptions::Password::TooShort
656
657 =item Koha::Exceptions::Password::WhitespaceCharacters
658
659 =item Koha::Exceptions::Password::TooWeak
660
661 =back
662
663 =cut
664
665 sub set_password {
666     my ( $self, $args ) = @_;
667
668     my $password = $args->{password};
669
670     unless ( $args->{skip_validation} ) {
671         my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password );
672
673         if ( !$is_valid ) {
674             if ( $error eq 'too_short' ) {
675                 my $min_length = C4::Context->preference('minPasswordLength');
676                 $min_length = 3 if not $min_length or $min_length < 3;
677
678                 my $password_length = length($password);
679                 Koha::Exceptions::Password::TooShort->throw(
680                     length => $password_length, min_length => $min_length );
681             }
682             elsif ( $error eq 'has_whitespaces' ) {
683                 Koha::Exceptions::Password::WhitespaceCharacters->throw();
684             }
685             elsif ( $error eq 'too_weak' ) {
686                 Koha::Exceptions::Password::TooWeak->throw();
687             }
688         }
689     }
690
691     my $digest = Koha::AuthUtils::hash_password($password);
692     $self->update(
693         {   password       => $digest,
694             login_attempts => 0,
695         }
696     );
697
698     logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" )
699         if C4::Context->preference("BorrowersLog");
700
701     return $self;
702 }
703
704
705 =head3 renew_account
706
707 my $new_expiry_date = $patron->renew_account
708
709 Extending the subscription to the expiry date.
710
711 =cut
712
713 sub renew_account {
714     my ($self) = @_;
715     my $date;
716     if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
717         $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
718     } else {
719         $date =
720             C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
721             ? dt_from_string( $self->dateexpiry )
722             : dt_from_string;
723     }
724     my $expiry_date = $self->category->get_expiry_date($date);
725
726     $self->dateexpiry($expiry_date);
727     $self->date_renewed( dt_from_string() );
728     $self->store();
729
730     $self->add_enrolment_fee_if_needed;
731
732     logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
733     return dt_from_string( $expiry_date )->truncate( to => 'day' );
734 }
735
736 =head3 has_overdues
737
738 my $has_overdues = $patron->has_overdues;
739
740 Returns the number of patron's overdues
741
742 =cut
743
744 sub has_overdues {
745     my ($self) = @_;
746     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
747     return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
748 }
749
750 =head3 track_login
751
752     $patron->track_login;
753     $patron->track_login({ force => 1 });
754
755     Tracks a (successful) login attempt.
756     The preference TrackLastPatronActivity must be enabled. Or you
757     should pass the force parameter.
758
759 =cut
760
761 sub track_login {
762     my ( $self, $params ) = @_;
763     return if
764         !$params->{force} &&
765         !C4::Context->preference('TrackLastPatronActivity');
766     $self->lastseen( dt_from_string() )->store;
767 }
768
769 =head3 move_to_deleted
770
771 my $is_moved = $patron->move_to_deleted;
772
773 Move a patron to the deletedborrowers table.
774 This can be done before deleting a patron, to make sure the data are not completely deleted.
775
776 =cut
777
778 sub move_to_deleted {
779     my ($self) = @_;
780     my $patron_infos = $self->unblessed;
781     delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
782     return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
783 }
784
785 =head3 article_requests
786
787 my @requests = $borrower->article_requests();
788 my $requests = $borrower->article_requests();
789
790 Returns either a list of ArticleRequests objects,
791 or an ArtitleRequests object, depending on the
792 calling context.
793
794 =cut
795
796 sub article_requests {
797     my ( $self ) = @_;
798
799     $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
800
801     return $self->{_article_requests};
802 }
803
804 =head3 article_requests_current
805
806 my @requests = $patron->article_requests_current
807
808 Returns the article requests associated with this patron that are incomplete
809
810 =cut
811
812 sub article_requests_current {
813     my ( $self ) = @_;
814
815     $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
816         {
817             borrowernumber => $self->id(),
818             -or          => [
819                 { status => Koha::ArticleRequest::Status::Pending },
820                 { status => Koha::ArticleRequest::Status::Processing }
821             ]
822         }
823     );
824
825     return $self->{_article_requests_current};
826 }
827
828 =head3 article_requests_finished
829
830 my @requests = $biblio->article_requests_finished
831
832 Returns the article requests associated with this patron that are completed
833
834 =cut
835
836 sub article_requests_finished {
837     my ( $self, $borrower ) = @_;
838
839     $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
840         {
841             borrowernumber => $self->id(),
842             -or          => [
843                 { status => Koha::ArticleRequest::Status::Completed },
844                 { status => Koha::ArticleRequest::Status::Canceled }
845             ]
846         }
847     );
848
849     return $self->{_article_requests_finished};
850 }
851
852 =head3 add_enrolment_fee_if_needed
853
854 my $enrolment_fee = $patron->add_enrolment_fee_if_needed;
855
856 Add enrolment fee for a patron if needed.
857
858 =cut
859
860 sub add_enrolment_fee_if_needed {
861     my ($self) = @_;
862     my $enrolment_fee = $self->category->enrolmentfee;
863     if ( $enrolment_fee && $enrolment_fee > 0 ) {
864         # insert fee in patron debts
865         C4::Accounts::manualinvoice( $self->borrowernumber, '', '', 'A', $enrolment_fee );
866     }
867     return $enrolment_fee || 0;
868 }
869
870 =head3 checkouts
871
872 my $checkouts = $patron->checkouts
873
874 =cut
875
876 sub checkouts {
877     my ($self) = @_;
878     my $checkouts = $self->_result->issues;
879     return Koha::Checkouts->_new_from_dbic( $checkouts );
880 }
881
882 =head3 pending_checkouts
883
884 my $pending_checkouts = $patron->pending_checkouts
885
886 This method will return the same as $self->checkouts, but with a prefetch on
887 items, biblio and biblioitems.
888
889 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
890
891 It should not be used directly, prefer to access fields you need instead of
892 retrieving all these fields in one go.
893
894
895 =cut
896
897 sub pending_checkouts {
898     my( $self ) = @_;
899     my $checkouts = $self->_result->issues->search(
900         {},
901         {
902             order_by => [
903                 { -desc => 'me.timestamp' },
904                 { -desc => 'issuedate' },
905                 { -desc => 'issue_id' }, # Sort by issue_id should be enough
906             ],
907             prefetch => { item => { biblio => 'biblioitems' } },
908         }
909     );
910     return Koha::Checkouts->_new_from_dbic( $checkouts );
911 }
912
913 =head3 old_checkouts
914
915 my $old_checkouts = $patron->old_checkouts
916
917 =cut
918
919 sub old_checkouts {
920     my ($self) = @_;
921     my $old_checkouts = $self->_result->old_issues;
922     return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
923 }
924
925 =head3 get_overdues
926
927 my $overdue_items = $patron->get_overdues
928
929 Return the overdue items
930
931 =cut
932
933 sub get_overdues {
934     my ($self) = @_;
935     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
936     return $self->checkouts->search(
937         {
938             'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
939         },
940         {
941             prefetch => { item => { biblio => 'biblioitems' } },
942         }
943     );
944 }
945
946 =head3 get_routing_lists
947
948 my @routinglists = $patron->get_routing_lists
949
950 Returns the routing lists a patron is subscribed to.
951
952 =cut
953
954 sub get_routing_lists {
955     my ($self) = @_;
956     my $routing_list_rs = $self->_result->subscriptionroutinglists;
957     return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
958 }
959
960 =head3 get_age
961
962 my $age = $patron->get_age
963
964 Return the age of the patron
965
966 =cut
967
968 sub get_age {
969     my ($self)    = @_;
970     my $today_str = dt_from_string->strftime("%Y-%m-%d");
971     return unless $self->dateofbirth;
972     my $dob_str   = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
973
974     my ( $dob_y,   $dob_m,   $dob_d )   = split /-/, $dob_str;
975     my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
976
977     my $age = $today_y - $dob_y;
978     if ( $dob_m . $dob_d > $today_m . $today_d ) {
979         $age--;
980     }
981
982     return $age;
983 }
984
985 =head3 account
986
987 my $account = $patron->account
988
989 =cut
990
991 sub account {
992     my ($self) = @_;
993     return Koha::Account->new( { patron_id => $self->borrowernumber } );
994 }
995
996 =head3 holds
997
998 my $holds = $patron->holds
999
1000 Return all the holds placed by this patron
1001
1002 =cut
1003
1004 sub holds {
1005     my ($self) = @_;
1006     my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
1007     return Koha::Holds->_new_from_dbic($holds_rs);
1008 }
1009
1010 =head3 old_holds
1011
1012 my $old_holds = $patron->old_holds
1013
1014 Return all the historical holds for this patron
1015
1016 =cut
1017
1018 sub old_holds {
1019     my ($self) = @_;
1020     my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
1021     return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
1022 }
1023
1024 =head3 notice_email_address
1025
1026   my $email = $patron->notice_email_address;
1027
1028 Return the email address of patron used for notices.
1029 Returns the empty string if no email address.
1030
1031 =cut
1032
1033 sub notice_email_address{
1034     my ( $self ) = @_;
1035
1036     my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
1037     # if syspref is set to 'first valid' (value == OFF), look up email address
1038     if ( $which_address eq 'OFF' ) {
1039         return $self->first_valid_email_address;
1040     }
1041
1042     return $self->$which_address || '';
1043 }
1044
1045 =head3 first_valid_email_address
1046
1047 my $first_valid_email_address = $patron->first_valid_email_address
1048
1049 Return the first valid email address for a patron.
1050 For now, the order  is defined as email, emailpro, B_email.
1051 Returns the empty string if the borrower has no email addresses.
1052
1053 =cut
1054
1055 sub first_valid_email_address {
1056     my ($self) = @_;
1057
1058     return $self->email() || $self->emailpro() || $self->B_email() || q{};
1059 }
1060
1061 =head3 get_club_enrollments
1062
1063 =cut
1064
1065 sub get_club_enrollments {
1066     my ( $self, $return_scalar ) = @_;
1067
1068     my $e = Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
1069
1070     return $e if $return_scalar;
1071
1072     return wantarray ? $e->as_list : $e;
1073 }
1074
1075 =head3 get_enrollable_clubs
1076
1077 =cut
1078
1079 sub get_enrollable_clubs {
1080     my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
1081
1082     my $params;
1083     $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
1084       if $is_enrollable_from_opac;
1085     $params->{is_email_required} = 0 unless $self->first_valid_email_address();
1086
1087     $params->{borrower} = $self;
1088
1089     my $e = Koha::Clubs->get_enrollable($params);
1090
1091     return $e if $return_scalar;
1092
1093     return wantarray ? $e->as_list : $e;
1094 }
1095
1096 =head3 account_locked
1097
1098 my $is_locked = $patron->account_locked
1099
1100 Return true if the patron has reach the maximum number of login attempts (see pref FailedLoginAttempts).
1101 Otherwise return false.
1102 If the pref is not set (empty string, null or 0), the feature is considered as disabled.
1103
1104 =cut
1105
1106 sub account_locked {
1107     my ($self) = @_;
1108     my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
1109     return ( $FailedLoginAttempts
1110           and $self->login_attempts
1111           and $self->login_attempts >= $FailedLoginAttempts )? 1 : 0;
1112 }
1113
1114 =head3 can_see_patron_infos
1115
1116 my $can_see = $patron->can_see_patron_infos( $patron );
1117
1118 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
1119
1120 =cut
1121
1122 sub can_see_patron_infos {
1123     my ( $self, $patron ) = @_;
1124     return unless $patron;
1125     return $self->can_see_patrons_from( $patron->library->branchcode );
1126 }
1127
1128 =head3 can_see_patrons_from
1129
1130 my $can_see = $patron->can_see_patrons_from( $branchcode );
1131
1132 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1133
1134 =cut
1135
1136 sub can_see_patrons_from {
1137     my ( $self, $branchcode ) = @_;
1138     my $can = 0;
1139     if ( $self->branchcode eq $branchcode ) {
1140         $can = 1;
1141     } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1142         $can = 1;
1143     } elsif ( my $library_groups = $self->library->library_groups ) {
1144         while ( my $library_group = $library_groups->next ) {
1145             if ( $library_group->parent->has_child( $branchcode ) ) {
1146                 $can = 1;
1147                 last;
1148             }
1149         }
1150     }
1151     return $can;
1152 }
1153
1154 =head3 libraries_where_can_see_patrons
1155
1156 my $libraries = $patron-libraries_where_can_see_patrons;
1157
1158 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1159 The branchcodes are arbitrarily returned sorted.
1160 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1161
1162 An empty array means no restriction, the patron can see patron's infos from any libraries.
1163
1164 =cut
1165
1166 sub libraries_where_can_see_patrons {
1167     my ( $self ) = @_;
1168     my $userenv = C4::Context->userenv;
1169
1170     return () unless $userenv; # For tests, but userenv should be defined in tests...
1171
1172     my @restricted_branchcodes;
1173     if (C4::Context::only_my_library) {
1174         push @restricted_branchcodes, $self->branchcode;
1175     }
1176     else {
1177         unless (
1178             $self->has_permission(
1179                 { borrowers => 'view_borrower_infos_from_any_libraries' }
1180             )
1181           )
1182         {
1183             my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1184             if ( $library_groups->count )
1185             {
1186                 while ( my $library_group = $library_groups->next ) {
1187                     my $parent = $library_group->parent;
1188                     if ( $parent->has_child( $self->branchcode ) ) {
1189                         push @restricted_branchcodes, $parent->children->get_column('branchcode');
1190                     }
1191                 }
1192             }
1193
1194             @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1195         }
1196     }
1197
1198     @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1199     @restricted_branchcodes = uniq(@restricted_branchcodes);
1200     @restricted_branchcodes = sort(@restricted_branchcodes);
1201     return @restricted_branchcodes;
1202 }
1203
1204 sub has_permission {
1205     my ( $self, $flagsrequired ) = @_;
1206     return unless $self->userid;
1207     # TODO code from haspermission needs to be moved here!
1208     return C4::Auth::haspermission( $self->userid, $flagsrequired );
1209 }
1210
1211 =head3 is_adult
1212
1213 my $is_adult = $patron->is_adult
1214
1215 Return true if the patron has a category with a type Adult (A) or Organization (I)
1216
1217 =cut
1218
1219 sub is_adult {
1220     my ( $self ) = @_;
1221     return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1222 }
1223
1224 =head3 is_child
1225
1226 my $is_child = $patron->is_child
1227
1228 Return true if the patron has a category with a type Child (C)
1229
1230 =cut
1231 sub is_child {
1232     my( $self ) = @_;
1233     return $self->category->category_type eq 'C' ? 1 : 0;
1234 }
1235
1236 =head3 has_valid_userid
1237
1238 my $patron = Koha::Patrons->find(42);
1239 $patron->userid( $new_userid );
1240 my $has_a_valid_userid = $patron->has_valid_userid
1241
1242 my $patron = Koha::Patron->new( $params );
1243 my $has_a_valid_userid = $patron->has_valid_userid
1244
1245 Return true if the current userid of this patron is valid/unique, otherwise false.
1246
1247 Note that this should be done in $self->store instead and raise an exception if needed.
1248
1249 =cut
1250
1251 sub has_valid_userid {
1252     my ($self) = @_;
1253
1254     return 0 unless $self->userid;
1255
1256     return 0 if ( $self->userid eq C4::Context->config('user') );    # DB user
1257
1258     my $already_exists = Koha::Patrons->search(
1259         {
1260             userid => $self->userid,
1261             (
1262                 $self->in_storage
1263                 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1264                 : ()
1265             ),
1266         }
1267     )->count;
1268     return $already_exists ? 0 : 1;
1269 }
1270
1271 =head3 generate_userid
1272
1273 my $patron = Koha::Patron->new( $params );
1274 $patron->generate_userid
1275
1276 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1277
1278 Set a generated userid ($firstname.$surname if there is a $firstname, or $surname if there is no value in $firstname) plus offset (0 if the $userid is unique, or a higher numeric value if not unique).
1279
1280 =cut
1281
1282 sub generate_userid {
1283     my ($self) = @_;
1284     my $offset = 0;
1285     my $firstname = $self->firstname // q{};
1286     my $surname = $self->surname // q{};
1287     #The script will "do" the following code and increment the $offset until the generated userid is unique
1288     do {
1289       $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1290       $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1291       my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1292       $userid = unac_string('utf-8',$userid);
1293       $userid .= $offset unless $offset == 0;
1294       $self->userid( $userid );
1295       $offset++;
1296      } while (! $self->has_valid_userid );
1297
1298      return $self;
1299
1300 }
1301
1302 =head2 Internal methods
1303
1304 =head3 _type
1305
1306 =cut
1307
1308 sub _type {
1309     return 'Borrower';
1310 }
1311
1312 =head1 AUTHOR
1313
1314 Kyle M Hall <kyle@bywatersolutions.com>
1315 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1316
1317 =cut
1318
1319 1;