Bug 20997: Replace missing credit_applied with 'Credit Applied'
[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::Accounts;
30 use C4::Context;
31 use C4::Log;
32 use Koha::AuthUtils;
33 use Koha::Checkouts;
34 use Koha::Database;
35 use Koha::DateUtils;
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 if ( ! can_load( modules => { 'Koha::NorwegianPatronDB' => undef } ) ) {
49    warn "Unable to load Koha::NorwegianPatronDB";
50 }
51
52 use base qw(Koha::Object);
53
54 our $RESULTSET_PATRON_ID_MAPPING = {
55     Accountline          => 'borrowernumber',
56     Aqbasketuser         => 'borrowernumber',
57     Aqbudget             => 'budget_owner_id',
58     Aqbudgetborrower     => 'borrowernumber',
59     ArticleRequest       => 'borrowernumber',
60     BorrowerAttribute    => 'borrowernumber',
61     BorrowerDebarment    => 'borrowernumber',
62     BorrowerFile         => 'borrowernumber',
63     BorrowerModification => 'borrowernumber',
64     ClubEnrollment       => 'borrowernumber',
65     Issue                => 'borrowernumber',
66     ItemsLastBorrower    => 'borrowernumber',
67     Linktracker          => 'borrowernumber',
68     Message              => 'borrowernumber',
69     MessageQueue         => 'borrowernumber',
70     OldIssue             => 'borrowernumber',
71     OldReserve           => 'borrowernumber',
72     Rating               => 'borrowernumber',
73     Reserve              => 'borrowernumber',
74     Review               => 'borrowernumber',
75     SearchHistory        => 'userid',
76     Statistic            => 'borrowernumber',
77     Suggestion           => 'suggestedby',
78     TagAll               => 'borrowernumber',
79     Virtualshelfcontent  => 'borrowernumber',
80     Virtualshelfshare    => 'borrowernumber',
81     Virtualshelve        => 'owner',
82 };
83
84 =head1 NAME
85
86 Koha::Patron - Koha Patron Object class
87
88 =head1 API
89
90 =head2 Class Methods
91
92 =cut
93
94 =head3 new
95
96 =cut
97
98 sub new {
99     my ( $class, $params ) = @_;
100
101     return $class->SUPER::new($params);
102 }
103
104 =head3 fixup_cardnumber
105
106 Autogenerate next cardnumber from highest value found in database
107
108 =cut
109
110 sub fixup_cardnumber {
111     my ( $self ) = @_;
112     my $max = Koha::Patrons->search({
113         cardnumber => {-regexp => '^-?[0-9]+$'}
114     }, {
115         select => \'CAST(cardnumber AS SIGNED)',
116         as => ['cast_cardnumber']
117     })->_resultset->get_column('cast_cardnumber')->max;
118     $self->cardnumber(($max || 0) +1);
119 }
120
121 =head3 trim_whitespace
122
123 trim whitespace from data which has some non-whitespace in it.
124 Could be moved to Koha::Object if need to be reused
125
126 =cut
127
128 sub trim_whitespaces {
129     my( $self ) = @_;
130
131     my $schema  = Koha::Database->new->schema;
132     my @columns = $schema->source($self->_type)->columns;
133
134     for my $column( @columns ) {
135         my $value = $self->$column;
136         if ( defined $value ) {
137             $value =~ s/^\s*|\s*$//g;
138             $self->$column($value);
139         }
140     }
141     return $self;
142 }
143
144 =head3 plain_text_password
145
146 $patron->plain_text_password( $password );
147
148 stores a copy of the unencrypted password in the object
149 for use in code before encrypting for db
150
151 =cut
152
153 sub plain_text_password {
154     my ( $self, $password ) = @_;
155     if ( $password ) {
156         $self->{_plain_text_password} = $password;
157         return $self;
158     }
159     return $self->{_plain_text_password}
160         if $self->{_plain_text_password};
161
162     return;
163 }
164
165 =head3 store
166
167 Patron specific store method to cleanup record
168 and do other necessary things before saving
169 to db
170
171 =cut
172
173 sub store {
174     my ($self) = @_;
175
176     $self->_result->result_source->schema->txn_do(
177         sub {
178             if (
179                 C4::Context->preference("autoMemberNum")
180                 and ( not defined $self->cardnumber
181                     or $self->cardnumber eq '' )
182               )
183             {
184                 # Warning: The caller is responsible for locking the members table in write
185                 # mode, to avoid database corruption.
186                 # We are in a transaction but the table is not locked
187                 $self->fixup_cardnumber;
188             }
189
190             unless( $self->category->in_storage ) {
191                 Koha::Exceptions::Object::FKConstraint->throw(
192                     broken_fk => 'categorycode',
193                     value     => $self->categorycode,
194                 );
195             }
196
197             $self->trim_whitespaces;
198
199             # We don't want invalid dates in the db (mysql has a bad habit of inserting 0000-00-00)
200             $self->dateofbirth(undef) unless $self->dateofbirth;
201             $self->debarred(undef)    unless $self->debarred;
202
203             # Set default values if not set
204             $self->sms_provider_id(undef) unless $self->sms_provider_id;
205             $self->guarantorid(undef)     unless $self->guarantorid;
206
207             unless ( $self->in_storage ) {    #AddMember
208
209                 # Generate a valid userid/login if needed
210                 $self->generate_userid
211                   if not $self->userid or not $self->has_valid_userid;
212
213                 # Add expiration date if it isn't already there
214                 unless ( $self->dateexpiry ) {
215                     $self->dateexpiry( $self->category->get_expiry_date );
216                 }
217
218                 # Add enrollment date if it isn't already there
219                 unless ( $self->dateenrolled ) {
220                     $self->dateenrolled(dt_from_string);
221                 }
222
223                 # Set the privacy depending on the patron's category
224                 my $default_privacy = $self->category->default_privacy || q{};
225                 $default_privacy =
226                     $default_privacy eq 'default' ? 1
227                   : $default_privacy eq 'never'   ? 2
228                   : $default_privacy eq 'forever' ? 0
229                   :                                                   undef;
230                 $self->privacy($default_privacy);
231
232                 unless ( defined $self->privacy_guarantor_checkouts ) {
233                     $self->privacy_guarantor_checkouts(0);
234                 }
235
236                 # Make a copy of the plain text password for later use
237                 $self->plain_text_password( $self->password );
238
239                 # Create a disabled account if no password provided
240                 $self->password( $self->password
241                     ? Koha::AuthUtils::hash_password( $self->password )
242                     : '!' );
243
244                 $self->borrowernumber(undef);
245
246                 $self = $self->SUPER::store;
247
248                 # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
249                 # cronjob will use for syncing with NL
250                 if (   C4::Context->preference('NorwegianPatronDBEnable')
251                     && C4::Context->preference('NorwegianPatronDBEnable') == 1 )
252                 {
253                     Koha::Database->new->schema->resultset('BorrowerSync')
254                       ->create(
255                         {
256                             'borrowernumber' => $self->borrowernumber,
257                             'synctype'       => 'norwegianpatrondb',
258                             'sync'           => 1,
259                             'syncstatus'     => 'new',
260                             'hashed_pin' =>
261                               Koha::NorwegianPatronDB::NLEncryptPIN($self->plain_text_password),
262                         }
263                       );
264                 }
265
266                 $self->add_enrolment_fee_if_needed;
267
268                 logaction( "MEMBERS", "CREATE", $self->borrowernumber, "" )
269                   if C4::Context->preference("BorrowersLog");
270             }
271             else {    #ModMember
272
273                 # Come from ModMember, but should not be possible (?)
274                 $self->dateenrolled(undef) unless $self->dateenrolled;
275                 $self->dateexpiry(undef)   unless $self->dateexpiry;
276
277
278                 my $self_from_storage = $self->get_from_storage;
279                 # FIXME We should not deal with that here, callers have to do this job
280                 # Moved from ModMember to prevent regressions
281                 unless ( $self->userid ) {
282                     my $stored_userid = $self_from_storage->userid;
283                     $self->userid($stored_userid);
284                 }
285
286                 # Password must be updated using $self->update_password
287                 $self->password($self_from_storage->password);
288
289                 if ( C4::Context->preference('FeeOnChangePatronCategory')
290                     and $self->category->categorycode ne
291                     $self_from_storage->category->categorycode )
292                 {
293                     $self->add_enrolment_fee_if_needed;
294                 }
295
296                 # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
297                 # cronjob will use for syncing with NL
298                 if (   C4::Context->preference('NorwegianPatronDBEnable')
299                     && C4::Context->preference('NorwegianPatronDBEnable') == 1 )
300                 {
301                     my $borrowersync = Koha::Database->new->schema->resultset('BorrowerSync')->find({
302                         'synctype'       => 'norwegianpatrondb',
303                         'borrowernumber' => $self->borrowernumber,
304                     });
305                     # Do not set to "edited" if syncstatus is "new". We need to sync as new before
306                     # we can sync as changed. And the "new sync" will pick up all changes since
307                     # the patron was created anyway.
308                     if ( $borrowersync->syncstatus ne 'new' && $borrowersync->syncstatus ne 'delete' ) {
309                         $borrowersync->update( { 'syncstatus' => 'edited' } );
310                     }
311                     # Set the value of 'sync'
312                     # FIXME THIS IS BROKEN # $borrowersync->update( { 'sync' => $data{'sync'} } );
313
314                     # Try to do the live sync
315                     Koha::NorwegianPatronDB::NLSync({ 'borrowernumber' => $self->borrowernumber });
316                 }
317
318                 my $borrowers_log = C4::Context->preference("BorrowersLog");
319                 my $previous_cardnumber = $self_from_storage->cardnumber;
320                 if ($borrowers_log
321                     && ( !defined $previous_cardnumber
322                         || $previous_cardnumber ne $self->cardnumber )
323                     )
324                 {
325                     logaction(
326                         "MEMBERS",
327                         "MODIFY",
328                         $self->borrowernumber,
329                         to_json(
330                             {
331                                 cardnumber_replaced => {
332                                     previous_cardnumber => $previous_cardnumber,
333                                     new_cardnumber      => $self->cardnumber,
334                                 }
335                             },
336                             { utf8 => 1, pretty => 1 }
337                         )
338                     );
339                 }
340
341                 logaction( "MEMBERS", "MODIFY", $self->borrowernumber,
342                     "UPDATE (executed w/ arg: " . $self->borrowernumber . ")" )
343                   if $borrowers_log;
344
345                 $self = $self->SUPER::store;
346             }
347         }
348     );
349     return $self;
350 }
351
352 =head3 delete
353
354 $patron->delete
355
356 Delete patron's holds, lists and finally the patron.
357
358 Lists owned by the borrower are deleted, but entries from the borrower to
359 other lists are kept.
360
361 =cut
362
363 sub delete {
364     my ($self) = @_;
365
366     my $deleted;
367     $self->_result->result_source->schema->txn_do(
368         sub {
369             # Delete Patron's holds
370             $self->holds->delete;
371
372             # Delete all lists and all shares of this borrower
373             # Consistent with the approach Koha uses on deleting individual lists
374             # Note that entries in virtualshelfcontents added by this borrower to
375             # lists of others will be handled by a table constraint: the borrower
376             # is set to NULL in those entries.
377             # NOTE:
378             # We could handle the above deletes via a constraint too.
379             # But a new BZ report 11889 has been opened to discuss another approach.
380             # Instead of deleting we could also disown lists (based on a pref).
381             # In that way we could save shared and public lists.
382             # The current table constraints support that idea now.
383             # This pref should then govern the results of other routines/methods such as
384             # Koha::Virtualshelf->new->delete too.
385             # FIXME Could be $patron->get_lists
386             $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
387
388             $deleted = $self->SUPER::delete;
389
390             logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
391         }
392     );
393     return $deleted;
394 }
395
396
397 =head3 category
398
399 my $patron_category = $patron->category
400
401 Return the patron category for this patron
402
403 =cut
404
405 sub category {
406     my ( $self ) = @_;
407     return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
408 }
409
410 =head3 guarantor
411
412 Returns a Koha::Patron object for this patron's guarantor
413
414 =cut
415
416 sub guarantor {
417     my ( $self ) = @_;
418
419     return unless $self->guarantorid();
420
421     return Koha::Patrons->find( $self->guarantorid() );
422 }
423
424 sub image {
425     my ( $self ) = @_;
426
427     return scalar Koha::Patron::Images->find( $self->borrowernumber );
428 }
429
430 sub library {
431     my ( $self ) = @_;
432     return Koha::Library->_new_from_dbic($self->_result->branchcode);
433 }
434
435 =head3 guarantees
436
437 Returns the guarantees (list of Koha::Patron) of this patron
438
439 =cut
440
441 sub guarantees {
442     my ( $self ) = @_;
443
444     return Koha::Patrons->search( { guarantorid => $self->borrowernumber }, { order_by => { -asc => ['surname','firstname'] } } );
445 }
446
447 =head3 housebound_profile
448
449 Returns the HouseboundProfile associated with this patron.
450
451 =cut
452
453 sub housebound_profile {
454     my ( $self ) = @_;
455     my $profile = $self->_result->housebound_profile;
456     return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
457         if ( $profile );
458     return;
459 }
460
461 =head3 housebound_role
462
463 Returns the HouseboundRole associated with this patron.
464
465 =cut
466
467 sub housebound_role {
468     my ( $self ) = @_;
469
470     my $role = $self->_result->housebound_role;
471     return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
472     return;
473 }
474
475 =head3 siblings
476
477 Returns the siblings of this patron.
478
479 =cut
480
481 sub siblings {
482     my ( $self ) = @_;
483
484     my $guarantor = $self->guarantor;
485
486     return unless $guarantor;
487
488     return Koha::Patrons->search(
489         {
490             guarantorid => {
491                 '!=' => undef,
492                 '=' => $guarantor->id,
493             },
494             borrowernumber => {
495                 '!=' => $self->borrowernumber,
496             }
497         }
498     );
499 }
500
501 =head3 merge_with
502
503     my $patron = Koha::Patrons->find($id);
504     $patron->merge_with( \@patron_ids );
505
506     This subroutine merges a list of patrons into the patron record. This is accomplished by finding
507     all related patron ids for the patrons to be merged in other tables and changing the ids to be that
508     of the keeper patron.
509
510 =cut
511
512 sub merge_with {
513     my ( $self, $patron_ids ) = @_;
514
515     my @patron_ids = @{ $patron_ids };
516
517     # Ensure the keeper isn't in the list of patrons to merge
518     @patron_ids = grep { $_ ne $self->id } @patron_ids;
519
520     my $schema = Koha::Database->new()->schema();
521
522     my $results;
523
524     $self->_result->result_source->schema->txn_do( sub {
525         foreach my $patron_id (@patron_ids) {
526             my $patron = Koha::Patrons->find( $patron_id );
527
528             next unless $patron;
529
530             # Unbless for safety, the patron will end up being deleted
531             $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
532
533             while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
534                 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
535                 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
536                 $rs->update({ $field => $self->id });
537             }
538
539             $patron->move_to_deleted();
540             $patron->delete();
541         }
542     });
543
544     return $results;
545 }
546
547
548
549 =head3 wants_check_for_previous_checkout
550
551     $wants_check = $patron->wants_check_for_previous_checkout;
552
553 Return 1 if Koha needs to perform PrevIssue checking, else 0.
554
555 =cut
556
557 sub wants_check_for_previous_checkout {
558     my ( $self ) = @_;
559     my $syspref = C4::Context->preference("checkPrevCheckout");
560
561     # Simple cases
562     ## Hard syspref trumps all
563     return 1 if ($syspref eq 'hardyes');
564     return 0 if ($syspref eq 'hardno');
565     ## Now, patron pref trumps all
566     return 1 if ($self->checkprevcheckout eq 'yes');
567     return 0 if ($self->checkprevcheckout eq 'no');
568
569     # More complex: patron inherits -> determine category preference
570     my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
571     return 1 if ($checkPrevCheckoutByCat eq 'yes');
572     return 0 if ($checkPrevCheckoutByCat eq 'no');
573
574     # Finally: category preference is inherit, default to 0
575     if ($syspref eq 'softyes') {
576         return 1;
577     } else {
578         return 0;
579     }
580 }
581
582 =head3 do_check_for_previous_checkout
583
584     $do_check = $patron->do_check_for_previous_checkout($item);
585
586 Return 1 if the bib associated with $ITEM has previously been checked out to
587 $PATRON, 0 otherwise.
588
589 =cut
590
591 sub do_check_for_previous_checkout {
592     my ( $self, $item ) = @_;
593
594     # Find all items for bib and extract item numbers.
595     my @items = Koha::Items->search({biblionumber => $item->{biblionumber}});
596     my @item_nos;
597     foreach my $item (@items) {
598         push @item_nos, $item->itemnumber;
599     }
600
601     # Create (old)issues search criteria
602     my $criteria = {
603         borrowernumber => $self->borrowernumber,
604         itemnumber => \@item_nos,
605     };
606
607     # Check current issues table
608     my $issues = Koha::Checkouts->search($criteria);
609     return 1 if $issues->count; # 0 || N
610
611     # Check old issues table
612     my $old_issues = Koha::Old::Checkouts->search($criteria);
613     return $old_issues->count;  # 0 || N
614 }
615
616 =head3 is_debarred
617
618 my $debarment_expiration = $patron->is_debarred;
619
620 Returns the date a patron debarment will expire, or undef if the patron is not
621 debarred
622
623 =cut
624
625 sub is_debarred {
626     my ($self) = @_;
627
628     return unless $self->debarred;
629     return $self->debarred
630       if $self->debarred =~ '^9999'
631       or dt_from_string( $self->debarred ) > dt_from_string;
632     return;
633 }
634
635 =head3 is_expired
636
637 my $is_expired = $patron->is_expired;
638
639 Returns 1 if the patron is expired or 0;
640
641 =cut
642
643 sub is_expired {
644     my ($self) = @_;
645     return 0 unless $self->dateexpiry;
646     return 0 if $self->dateexpiry =~ '^9999';
647     return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
648     return 0;
649 }
650
651 =head3 is_going_to_expire
652
653 my $is_going_to_expire = $patron->is_going_to_expire;
654
655 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
656
657 =cut
658
659 sub is_going_to_expire {
660     my ($self) = @_;
661
662     my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
663
664     return 0 unless $delay;
665     return 0 unless $self->dateexpiry;
666     return 0 if $self->dateexpiry =~ '^9999';
667     return 1 if dt_from_string( $self->dateexpiry )->subtract( days => $delay ) < dt_from_string->truncate( to => 'day' );
668     return 0;
669 }
670
671 =head3 update_password
672
673 my $updated = $patron->update_password( $userid, $password );
674
675 Update the userid and the password of a patron.
676 If the userid already exists, returns and let DBIx::Class warns
677 This will add an entry to action_logs if BorrowersLog is set.
678
679 =cut
680
681 sub update_password {
682     my ( $self, $userid, $password ) = @_;
683     eval { $self->userid($userid)->store; };
684     return if $@; # Make sure the userid is not already in used by another patron
685
686     return 0 if $password eq '****' or $password eq '';
687
688     if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
689         # Update the hashed PIN in borrower_sync.hashed_pin, before Koha hashes it
690         Koha::NorwegianPatronDB::NLUpdateHashedPIN( $self->borrowernumber, $password );
691     }
692
693     my $digest = Koha::AuthUtils::hash_password($password);
694     $self->update(
695         {
696             password       => $digest,
697             login_attempts => 0,
698         }
699     );
700
701     logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
702     return $digest;
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 $self->can_see_patrons_from( $patron->library->branchcode );
1125 }
1126
1127 =head3 can_see_patrons_from
1128
1129 my $can_see = $patron->can_see_patrons_from( $branchcode );
1130
1131 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
1132
1133 =cut
1134
1135 sub can_see_patrons_from {
1136     my ( $self, $branchcode ) = @_;
1137     my $can = 0;
1138     if ( $self->branchcode eq $branchcode ) {
1139         $can = 1;
1140     } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
1141         $can = 1;
1142     } elsif ( my $library_groups = $self->library->library_groups ) {
1143         while ( my $library_group = $library_groups->next ) {
1144             if ( $library_group->parent->has_child( $branchcode ) ) {
1145                 $can = 1;
1146                 last;
1147             }
1148         }
1149     }
1150     return $can;
1151 }
1152
1153 =head3 libraries_where_can_see_patrons
1154
1155 my $libraries = $patron-libraries_where_can_see_patrons;
1156
1157 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
1158 The branchcodes are arbitrarily returned sorted.
1159 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
1160
1161 An empty array means no restriction, the patron can see patron's infos from any libraries.
1162
1163 =cut
1164
1165 sub libraries_where_can_see_patrons {
1166     my ( $self ) = @_;
1167     my $userenv = C4::Context->userenv;
1168
1169     return () unless $userenv; # For tests, but userenv should be defined in tests...
1170
1171     my @restricted_branchcodes;
1172     if (C4::Context::only_my_library) {
1173         push @restricted_branchcodes, $self->branchcode;
1174     }
1175     else {
1176         unless (
1177             $self->has_permission(
1178                 { borrowers => 'view_borrower_infos_from_any_libraries' }
1179             )
1180           )
1181         {
1182             my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
1183             if ( $library_groups->count )
1184             {
1185                 while ( my $library_group = $library_groups->next ) {
1186                     my $parent = $library_group->parent;
1187                     if ( $parent->has_child( $self->branchcode ) ) {
1188                         push @restricted_branchcodes, $parent->children->get_column('branchcode');
1189                     }
1190                 }
1191             }
1192
1193             @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
1194         }
1195     }
1196
1197     @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
1198     @restricted_branchcodes = uniq(@restricted_branchcodes);
1199     @restricted_branchcodes = sort(@restricted_branchcodes);
1200     return @restricted_branchcodes;
1201 }
1202
1203 sub has_permission {
1204     my ( $self, $flagsrequired ) = @_;
1205     return unless $self->userid;
1206     # TODO code from haspermission needs to be moved here!
1207     return C4::Auth::haspermission( $self->userid, $flagsrequired );
1208 }
1209
1210 =head3 is_adult
1211
1212 my $is_adult = $patron->is_adult
1213
1214 Return true if the patron has a category with a type Adult (A) or Organization (I)
1215
1216 =cut
1217
1218 sub is_adult {
1219     my ( $self ) = @_;
1220     return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
1221 }
1222
1223 =head3 is_child
1224
1225 my $is_child = $patron->is_child
1226
1227 Return true if the patron has a category with a type Child (C)
1228
1229 =cut
1230 sub is_child {
1231     my( $self ) = @_;
1232     return $self->category->category_type eq 'C' ? 1 : 0;
1233 }
1234
1235 =head3 has_valid_userid
1236
1237 my $patron = Koha::Patrons->find(42);
1238 $patron->userid( $new_userid );
1239 my $has_a_valid_userid = $patron->has_valid_userid
1240
1241 my $patron = Koha::Patron->new( $params );
1242 my $has_a_valid_userid = $patron->has_valid_userid
1243
1244 Return true if the current userid of this patron is valid/unique, otherwise false.
1245
1246 Note that this should be done in $self->store instead and raise an exception if needed.
1247
1248 =cut
1249
1250 sub has_valid_userid {
1251     my ($self) = @_;
1252
1253     return 0 unless $self->userid;
1254
1255     return 0 if ( $self->userid eq C4::Context->config('user') );    # DB user
1256
1257     my $already_exists = Koha::Patrons->search(
1258         {
1259             userid => $self->userid,
1260             (
1261                 $self->in_storage
1262                 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1263                 : ()
1264             ),
1265         }
1266     )->count;
1267     return $already_exists ? 0 : 1;
1268 }
1269
1270 =head3 generate_userid
1271
1272 my $patron = Koha::Patron->new( $params );
1273 $patron->generate_userid
1274
1275 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1276
1277 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).
1278
1279 =cut
1280
1281 sub generate_userid {
1282     my ($self) = @_;
1283     my $offset = 0;
1284     my $firstname = $self->firstname // q{};
1285     my $surname = $self->surname // q{};
1286     #The script will "do" the following code and increment the $offset until the generated userid is unique
1287     do {
1288       $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1289       $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1290       my $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1291       $userid = unac_string('utf-8',$userid);
1292       $userid .= $offset unless $offset == 0;
1293       $self->userid( $userid );
1294       $offset++;
1295      } while (! $self->has_valid_userid );
1296
1297      return $self;
1298
1299 }
1300
1301 =head2 Internal methods
1302
1303 =head3 _type
1304
1305 =cut
1306
1307 sub _type {
1308     return 'Borrower';
1309 }
1310
1311 =head1 AUTHOR
1312
1313 Kyle M Hall <kyle@bywatersolutions.com>
1314 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1315
1316 =cut
1317
1318 1;