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