Bug 24760: Use C4::BackgroundJob->fetch in tests
[koha.git] / C4 / Members.pm
blobeb5dbfb4e27cb91ad37f749e3314097818213a6a
1 package C4::Members;
3 # Copyright 2000-2003 Katipo Communications
4 # Copyright 2010 BibLibre
5 # Parts Copyright 2010 Catalyst IT
7 # This file is part of Koha.
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.
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.
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>.
23 use Modern::Perl;
24 use C4::Context;
25 use String::Random qw( random_string );
26 use Scalar::Util qw( looks_like_number );
27 use Date::Calc qw/Today check_date Date_to_Days/;
28 use List::MoreUtils qw( uniq );
29 use JSON qw(to_json);
30 use C4::Log; # logaction
31 use C4::Overdues;
32 use C4::Reserves;
33 use C4::Accounts;
34 use C4::Biblio;
35 use C4::Letters;
36 use C4::Members::Attributes qw(SearchIdMatchingAttribute UpdateBorrowerAttribute);
37 use C4::NewsChannels; #get slip news
38 use DateTime;
39 use Koha::Database;
40 use Koha::DateUtils;
41 use Koha::AuthUtils qw(hash_password);
42 use Koha::Database;
43 use Koha::Holds;
44 use Koha::List::Patron;
45 use Koha::Patrons;
46 use Koha::Patron::Categories;
48 our (@ISA,@EXPORT,@EXPORT_OK,$debug);
50 BEGIN {
51 $debug = $ENV{DEBUG} || 0;
52 require Exporter;
53 @ISA = qw(Exporter);
54 #Get data
55 push @EXPORT, qw(
57 &GetAllIssues
59 &GetBorrowersToExpunge
61 &IssueSlip
64 #Check data
65 push @EXPORT, qw(
66 &checkuserpassword
67 &checkcardnumber
71 =head1 NAME
73 C4::Members - Perl Module containing convenience functions for member handling
75 =head1 SYNOPSIS
77 use C4::Members;
79 =head1 DESCRIPTION
81 This module contains routines for adding, modifying and deleting members/patrons/borrowers
83 =head1 FUNCTIONS
85 =head2 patronflags
87 $flags = &patronflags($patron);
89 This function is not exported.
91 The following will be set where applicable:
92 $flags->{CHARGES}->{amount} Amount of debt
93 $flags->{CHARGES}->{noissues} Set if debt amount >$5.00 (or syspref noissuescharge)
94 $flags->{CHARGES}->{message} Message -- deprecated
96 $flags->{CREDITS}->{amount} Amount of credit
97 $flags->{CREDITS}->{message} Message -- deprecated
99 $flags->{ GNA } Patron has no valid address
100 $flags->{ GNA }->{noissues} Set for each GNA
101 $flags->{ GNA }->{message} "Borrower has no valid address" -- deprecated
103 $flags->{ LOST } Patron's card reported lost
104 $flags->{ LOST }->{noissues} Set for each LOST
105 $flags->{ LOST }->{message} Message -- deprecated
107 $flags->{DBARRED} Set if patron debarred, no access
108 $flags->{DBARRED}->{noissues} Set for each DBARRED
109 $flags->{DBARRED}->{message} Message -- deprecated
111 $flags->{ NOTES }
112 $flags->{ NOTES }->{message} The note itself. NOT deprecated
114 $flags->{ ODUES } Set if patron has overdue books.
115 $flags->{ ODUES }->{message} "Yes" -- deprecated
116 $flags->{ ODUES }->{itemlist} ref-to-array: list of overdue books
117 $flags->{ ODUES }->{itemlisttext} Text list of overdue items -- deprecated
119 $flags->{WAITING} Set if any of patron's reserves are available
120 $flags->{WAITING}->{message} Message -- deprecated
121 $flags->{WAITING}->{itemlist} ref-to-array: list of available items
123 =over
125 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlist}> is a reference-to-array listing the
126 overdue items. Its elements are references-to-hash, each describing an
127 overdue item. The keys are selected fields from the issues, biblio,
128 biblioitems, and items tables of the Koha database.
130 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlisttext}> is a string giving a text listing of
131 the overdue items, one per line. Deprecated.
133 =item C<$flags-E<gt>{WAITING}-E<gt>{itemlist}> is a reference-to-array listing the
134 available items. Each element is a reference-to-hash whose keys are
135 fields from the reserves table of the Koha database.
137 =back
139 All the "message" fields that include language generated in this function are deprecated,
140 because such strings belong properly in the display layer.
142 The "message" field that comes from the DB is OK.
144 =cut
146 # TODO: use {anonymous => hashes} instead of a dozen %flaginfo
147 # FIXME rename this function.
148 # DEPRECATED Do not use this subroutine!
149 sub patronflags {
150 my %flags;
151 my ( $patroninformation) = @_;
152 my $dbh=C4::Context->dbh;
153 my $patron = Koha::Patrons->find( $patroninformation->{borrowernumber} );
154 my $account = $patron->account;
155 my $owing = $account->non_issues_charges;
156 if ( $owing > 0 ) {
157 my %flaginfo;
158 my $noissuescharge = C4::Context->preference("noissuescharge") || 5;
159 $flaginfo{'message'} = sprintf 'Patron owes %.02f', $owing;
160 $flaginfo{'amount'} = sprintf "%.02f", $owing;
161 if ( $owing > $noissuescharge && !C4::Context->preference("AllowFineOverride") ) {
162 $flaginfo{'noissues'} = 1;
164 $flags{'CHARGES'} = \%flaginfo;
166 elsif ( ( my $balance = $account->balance ) < 0 ) {
167 my %flaginfo;
168 $flaginfo{'message'} = sprintf 'Patron has credit of %.02f', -$balance;
169 $flaginfo{'amount'} = sprintf "%.02f", $balance;
170 $flags{'CREDITS'} = \%flaginfo;
173 # Check the debt of the guarntees of this patron
174 my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
175 $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
176 if ( defined $no_issues_charge_guarantees ) {
177 my $p = Koha::Patrons->find( $patroninformation->{borrowernumber} );
178 my @guarantees = map { $_->guarantee } $p->guarantee_relationships;
179 my $guarantees_non_issues_charges;
180 foreach my $g ( @guarantees ) {
181 $guarantees_non_issues_charges += $g->account->non_issues_charges;
184 if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees ) {
185 my %flaginfo;
186 $flaginfo{'message'} = sprintf 'patron guarantees owe %.02f', $guarantees_non_issues_charges;
187 $flaginfo{'amount'} = $guarantees_non_issues_charges;
188 $flaginfo{'noissues'} = 1 unless C4::Context->preference("allowfineoverride");
189 $flags{'CHARGES_GUARANTEES'} = \%flaginfo;
193 if ( $patroninformation->{'gonenoaddress'}
194 && $patroninformation->{'gonenoaddress'} == 1 )
196 my %flaginfo;
197 $flaginfo{'message'} = 'Borrower has no valid address.';
198 $flaginfo{'noissues'} = 1;
199 $flags{'GNA'} = \%flaginfo;
201 if ( $patroninformation->{'lost'} && $patroninformation->{'lost'} == 1 ) {
202 my %flaginfo;
203 $flaginfo{'message'} = 'Borrower\'s card reported lost.';
204 $flaginfo{'noissues'} = 1;
205 $flags{'LOST'} = \%flaginfo;
207 if ( $patroninformation->{'debarred'} && check_date( split( /-/, $patroninformation->{'debarred'} ) ) ) {
208 if ( Date_to_Days(Date::Calc::Today) < Date_to_Days( split( /-/, $patroninformation->{'debarred'} ) ) ) {
209 my %flaginfo;
210 $flaginfo{'debarredcomment'} = $patroninformation->{'debarredcomment'};
211 $flaginfo{'message'} = $patroninformation->{'debarredcomment'};
212 $flaginfo{'noissues'} = 1;
213 $flaginfo{'dateend'} = $patroninformation->{'debarred'};
214 $flags{'DBARRED'} = \%flaginfo;
217 if ( $patroninformation->{'borrowernotes'}
218 && $patroninformation->{'borrowernotes'} )
220 my %flaginfo;
221 $flaginfo{'message'} = $patroninformation->{'borrowernotes'};
222 $flags{'NOTES'} = \%flaginfo;
224 my ( $odues, $itemsoverdue ) = C4::Overdues::checkoverdues($patroninformation->{'borrowernumber'});
225 if ( $odues && $odues > 0 ) {
226 my %flaginfo;
227 $flaginfo{'message'} = "Yes";
228 $flaginfo{'itemlist'} = $itemsoverdue;
229 foreach ( sort { $a->{'date_due'} cmp $b->{'date_due'} }
230 @$itemsoverdue )
232 $flaginfo{'itemlisttext'} .=
233 "$_->{'date_due'} $_->{'barcode'} $_->{'title'} \n"; # newline is display layer
235 $flags{'ODUES'} = \%flaginfo;
238 my $waiting_holds = $patron->holds->search({ found => 'W' });
239 my $nowaiting = $waiting_holds->count;
240 if ( $nowaiting > 0 ) {
241 my %flaginfo;
242 $flaginfo{'message'} = "Reserved items available";
243 $flaginfo{'itemlist'} = $waiting_holds->unblessed;
244 $flags{'WAITING'} = \%flaginfo;
246 return ( \%flags );
249 =head2 GetAllIssues
251 $issues = &GetAllIssues($borrowernumber, $sortkey, $limit);
253 Looks up what the patron with the given borrowernumber has borrowed,
254 and sorts the results.
256 C<$sortkey> is the name of a field on which to sort the results. This
257 should be the name of a field in the C<issues>, C<biblio>,
258 C<biblioitems>, or C<items> table in the Koha database.
260 C<$limit> is the maximum number of results to return.
262 C<&GetAllIssues> an arrayref, C<$issues>, of hashrefs, the keys of which
263 are the fields from the C<issues>, C<biblio>, C<biblioitems>, and
264 C<items> tables of the Koha database.
266 =cut
269 sub GetAllIssues {
270 my ( $borrowernumber, $order, $limit ) = @_;
272 return unless $borrowernumber;
273 $order = 'date_due desc' unless $order;
275 my $dbh = C4::Context->dbh;
276 my $query =
277 'SELECT *, issues.timestamp as issuestimestamp, issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
278 FROM issues
279 LEFT JOIN items on items.itemnumber=issues.itemnumber
280 LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
281 LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
282 WHERE borrowernumber=?
283 UNION ALL
284 SELECT *, old_issues.timestamp as issuestimestamp, old_issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
285 FROM old_issues
286 LEFT JOIN items on items.itemnumber=old_issues.itemnumber
287 LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
288 LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
289 WHERE borrowernumber=? AND old_issues.itemnumber IS NOT NULL
290 order by ' . $order;
291 if ($limit) {
292 $query .= " limit $limit";
295 my $sth = $dbh->prepare($query);
296 $sth->execute( $borrowernumber, $borrowernumber );
297 return $sth->fetchall_arrayref( {} );
300 sub checkcardnumber {
301 my ( $cardnumber, $borrowernumber ) = @_;
303 # If cardnumber is null, we assume they're allowed.
304 return 0 unless defined $cardnumber;
306 my $dbh = C4::Context->dbh;
307 my $query = "SELECT * FROM borrowers WHERE cardnumber=?";
308 $query .= " AND borrowernumber <> ?" if ($borrowernumber);
309 my $sth = $dbh->prepare($query);
310 $sth->execute(
311 $cardnumber,
312 ( $borrowernumber ? $borrowernumber : () )
315 return 1 if $sth->fetchrow_hashref;
317 my ( $min_length, $max_length ) = get_cardnumber_length();
318 return 2
319 if length $cardnumber > $max_length
320 or length $cardnumber < $min_length;
322 return 0;
325 =head2 get_cardnumber_length
327 my ($min, $max) = C4::Members::get_cardnumber_length()
329 Returns the minimum and maximum length for patron cardnumbers as
330 determined by the CardnumberLength system preference, the
331 BorrowerMandatoryField system preference, and the width of the
332 database column.
334 =cut
336 sub get_cardnumber_length {
337 my $borrower = Koha::Database->new->schema->resultset('Borrower');
338 my $field_size = $borrower->result_source->column_info('cardnumber')->{size};
339 my ( $min, $max ) = ( 0, $field_size ); # borrowers.cardnumber is a nullable varchar(20)
340 $min = 1 if C4::Context->preference('BorrowerMandatoryField') =~ /cardnumber/;
341 if ( my $cardnumber_length = C4::Context->preference('CardnumberLength') ) {
342 # Is integer and length match
343 if ( $cardnumber_length =~ m|^\d+$| ) {
344 $min = $max = $cardnumber_length
345 if $cardnumber_length >= $min
346 and $cardnumber_length <= $max;
348 # Else assuming it is a range
349 elsif ( $cardnumber_length =~ m|(\d*),(\d*)| ) {
350 $min = $1 if $1 and $min < $1;
351 $max = $2 if $2 and $max > $2;
355 $min = $max if $min > $max;
356 return ( $min, $max );
359 =head2 GetBorrowersToExpunge
361 $borrowers = &GetBorrowersToExpunge(
362 not_borrowed_since => $not_borrowed_since,
363 expired_before => $expired_before,
364 category_code => $category_code,
365 patron_list_id => $patron_list_id,
366 branchcode => $branchcode
369 This function get all borrowers based on the given criteria.
371 =cut
373 sub GetBorrowersToExpunge {
375 my $params = shift;
376 my $filterdate = $params->{'not_borrowed_since'};
377 my $filterexpiry = $params->{'expired_before'};
378 my $filterlastseen = $params->{'last_seen'};
379 my $filtercategory = $params->{'category_code'};
380 my $filterbranch = $params->{'branchcode'} ||
381 ((C4::Context->preference('IndependentBranches')
382 && C4::Context->userenv
383 && !C4::Context->IsSuperLibrarian()
384 && C4::Context->userenv->{branch})
385 ? C4::Context->userenv->{branch}
386 : "");
387 my $filterpatronlist = $params->{'patron_list_id'};
389 my $dbh = C4::Context->dbh;
390 my $query = q|
391 SELECT *
392 FROM (
393 SELECT borrowers.borrowernumber,
394 MAX(old_issues.timestamp) AS latestissue,
395 MAX(issues.timestamp) AS currentissue
396 FROM borrowers
397 JOIN categories USING (categorycode)
398 LEFT JOIN (
399 SELECT guarantor_id
400 FROM borrower_relationships
401 WHERE guarantor_id IS NOT NULL
402 AND guarantor_id <> 0
403 ) as tmp ON borrowers.borrowernumber=tmp.guarantor_id
404 LEFT JOIN old_issues USING (borrowernumber)
405 LEFT JOIN issues USING (borrowernumber)|;
406 if ( $filterpatronlist ){
407 $query .= q| LEFT JOIN patron_list_patrons USING (borrowernumber)|;
409 $query .= q| WHERE category_type <> 'S'
410 AND tmp.guarantor_id IS NULL
412 my @query_params;
413 if ( $filterbranch && $filterbranch ne "" ) {
414 $query.= " AND borrowers.branchcode = ? ";
415 push( @query_params, $filterbranch );
417 if ( $filterexpiry ) {
418 $query .= " AND dateexpiry < ? ";
419 push( @query_params, $filterexpiry );
421 if ( $filterlastseen ) {
422 $query .= ' AND lastseen < ? ';
423 push @query_params, $filterlastseen;
425 if ( $filtercategory ) {
426 $query .= " AND categorycode = ? ";
427 push( @query_params, $filtercategory );
429 if ( $filterpatronlist ){
430 $query.=" AND patron_list_id = ? ";
431 push( @query_params, $filterpatronlist );
433 $query .= " GROUP BY borrowers.borrowernumber";
434 $query .= q|
435 ) xxx WHERE currentissue IS NULL|;
436 if ( $filterdate ) {
437 $query.=" AND ( latestissue < ? OR latestissue IS NULL ) ";
438 push @query_params,$filterdate;
441 warn $query if $debug;
443 my $sth = $dbh->prepare($query);
444 if (scalar(@query_params)>0){
445 $sth->execute(@query_params);
447 else {
448 $sth->execute;
451 my @results;
452 while ( my $data = $sth->fetchrow_hashref ) {
453 push @results, $data;
455 return \@results;
458 =head2 IssueSlip
460 IssueSlip($branchcode, $borrowernumber, $quickslip)
462 Returns letter hash ( see C4::Letters::GetPreparedLetter )
464 $quickslip is boolean, to indicate whether we want a quick slip
466 IssueSlip populates ISSUESLIP and ISSUEQSLIP, and will make the following expansions:
468 Both slips:
470 <<branches.*>>
471 <<borrowers.*>>
473 ISSUESLIP:
475 <checkedout>
476 <<biblio.*>>
477 <<items.*>>
478 <<biblioitems.*>>
479 <<issues.*>>
480 </checkedout>
482 <overdue>
483 <<biblio.*>>
484 <<items.*>>
485 <<biblioitems.*>>
486 <<issues.*>>
487 </overdue>
489 <news>
490 <<opac_news.*>>
491 </news>
493 ISSUEQSLIP:
495 <checkedout>
496 <<biblio.*>>
497 <<items.*>>
498 <<biblioitems.*>>
499 <<issues.*>>
500 </checkedout>
502 NOTE: Fields from tables issues, items, biblio and biblioitems are available
504 =cut
506 sub IssueSlip {
507 my ($branch, $borrowernumber, $quickslip) = @_;
509 # FIXME Check callers before removing this statement
510 #return unless $borrowernumber;
512 my $patron = Koha::Patrons->find( $borrowernumber );
513 return unless $patron;
515 my $pending_checkouts = $patron->pending_checkouts; # Should be $patron->checkouts->pending?
517 my ($letter_code, %repeat, %loops);
518 if ( $quickslip ) {
519 my $today_start = dt_from_string->set( hour => 0, minute => 0, second => 0 );
520 my $today_end = dt_from_string->set( hour => 23, minute => 59, second => 0 );
521 $today_start = Koha::Database->new->schema->storage->datetime_parser->format_datetime( $today_start );
522 $today_end = Koha::Database->new->schema->storage->datetime_parser->format_datetime( $today_end );
523 $letter_code = 'ISSUEQSLIP';
525 # issue date or lastreneweddate is today
526 my $todays_checkouts = $pending_checkouts->search(
528 -or => {
529 issuedate => {
530 '>=' => $today_start,
531 '<=' => $today_end,
533 lastreneweddate =>
534 { '>=' => $today_start, '<=' => $today_end, }
538 my @checkouts;
539 while ( my $c = $todays_checkouts->next ) {
540 my $all = $c->unblessed_all_relateds;
541 push @checkouts, {
542 biblio => $all,
543 items => $all,
544 biblioitems => $all,
545 issues => $all,
549 %repeat = (
550 checkedout => \@checkouts, # Historical syntax
552 %loops = (
553 issues => [ map { $_->{issues}{itemnumber} } @checkouts ], # TT syntax
556 else {
557 my $today = Koha::Database->new->schema->storage->datetime_parser->format_datetime( dt_from_string );
558 # Checkouts due in the future
559 my $checkouts = $pending_checkouts->search({ date_due => { '>' => $today } });
560 my @checkouts; my @overdues;
561 while ( my $c = $checkouts->next ) {
562 my $all = $c->unblessed_all_relateds;
563 push @checkouts, {
564 biblio => $all,
565 items => $all,
566 biblioitems => $all,
567 issues => $all,
571 # Checkouts due in the past are overdues
572 my $overdues = $pending_checkouts->search({ date_due => { '<=' => $today } });
573 while ( my $o = $overdues->next ) {
574 my $all = $o->unblessed_all_relateds;
575 push @overdues, {
576 biblio => $all,
577 items => $all,
578 biblioitems => $all,
579 issues => $all,
582 my $news = GetNewsToDisplay( "slip", $branch );
583 my @news = map {
584 $_->{'timestamp'} = $_->{'newdate'};
585 { opac_news => $_ }
586 } @$news;
587 $letter_code = 'ISSUESLIP';
588 %repeat = (
589 checkedout => \@checkouts,
590 overdue => \@overdues,
591 news => \@news,
593 %loops = (
594 issues => [ map { $_->{issues}{itemnumber} } @checkouts ],
595 overdues => [ map { $_->{issues}{itemnumber} } @overdues ],
596 opac_news => [ map { $_->{opac_news}{idnew} } @news ],
600 return C4::Letters::GetPreparedLetter (
601 module => 'circulation',
602 letter_code => $letter_code,
603 branchcode => $branch,
604 lang => $patron->lang,
605 tables => {
606 'branches' => $branch,
607 'borrowers' => $borrowernumber,
609 repeat => \%repeat,
610 loops => \%loops,
614 =head2 DeleteExpiredOpacRegistrations
616 Delete accounts that haven't been upgraded from the 'temporary' category
617 Returns the number of removed patrons
619 =cut
621 sub DeleteExpiredOpacRegistrations {
623 my $delay = C4::Context->preference('PatronSelfRegistrationExpireTemporaryAccountsDelay');
624 my $category_code = C4::Context->preference('PatronSelfRegistrationDefaultCategory');
626 return 0 if not $category_code or not defined $delay or $delay eq q||;
627 my $date_enrolled = dt_from_string();
628 $date_enrolled->subtract( days => $delay );
630 my $registrations_to_del = Koha::Patrons->search({
631 dateenrolled => {'<=' => $date_enrolled->ymd},
632 categorycode => $category_code,
635 my $cnt=0;
636 while ( my $registration = $registrations_to_del->next() ) {
637 next if $registration->checkouts->count || $registration->account->balance;
638 $registration->delete;
639 $cnt++;
641 return $cnt;
644 =head2 DeleteUnverifiedOpacRegistrations
646 Delete all unverified self registrations in borrower_modifications,
647 older than the specified number of days.
649 =cut
651 sub DeleteUnverifiedOpacRegistrations {
652 my ( $days ) = @_;
653 my $dbh = C4::Context->dbh;
654 my $sql=qq|
655 DELETE FROM borrower_modifications
656 WHERE borrowernumber = 0 AND DATEDIFF( NOW(), timestamp ) > ?|;
657 my $cnt=$dbh->do($sql, undef, ($days) );
658 return $cnt eq '0E0'? 0: $cnt;
661 END { } # module clean-up code here (global destructor)
665 __END__
667 =head1 AUTHOR
669 Koha Team
671 =cut