Bug 33940: Move C4::Members cardnumber sub to Koha::Policy::Patrons::Cardnumber
[koha.git] / C4 / Members.pm
1 package C4::Members;
2
3 # Copyright 2000-2003 Katipo Communications
4 # Copyright 2010 BibLibre
5 # Parts Copyright 2010 Catalyst IT
6 #
7 # This file is part of Koha.
8 #
9 # Koha is free software; you can redistribute it and/or modify it
10 # under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # Koha is distributed in the hope that it will be useful, but
15 # WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with Koha; if not, see <http://www.gnu.org/licenses>.
21
22
23 use Modern::Perl;
24 use C4::Context;
25 use Scalar::Util qw( looks_like_number );
26 use Date::Calc qw( check_date Date_to_Days );
27 use C4::Overdues qw( checkoverdues );
28 use C4::Reserves;
29 use C4::Accounts;
30 use C4::Letters qw( GetPreparedLetter );
31 use DateTime;
32 use Koha::Database;
33 use Koha::DateUtils qw( dt_from_string output_pref );
34 use Koha::Database;
35 use Koha::Holds;
36 use Koha::AdditionalContents;
37 use Koha::Patrons;
38 use Koha::Patron::Categories;
39
40 our (@ISA, @EXPORT_OK);
41 BEGIN {
42     require Exporter;
43     @ISA = qw(Exporter);
44     @EXPORT_OK = qw(
45       GetAllIssues
46
47       GetBorrowersToExpunge
48
49       IssueSlip
50
51       DeleteUnverifiedOpacRegistrations
52       DeleteExpiredOpacRegistrations
53     );
54 }
55
56 =head1 NAME
57
58 C4::Members - Perl Module containing convenience functions for member handling
59
60 =head1 SYNOPSIS
61
62 use C4::Members;
63
64 =head1 DESCRIPTION
65
66 This module contains routines for adding, modifying and deleting members/patrons/borrowers
67
68 =head1 FUNCTIONS
69
70 =head2 patronflags
71
72  $flags = &patronflags($patron);
73
74 This function is not exported.
75
76 The following will be set where applicable:
77  $flags->{CHARGES}->{amount}        Amount of debt
78  $flags->{CHARGES}->{noissues}      Set if debt amount >$5.00 (or syspref noissuescharge)
79  $flags->{CHARGES}->{message}       Message -- deprecated
80
81  $flags->{CREDITS}->{amount}        Amount of credit
82  $flags->{CREDITS}->{message}       Message -- deprecated
83
84  $flags->{  GNA  }                  Patron has no valid address
85  $flags->{  GNA  }->{noissues}      Set for each GNA
86  $flags->{  GNA  }->{message}       "Borrower has no valid address" -- deprecated
87
88  $flags->{ LOST  }                  Patron's card reported lost
89  $flags->{ LOST  }->{noissues}      Set for each LOST
90  $flags->{ LOST  }->{message}       Message -- deprecated
91
92  $flags->{DBARRED}                  Set if patron debarred, no access
93  $flags->{DBARRED}->{noissues}      Set for each DBARRED
94  $flags->{DBARRED}->{message}       Message -- deprecated
95
96  $flags->{ NOTES }
97  $flags->{ NOTES }->{message}       The note itself.  NOT deprecated
98
99  $flags->{ ODUES }                  Set if patron has overdue books.
100  $flags->{ ODUES }->{message}       "Yes"  -- deprecated
101  $flags->{ ODUES }->{itemlist}      ref-to-array: list of overdue books
102  $flags->{ ODUES }->{itemlisttext}  Text list of overdue items -- deprecated
103
104  $flags->{WAITING}                  Set if any of patron's reserves are available
105  $flags->{WAITING}->{message}       Message -- deprecated
106  $flags->{WAITING}->{itemlist}      ref-to-array: list of available items
107
108 =over
109
110 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlist}> is a reference-to-array listing the
111 overdue items. Its elements are references-to-hash, each describing an
112 overdue item. The keys are selected fields from the issues, biblio,
113 biblioitems, and items tables of the Koha database.
114
115 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlisttext}> is a string giving a text listing of
116 the overdue items, one per line.  Deprecated.
117
118 =item C<$flags-E<gt>{WAITING}-E<gt>{itemlist}> is a reference-to-array listing the
119 available items. Each element is a reference-to-hash whose keys are
120 fields from the reserves table of the Koha database.
121
122 =back
123
124 All the "message" fields that include language generated in this function are deprecated,
125 because such strings belong properly in the display layer.
126
127 The "message" field that comes from the DB is OK.
128
129 =cut
130
131 # TODO: use {anonymous => hashes} instead of a dozen %flaginfo
132 # FIXME rename this function.
133 # DEPRECATED Do not use this subroutine!
134 sub patronflags {
135     my %flags;
136     my ( $patroninformation) = @_;
137     my $dbh=C4::Context->dbh;
138     my $patron = Koha::Patrons->find( $patroninformation->{borrowernumber} );
139     my $account = $patron->account;
140     my $owing = $account->non_issues_charges;
141     if ( $owing > 0 ) {
142         my %flaginfo;
143         my $noissuescharge = C4::Context->preference("noissuescharge") || 5;
144         $flaginfo{'message'} = sprintf 'Patron owes %.02f', $owing;
145         $flaginfo{'amount'}  = sprintf "%.02f", $owing;
146         if ( $owing > $noissuescharge && !C4::Context->preference("AllowFineOverride") ) {
147             $flaginfo{'noissues'} = 1;
148         }
149         $flags{'CHARGES'} = \%flaginfo;
150     }
151     elsif ( ( my $balance = $account->balance ) < 0 ) {
152         my %flaginfo;
153         $flaginfo{'message'} = sprintf 'Patron has credit of %.02f', -$balance;
154         $flaginfo{'amount'}  = sprintf "%.02f", $balance;
155         $flags{'CREDITS'} = \%flaginfo;
156     }
157
158     # Check the debt of the guarntees of this patron
159     my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
160     $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
161     if ( defined $no_issues_charge_guarantees ) {
162         my $p = Koha::Patrons->find( $patroninformation->{borrowernumber} );
163         my @guarantees = map { $_->guarantee } $p->guarantee_relationships->as_list;
164         my $guarantees_non_issues_charges = 0;
165         foreach my $g ( @guarantees ) {
166             $guarantees_non_issues_charges += $g->account->non_issues_charges;
167         }
168
169         if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees ) {
170             my %flaginfo;
171             $flaginfo{'message'} = sprintf 'patron guarantees owe %.02f', $guarantees_non_issues_charges;
172             $flaginfo{'amount'}  = $guarantees_non_issues_charges;
173             $flaginfo{'noissues'} = 1 unless C4::Context->preference("allowfineoverride");
174             $flags{'CHARGES_GUARANTEES'} = \%flaginfo;
175         }
176     }
177
178     if (   $patroninformation->{'gonenoaddress'}
179         && $patroninformation->{'gonenoaddress'} == 1 )
180     {
181         my %flaginfo;
182         $flaginfo{'message'}  = 'Borrower has no valid address.';
183         $flaginfo{'noissues'} = 1;
184         $flags{'GNA'}         = \%flaginfo;
185     }
186     if ( $patroninformation->{'lost'} && $patroninformation->{'lost'} == 1 ) {
187         my %flaginfo;
188         $flaginfo{'message'}  = 'Borrower\'s card reported lost.';
189         $flaginfo{'noissues'} = 1;
190         $flags{'LOST'}        = \%flaginfo;
191     }
192     if ( $patroninformation->{'debarred'} && check_date( split( /-/, $patroninformation->{'debarred'} ) ) ) {
193         if ( Date_to_Days(Date::Calc::Today) < Date_to_Days( split( /-/, $patroninformation->{'debarred'} ) ) ) {
194             my %flaginfo;
195             $flaginfo{'debarredcomment'} = $patroninformation->{'debarredcomment'};
196             $flaginfo{'message'}         = $patroninformation->{'debarredcomment'};
197             $flaginfo{'noissues'}        = 1;
198             $flaginfo{'dateend'}         = $patroninformation->{'debarred'};
199             $flags{'DBARRED'}           = \%flaginfo;
200         }
201     }
202     if (   $patroninformation->{'borrowernotes'}
203         && $patroninformation->{'borrowernotes'} )
204     {
205         my %flaginfo;
206         $flaginfo{'message'} = $patroninformation->{'borrowernotes'};
207         $flags{'NOTES'}      = \%flaginfo;
208     }
209     my ( $odues, $itemsoverdue ) = C4::Overdues::checkoverdues($patroninformation->{'borrowernumber'});
210     if ( $odues && $odues > 0 ) {
211         my %flaginfo;
212         $flaginfo{'message'}  = "Yes";
213         $flaginfo{'itemlist'} = $itemsoverdue;
214         foreach ( sort { $a->{'date_due'} cmp $b->{'date_due'} }
215             @$itemsoverdue )
216         {
217             $flaginfo{'itemlisttext'} .=
218               "$_->{'date_due'} $_->{'barcode'} $_->{'title'} \n";  # newline is display layer
219         }
220         $flags{'ODUES'} = \%flaginfo;
221     }
222
223     my $waiting_holds = $patron->holds->search({ found => 'W' });
224     my $nowaiting = $waiting_holds->count;
225     if ( $nowaiting > 0 ) {
226         my %flaginfo;
227         $flaginfo{'message'}  = "Reserved items available";
228         $flaginfo{'itemlist'} = $waiting_holds->unblessed;
229         $flags{'WAITING'}     = \%flaginfo;
230     }
231     return ( \%flags );
232 }
233
234 =head2 GetAllIssues
235
236   $issues = &GetAllIssues($borrowernumber, $sortkey, $limit);
237
238 Looks up what the patron with the given borrowernumber has borrowed,
239 and sorts the results.
240
241 C<$sortkey> is the name of a field on which to sort the results. This
242 should be the name of a field in the C<issues>, C<biblio>,
243 C<biblioitems>, or C<items> table in the Koha database.
244
245 C<$limit> is the maximum number of results to return.
246
247 C<&GetAllIssues> an arrayref, C<$issues>, of hashrefs, the keys of which
248 are the fields from the C<issues>, C<biblio>, C<biblioitems>, and
249 C<items> tables of the Koha database.
250
251 =cut
252
253 #'
254 sub GetAllIssues {
255     my ( $borrowernumber, $order, $limit ) = @_;
256
257     return unless $borrowernumber;
258     $order = 'date_due desc' unless $order;
259
260     my $dbh = C4::Context->dbh;
261     my $query =
262 'SELECT issues.*, items.*, biblio.*, biblioitems.*, issues.timestamp as issuestimestamp, issues.renewals_count AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp,borrowers.firstname,borrowers.surname
263   FROM issues
264   LEFT JOIN items on items.itemnumber=issues.itemnumber
265   LEFT JOIN borrowers on borrowers.borrowernumber=issues.issuer_id
266   LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
267   LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
268   WHERE issues.borrowernumber=?
269   UNION ALL
270   SELECT old_issues.*, items.*, biblio.*, biblioitems.*, old_issues.timestamp as issuestimestamp, old_issues.renewals_count AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp,borrowers.firstname,borrowers.surname
271   FROM old_issues
272   LEFT JOIN items on items.itemnumber=old_issues.itemnumber
273   LEFT JOIN borrowers on borrowers.borrowernumber=old_issues.issuer_id
274   LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
275   LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
276   WHERE old_issues.borrowernumber=? AND old_issues.itemnumber IS NOT NULL
277   order by ' . $order;
278     if ($limit) {
279         $query .= " limit $limit";
280     }
281
282     my $sth = $dbh->prepare($query);
283     $sth->execute( $borrowernumber, $borrowernumber );
284     return $sth->fetchall_arrayref( {} );
285 }
286
287 =head2 GetBorrowersToExpunge
288
289   $borrowers = &GetBorrowersToExpunge(
290       not_borrowed_since => $not_borrowed_since,
291       expired_before       => $expired_before,
292       category_code        => \@category_code,
293       patron_list_id       => $patron_list_id,
294       branchcode           => $branchcode
295   );
296
297   This function get all borrowers based on the given criteria.
298
299 =cut
300
301 sub GetBorrowersToExpunge {
302
303     my $params = shift;
304     my $filterdate       = $params->{'not_borrowed_since'};
305     my $filterexpiry     = $params->{'expired_before'};
306     my $filterlastseen   = $params->{'last_seen'};
307     my $filtercategory   = $params->{'category_code'};
308     my $filterbranch     = $params->{'branchcode'} ||
309                         ((C4::Context->preference('IndependentBranches')
310                              && C4::Context->userenv
311                              && !C4::Context->IsSuperLibrarian()
312                              && C4::Context->userenv->{branch})
313                          ? C4::Context->userenv->{branch}
314                          : "");
315     my $filterpatronlist = $params->{'patron_list_id'};
316
317     my $dbh   = C4::Context->dbh;
318     my $query = q|
319         SELECT *
320         FROM (
321             SELECT borrowers.borrowernumber,
322                    MAX(old_issues.timestamp) AS latestissue,
323                    MAX(issues.timestamp) AS currentissue
324             FROM   borrowers
325             JOIN   categories USING (categorycode)
326             LEFT JOIN (
327                 SELECT guarantor_id
328                 FROM borrower_relationships
329                 WHERE guarantor_id IS NOT NULL
330                     AND guarantor_id <> 0
331             ) as tmp ON borrowers.borrowernumber=tmp.guarantor_id
332             LEFT JOIN old_issues USING (borrowernumber)
333             LEFT JOIN issues USING (borrowernumber)|;
334     if ( $filterpatronlist  ){
335         $query .= q| LEFT JOIN patron_list_patrons USING (borrowernumber)|;
336     }
337     $query .= q| WHERE  category_type <> 'S'
338         AND ( borrowers.flags IS NULL OR borrowers.flags = 0 )
339         AND tmp.guarantor_id IS NULL
340     |;
341     my @query_params;
342     if ( $filterbranch && $filterbranch ne "" ) {
343         $query.= " AND borrowers.branchcode = ? ";
344         push( @query_params, $filterbranch );
345     }
346     if ( $filterexpiry ) {
347         $query .= " AND dateexpiry < ? ";
348         push( @query_params, $filterexpiry );
349     }
350     if ( $filterlastseen ) {
351         $query .= ' AND lastseen < ? ';
352         push @query_params, $filterlastseen;
353     }
354     if ( $filtercategory ) {
355         if (ref($filtercategory) ne 'ARRAY' ) {
356             $filtercategory = [ $filtercategory ];
357         }
358         if ( @$filtercategory ) {
359             $query .= " AND categorycode IN (" . join(',', ('?') x @$filtercategory) . ") ";
360             push( @query_params, @$filtercategory );
361         }
362     }
363     if ( $filterpatronlist ){
364         $query.=" AND patron_list_id = ? ";
365         push( @query_params, $filterpatronlist );
366     }
367     $query .= " GROUP BY borrowers.borrowernumber";
368     $query .= q|
369         ) xxx WHERE currentissue IS NULL|;
370     if ( $filterdate ) {
371         $query.=" AND ( latestissue < ? OR latestissue IS NULL ) ";
372         push @query_params,$filterdate;
373     }
374
375     if ( my $anonymous_patron = C4::Context->preference("AnonymousPatron") ) {
376         $query .= q{ AND borrowernumber != ? };
377         push( @query_params, $anonymous_patron );
378     }
379
380     my $sth = $dbh->prepare($query);
381     if (scalar(@query_params)>0){
382         $sth->execute(@query_params);
383     }
384     else {
385         $sth->execute;
386     }
387
388     my @results;
389     while ( my $data = $sth->fetchrow_hashref ) {
390         push @results, $data;
391     }
392     return \@results;
393 }
394
395 =head2 IssueSlip
396
397   IssueSlip($branchcode, $borrowernumber, $quickslip)
398
399   Returns letter hash ( see C4::Letters::GetPreparedLetter )
400
401   $quickslip is boolean, to indicate whether we want a quick slip
402
403   IssueSlip populates ISSUESLIP and ISSUEQSLIP, and will make the following expansions:
404
405   Both slips:
406
407       <<branches.*>>
408       <<borrowers.*>>
409
410   ISSUESLIP:
411
412       <checkedout>
413          <<biblio.*>>
414          <<items.*>>
415          <<biblioitems.*>>
416          <<issues.*>>
417       </checkedout>
418
419       <overdue>
420          <<biblio.*>>
421          <<items.*>>
422          <<biblioitems.*>>
423          <<issues.*>>
424       </overdue>
425
426       <news>
427          <<additional_contents.*>>
428       </news>
429
430   ISSUEQSLIP:
431
432       <checkedout>
433          <<biblio.*>>
434          <<items.*>>
435          <<biblioitems.*>>
436          <<issues.*>>
437       </checkedout>
438
439   NOTE: Fields from tables issues, items, biblio and biblioitems are available
440
441 =cut
442
443 sub IssueSlip {
444     my ($branch, $borrowernumber, $quickslip) = @_;
445
446     # FIXME Check callers before removing this statement
447     #return unless $borrowernumber;
448
449     my $patron = Koha::Patrons->find( $borrowernumber );
450     return unless $patron;
451
452     my $pending_checkouts = $patron->pending_checkouts; # Should be $patron->checkouts->pending?
453
454     my ($letter_code, %repeat, %loops);
455     if ( $quickslip ) {
456         my $today_start = dt_from_string->set( hour => 0, minute => 0, second => 0 );
457         my $today_end = dt_from_string->set( hour => 23, minute => 59, second => 0 );
458         $today_start = Koha::Database->new->schema->storage->datetime_parser->format_datetime( $today_start );
459         $today_end = Koha::Database->new->schema->storage->datetime_parser->format_datetime( $today_end );
460         $letter_code = 'ISSUEQSLIP';
461
462         # issue date or lastreneweddate is today
463         my $todays_checkouts = $pending_checkouts->search(
464             {
465                 -or => {
466                     issuedate => {
467                         '>=' => $today_start,
468                         '<=' => $today_end,
469                     },
470                     lastreneweddate =>
471                       { '>=' => $today_start, '<=' => $today_end, }
472                 }
473             }
474         );
475         my @checkouts;
476         while ( my $c = $todays_checkouts->next ) {
477             my $all = $c->unblessed_all_relateds;
478             push @checkouts, {
479                 biblio      => $all,
480                 items       => $all,
481                 biblioitems => $all,
482                 issues      => $all,
483             };
484         }
485
486         %repeat =  (
487             checkedout => \@checkouts, # Historical syntax
488         );
489         %loops = (
490             issues => [ map { $_->{issues}{itemnumber} } @checkouts ], # TT syntax
491         );
492     }
493     else {
494         my $today = Koha::Database->new->schema->storage->datetime_parser->format_datetime( dt_from_string );
495         # Checkouts due in the future
496         my $checkouts = $pending_checkouts->search({ date_due => { '>' => $today } });
497         my @checkouts; my @overdues;
498         while ( my $c = $checkouts->next ) {
499             my $all = $c->unblessed_all_relateds;
500             push @checkouts, {
501                 biblio      => $all,
502                 items       => $all,
503                 biblioitems => $all,
504                 issues      => $all,
505             };
506         }
507
508         # Checkouts due in the past are overdues
509         my $overdues = $pending_checkouts->search({ date_due => { '<=' => $today } });
510         while ( my $o = $overdues->next ) {
511             my $all = $o->unblessed_all_relateds;
512             push @overdues, {
513                 biblio      => $all,
514                 items       => $all,
515                 biblioitems => $all,
516                 issues      => $all,
517             };
518         }
519         my $news = Koha::AdditionalContents->search_for_display(
520             {
521                 category   => 'news',
522                 location   => 'slip',
523                 lang       => $patron->lang,
524                 library_id => $branch,
525             }
526         );
527         my @news;
528         while ( my $n = $news->next ) {
529             my $all = $n->unblessed_all_relateds;
530
531             # FIXME We keep newdate and timestamp for backward compatibility (from GetNewsToDisplay)
532             # But we should remove them and adjust the existing templates in a db rev
533             # FIXME This must be formatted in the notice template
534             my $published_on_dt = output_pref({ dt => dt_from_string( $all->{published_on} ), dateonly => 1 });
535             $all->{newdate} = $published_on_dt;
536             $all->{timestamp} = $published_on_dt;
537
538             push @news, {
539                 additional_contents => $all,
540             };
541         }
542         $letter_code = 'ISSUESLIP';
543         %repeat      = (
544             checkedout => \@checkouts,
545             overdue    => \@overdues,
546             news       => \@news,
547         );
548         %loops = (
549             issues => [ map { $_->{issues}{itemnumber} } @checkouts ],
550             overdues   => [ map { $_->{issues}{itemnumber} } @overdues ],
551             opac_news => [ map { $_->{additional_contents}{idnew} } @news ],
552             additional_contents => [ map { $_->{additional_contents}{idnew} } @news ],
553         );
554     }
555
556     return  C4::Letters::GetPreparedLetter (
557         module => 'circulation',
558         letter_code => $letter_code,
559         branchcode => $branch,
560         lang => $patron->lang,
561         tables => {
562             'branches'    => $branch,
563             'borrowers'   => $borrowernumber,
564         },
565         repeat => \%repeat,
566         loops => \%loops,
567     );
568 }
569
570 =head2 DeleteExpiredOpacRegistrations
571
572     Delete accounts that haven't been upgraded from the 'temporary' category
573     Returns the number of removed patrons
574
575 =cut
576
577 sub DeleteExpiredOpacRegistrations {
578
579     my $delay = C4::Context->preference('PatronSelfRegistrationExpireTemporaryAccountsDelay');
580     my $category_code = C4::Context->preference('PatronSelfRegistrationDefaultCategory');
581
582     return 0 unless $category_code && $delay;
583         # DO NOT REMOVE test on delay here!
584         # Some libraries may not use a temporary category, but want to keep patrons.
585         # We should not delete patrons when the value is NULL, empty string or 0.
586
587     my $date_enrolled = dt_from_string();
588     $date_enrolled->subtract( days => $delay );
589
590     my $registrations_to_del = Koha::Patrons->search({
591         dateenrolled => {'<=' => $date_enrolled->ymd},
592         categorycode => $category_code,
593     });
594
595     my $cnt=0;
596     while ( my $registration = $registrations_to_del->next() ) {
597         next if $registration->checkouts->count || $registration->account->balance;
598         $registration->delete;
599         $cnt++;
600     }
601     return $cnt;
602 }
603
604 =head2 DeleteUnverifiedOpacRegistrations
605
606     Delete all unverified self registrations in borrower_modifications,
607     older than the specified number of days.
608
609 =cut
610
611 sub DeleteUnverifiedOpacRegistrations {
612     my ( $days ) = @_;
613     my $dbh = C4::Context->dbh;
614     my $sql=qq|
615 DELETE FROM borrower_modifications
616 WHERE borrowernumber = 0 AND DATEDIFF( NOW(), timestamp ) > ?|;
617     my $cnt=$dbh->do($sql, undef, ($days) );
618     return $cnt eq '0E0'? 0: $cnt;
619 }
620
621 END { }    # module clean-up code here (global destructor)
622
623 1;
624
625 __END__
626
627 =head1 AUTHOR
628
629 Koha Team
630
631 =cut