Bug 20944: Add route to add credits to a patron's account
[koha.git] / C4 / Members.pm
blob0cd424b3955a6721ab9bd24591f55df8645ed750
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 strict;
24 #use warnings; FIXME - Bug 2505
25 use C4::Context;
26 use String::Random qw( random_string );
27 use Scalar::Util qw( looks_like_number );
28 use Date::Calc qw/Today check_date Date_to_Days/;
29 use List::MoreUtils qw( uniq );
30 use JSON qw(to_json);
31 use C4::Log; # logaction
32 use C4::Overdues;
33 use C4::Reserves;
34 use C4::Accounts;
35 use C4::Biblio;
36 use C4::Letters;
37 use C4::Members::Attributes qw(SearchIdMatchingAttribute UpdateBorrowerAttribute);
38 use C4::NewsChannels; #get slip news
39 use DateTime;
40 use Koha::Database;
41 use Koha::DateUtils;
42 use Text::Unaccent qw( unac_string );
43 use Koha::AuthUtils qw(hash_password);
44 use Koha::Database;
45 use Koha::Holds;
46 use Koha::List::Patron;
47 use Koha::Patrons;
48 use Koha::Patron::Categories;
49 use Koha::Schema;
51 our (@ISA,@EXPORT,@EXPORT_OK,$debug);
53 BEGIN {
54 $debug = $ENV{DEBUG} || 0;
55 require Exporter;
56 @ISA = qw(Exporter);
57 #Get data
58 push @EXPORT, qw(
60 &GetAllIssues
62 &GetBorrowersToExpunge
64 &IssueSlip
67 #Modify data
68 push @EXPORT, qw(
69 &changepassword
72 #Check data
73 push @EXPORT, qw(
74 &checkuserpassword
75 &checkcardnumber
79 =head1 NAME
81 C4::Members - Perl Module containing convenience functions for member handling
83 =head1 SYNOPSIS
85 use C4::Members;
87 =head1 DESCRIPTION
89 This module contains routines for adding, modifying and deleting members/patrons/borrowers
91 =head1 FUNCTIONS
93 =head2 patronflags
95 $flags = &patronflags($patron);
97 This function is not exported.
99 The following will be set where applicable:
100 $flags->{CHARGES}->{amount} Amount of debt
101 $flags->{CHARGES}->{noissues} Set if debt amount >$5.00 (or syspref noissuescharge)
102 $flags->{CHARGES}->{message} Message -- deprecated
104 $flags->{CREDITS}->{amount} Amount of credit
105 $flags->{CREDITS}->{message} Message -- deprecated
107 $flags->{ GNA } Patron has no valid address
108 $flags->{ GNA }->{noissues} Set for each GNA
109 $flags->{ GNA }->{message} "Borrower has no valid address" -- deprecated
111 $flags->{ LOST } Patron's card reported lost
112 $flags->{ LOST }->{noissues} Set for each LOST
113 $flags->{ LOST }->{message} Message -- deprecated
115 $flags->{DBARRED} Set if patron debarred, no access
116 $flags->{DBARRED}->{noissues} Set for each DBARRED
117 $flags->{DBARRED}->{message} Message -- deprecated
119 $flags->{ NOTES }
120 $flags->{ NOTES }->{message} The note itself. NOT deprecated
122 $flags->{ ODUES } Set if patron has overdue books.
123 $flags->{ ODUES }->{message} "Yes" -- deprecated
124 $flags->{ ODUES }->{itemlist} ref-to-array: list of overdue books
125 $flags->{ ODUES }->{itemlisttext} Text list of overdue items -- deprecated
127 $flags->{WAITING} Set if any of patron's reserves are available
128 $flags->{WAITING}->{message} Message -- deprecated
129 $flags->{WAITING}->{itemlist} ref-to-array: list of available items
131 =over
133 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlist}> is a reference-to-array listing the
134 overdue items. Its elements are references-to-hash, each describing an
135 overdue item. The keys are selected fields from the issues, biblio,
136 biblioitems, and items tables of the Koha database.
138 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlisttext}> is a string giving a text listing of
139 the overdue items, one per line. Deprecated.
141 =item C<$flags-E<gt>{WAITING}-E<gt>{itemlist}> is a reference-to-array listing the
142 available items. Each element is a reference-to-hash whose keys are
143 fields from the reserves table of the Koha database.
145 =back
147 All the "message" fields that include language generated in this function are deprecated,
148 because such strings belong properly in the display layer.
150 The "message" field that comes from the DB is OK.
152 =cut
154 # TODO: use {anonymous => hashes} instead of a dozen %flaginfo
155 # FIXME rename this function.
156 # DEPRECATED Do not use this subroutine!
157 sub patronflags {
158 my %flags;
159 my ( $patroninformation) = @_;
160 my $dbh=C4::Context->dbh;
161 my $patron = Koha::Patrons->find( $patroninformation->{borrowernumber} );
162 my $account = $patron->account;
163 my $owing = $account->non_issues_charges;
164 if ( $owing > 0 ) {
165 my %flaginfo;
166 my $noissuescharge = C4::Context->preference("noissuescharge") || 5;
167 $flaginfo{'message'} = sprintf 'Patron owes %.02f', $owing;
168 $flaginfo{'amount'} = sprintf "%.02f", $owing;
169 if ( $owing > $noissuescharge && !C4::Context->preference("AllowFineOverride") ) {
170 $flaginfo{'noissues'} = 1;
172 $flags{'CHARGES'} = \%flaginfo;
174 elsif ( ( my $balance = $account->balance ) < 0 ) {
175 my %flaginfo;
176 $flaginfo{'message'} = sprintf 'Patron has credit of %.02f', -$balance;
177 $flaginfo{'amount'} = sprintf "%.02f", $balance;
178 $flags{'CREDITS'} = \%flaginfo;
181 # Check the debt of the guarntees of this patron
182 my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
183 $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
184 if ( defined $no_issues_charge_guarantees ) {
185 my $p = Koha::Patrons->find( $patroninformation->{borrowernumber} );
186 my @guarantees = $p->guarantees();
187 my $guarantees_non_issues_charges;
188 foreach my $g ( @guarantees ) {
189 $guarantees_non_issues_charges += $g->account->non_issues_charges;
192 if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees ) {
193 my %flaginfo;
194 $flaginfo{'message'} = sprintf 'patron guarantees owe %.02f', $guarantees_non_issues_charges;
195 $flaginfo{'amount'} = $guarantees_non_issues_charges;
196 $flaginfo{'noissues'} = 1 unless C4::Context->preference("allowfineoverride");
197 $flags{'CHARGES_GUARANTEES'} = \%flaginfo;
201 if ( $patroninformation->{'gonenoaddress'}
202 && $patroninformation->{'gonenoaddress'} == 1 )
204 my %flaginfo;
205 $flaginfo{'message'} = 'Borrower has no valid address.';
206 $flaginfo{'noissues'} = 1;
207 $flags{'GNA'} = \%flaginfo;
209 if ( $patroninformation->{'lost'} && $patroninformation->{'lost'} == 1 ) {
210 my %flaginfo;
211 $flaginfo{'message'} = 'Borrower\'s card reported lost.';
212 $flaginfo{'noissues'} = 1;
213 $flags{'LOST'} = \%flaginfo;
215 if ( $patroninformation->{'debarred'} && check_date( split( /-/, $patroninformation->{'debarred'} ) ) ) {
216 if ( Date_to_Days(Date::Calc::Today) < Date_to_Days( split( /-/, $patroninformation->{'debarred'} ) ) ) {
217 my %flaginfo;
218 $flaginfo{'debarredcomment'} = $patroninformation->{'debarredcomment'};
219 $flaginfo{'message'} = $patroninformation->{'debarredcomment'};
220 $flaginfo{'noissues'} = 1;
221 $flaginfo{'dateend'} = $patroninformation->{'debarred'};
222 $flags{'DBARRED'} = \%flaginfo;
225 if ( $patroninformation->{'borrowernotes'}
226 && $patroninformation->{'borrowernotes'} )
228 my %flaginfo;
229 $flaginfo{'message'} = $patroninformation->{'borrowernotes'};
230 $flags{'NOTES'} = \%flaginfo;
232 my ( $odues, $itemsoverdue ) = C4::Overdues::checkoverdues($patroninformation->{'borrowernumber'});
233 if ( $odues && $odues > 0 ) {
234 my %flaginfo;
235 $flaginfo{'message'} = "Yes";
236 $flaginfo{'itemlist'} = $itemsoverdue;
237 foreach ( sort { $a->{'date_due'} cmp $b->{'date_due'} }
238 @$itemsoverdue )
240 $flaginfo{'itemlisttext'} .=
241 "$_->{'date_due'} $_->{'barcode'} $_->{'title'} \n"; # newline is display layer
243 $flags{'ODUES'} = \%flaginfo;
246 my $waiting_holds = $patron->holds->search({ found => 'W' });
247 my $nowaiting = $waiting_holds->count;
248 if ( $nowaiting > 0 ) {
249 my %flaginfo;
250 $flaginfo{'message'} = "Reserved items available";
251 $flaginfo{'itemlist'} = $waiting_holds->unblessed;
252 $flags{'WAITING'} = \%flaginfo;
254 return ( \%flags );
257 =head2 GetAllIssues
259 $issues = &GetAllIssues($borrowernumber, $sortkey, $limit);
261 Looks up what the patron with the given borrowernumber has borrowed,
262 and sorts the results.
264 C<$sortkey> is the name of a field on which to sort the results. This
265 should be the name of a field in the C<issues>, C<biblio>,
266 C<biblioitems>, or C<items> table in the Koha database.
268 C<$limit> is the maximum number of results to return.
270 C<&GetAllIssues> an arrayref, C<$issues>, of hashrefs, the keys of which
271 are the fields from the C<issues>, C<biblio>, C<biblioitems>, and
272 C<items> tables of the Koha database.
274 =cut
277 sub GetAllIssues {
278 my ( $borrowernumber, $order, $limit ) = @_;
280 return unless $borrowernumber;
281 $order = 'date_due desc' unless $order;
283 my $dbh = C4::Context->dbh;
284 my $query =
285 'SELECT *, issues.timestamp as issuestimestamp, issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
286 FROM issues
287 LEFT JOIN items on items.itemnumber=issues.itemnumber
288 LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
289 LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
290 WHERE borrowernumber=?
291 UNION ALL
292 SELECT *, old_issues.timestamp as issuestimestamp, old_issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
293 FROM old_issues
294 LEFT JOIN items on items.itemnumber=old_issues.itemnumber
295 LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
296 LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
297 WHERE borrowernumber=? AND old_issues.itemnumber IS NOT NULL
298 order by ' . $order;
299 if ($limit) {
300 $query .= " limit $limit";
303 my $sth = $dbh->prepare($query);
304 $sth->execute( $borrowernumber, $borrowernumber );
305 return $sth->fetchall_arrayref( {} );
308 sub checkcardnumber {
309 my ( $cardnumber, $borrowernumber ) = @_;
311 # If cardnumber is null, we assume they're allowed.
312 return 0 unless defined $cardnumber;
314 my $dbh = C4::Context->dbh;
315 my $query = "SELECT * FROM borrowers WHERE cardnumber=?";
316 $query .= " AND borrowernumber <> ?" if ($borrowernumber);
317 my $sth = $dbh->prepare($query);
318 $sth->execute(
319 $cardnumber,
320 ( $borrowernumber ? $borrowernumber : () )
323 return 1 if $sth->fetchrow_hashref;
325 my ( $min_length, $max_length ) = get_cardnumber_length();
326 return 2
327 if length $cardnumber > $max_length
328 or length $cardnumber < $min_length;
330 return 0;
333 =head2 get_cardnumber_length
335 my ($min, $max) = C4::Members::get_cardnumber_length()
337 Returns the minimum and maximum length for patron cardnumbers as
338 determined by the CardnumberLength system preference, the
339 BorrowerMandatoryField system preference, and the width of the
340 database column.
342 =cut
344 sub get_cardnumber_length {
345 my $borrower = Koha::Schema->resultset('Borrower');
346 my $field_size = $borrower->result_source->column_info('cardnumber')->{size};
347 my ( $min, $max ) = ( 0, $field_size ); # borrowers.cardnumber is a nullable varchar(20)
348 $min = 1 if C4::Context->preference('BorrowerMandatoryField') =~ /cardnumber/;
349 if ( my $cardnumber_length = C4::Context->preference('CardnumberLength') ) {
350 # Is integer and length match
351 if ( $cardnumber_length =~ m|^\d+$| ) {
352 $min = $max = $cardnumber_length
353 if $cardnumber_length >= $min
354 and $cardnumber_length <= $max;
356 # Else assuming it is a range
357 elsif ( $cardnumber_length =~ m|(\d*),(\d*)| ) {
358 $min = $1 if $1 and $min < $1;
359 $max = $2 if $2 and $max > $2;
363 $min = $max if $min > $max;
364 return ( $min, $max );
367 =head2 GetBorrowersToExpunge
369 $borrowers = &GetBorrowersToExpunge(
370 not_borrowed_since => $not_borrowed_since,
371 expired_before => $expired_before,
372 category_code => $category_code,
373 patron_list_id => $patron_list_id,
374 branchcode => $branchcode
377 This function get all borrowers based on the given criteria.
379 =cut
381 sub GetBorrowersToExpunge {
383 my $params = shift;
384 my $filterdate = $params->{'not_borrowed_since'};
385 my $filterexpiry = $params->{'expired_before'};
386 my $filterlastseen = $params->{'last_seen'};
387 my $filtercategory = $params->{'category_code'};
388 my $filterbranch = $params->{'branchcode'} ||
389 ((C4::Context->preference('IndependentBranches')
390 && C4::Context->userenv
391 && !C4::Context->IsSuperLibrarian()
392 && C4::Context->userenv->{branch})
393 ? C4::Context->userenv->{branch}
394 : "");
395 my $filterpatronlist = $params->{'patron_list_id'};
397 my $dbh = C4::Context->dbh;
398 my $query = q|
399 SELECT *
400 FROM (
401 SELECT borrowers.borrowernumber,
402 MAX(old_issues.timestamp) AS latestissue,
403 MAX(issues.timestamp) AS currentissue
404 FROM borrowers
405 JOIN categories USING (categorycode)
406 LEFT JOIN (
407 SELECT guarantorid
408 FROM borrowers
409 WHERE guarantorid IS NOT NULL
410 AND guarantorid <> 0
411 ) as tmp ON borrowers.borrowernumber=tmp.guarantorid
412 LEFT JOIN old_issues USING (borrowernumber)
413 LEFT JOIN issues USING (borrowernumber)|;
414 if ( $filterpatronlist ){
415 $query .= q| LEFT JOIN patron_list_patrons USING (borrowernumber)|;
417 $query .= q| WHERE category_type <> 'S'
418 AND tmp.guarantorid IS NULL
420 my @query_params;
421 if ( $filterbranch && $filterbranch ne "" ) {
422 $query.= " AND borrowers.branchcode = ? ";
423 push( @query_params, $filterbranch );
425 if ( $filterexpiry ) {
426 $query .= " AND dateexpiry < ? ";
427 push( @query_params, $filterexpiry );
429 if ( $filterlastseen ) {
430 $query .= ' AND lastseen < ? ';
431 push @query_params, $filterlastseen;
433 if ( $filtercategory ) {
434 $query .= " AND categorycode = ? ";
435 push( @query_params, $filtercategory );
437 if ( $filterpatronlist ){
438 $query.=" AND patron_list_id = ? ";
439 push( @query_params, $filterpatronlist );
441 $query .= " GROUP BY borrowers.borrowernumber";
442 $query .= q|
443 ) xxx WHERE currentissue IS NULL|;
444 if ( $filterdate ) {
445 $query.=" AND ( latestissue < ? OR latestissue IS NULL ) ";
446 push @query_params,$filterdate;
449 warn $query if $debug;
451 my $sth = $dbh->prepare($query);
452 if (scalar(@query_params)>0){
453 $sth->execute(@query_params);
455 else {
456 $sth->execute;
459 my @results;
460 while ( my $data = $sth->fetchrow_hashref ) {
461 push @results, $data;
463 return \@results;
466 =head2 IssueSlip
468 IssueSlip($branchcode, $borrowernumber, $quickslip)
470 Returns letter hash ( see C4::Letters::GetPreparedLetter )
472 $quickslip is boolean, to indicate whether we want a quick slip
474 IssueSlip populates ISSUESLIP and ISSUEQSLIP, and will make the following expansions:
476 Both slips:
478 <<branches.*>>
479 <<borrowers.*>>
481 ISSUESLIP:
483 <checkedout>
484 <<biblio.*>>
485 <<items.*>>
486 <<biblioitems.*>>
487 <<issues.*>>
488 </checkedout>
490 <overdue>
491 <<biblio.*>>
492 <<items.*>>
493 <<biblioitems.*>>
494 <<issues.*>>
495 </overdue>
497 <news>
498 <<opac_news.*>>
499 </news>
501 ISSUEQSLIP:
503 <checkedout>
504 <<biblio.*>>
505 <<items.*>>
506 <<biblioitems.*>>
507 <<issues.*>>
508 </checkedout>
510 NOTE: Fields from tables issues, items, biblio and biblioitems are available
512 =cut
514 sub IssueSlip {
515 my ($branch, $borrowernumber, $quickslip) = @_;
517 # FIXME Check callers before removing this statement
518 #return unless $borrowernumber;
520 my $patron = Koha::Patrons->find( $borrowernumber );
521 return unless $patron;
523 my $pending_checkouts = $patron->pending_checkouts; # Should be $patron->checkouts->pending?
525 my ($letter_code, %repeat, %loops);
526 if ( $quickslip ) {
527 my $today_start = dt_from_string->set( hour => 0, minute => 0, second => 0 );
528 my $today_end = dt_from_string->set( hour => 23, minute => 59, second => 0 );
529 $today_start = Koha::Database->new->schema->storage->datetime_parser->format_datetime( $today_start );
530 $today_end = Koha::Database->new->schema->storage->datetime_parser->format_datetime( $today_end );
531 $letter_code = 'ISSUEQSLIP';
533 # issue date or lastreneweddate is today
534 my $todays_checkouts = $pending_checkouts->search(
536 -or => {
537 issuedate => {
538 '>=' => $today_start,
539 '<=' => $today_end,
541 lastreneweddate =>
542 { '>=' => $today_start, '<=' => $today_end, }
546 my @checkouts;
547 while ( my $c = $todays_checkouts->next ) {
548 my $all = $c->unblessed_all_relateds;
549 push @checkouts, {
550 biblio => $all,
551 items => $all,
552 biblioitems => $all,
553 issues => $all,
557 %repeat = (
558 checkedout => \@checkouts, # Historical syntax
560 %loops = (
561 issues => [ map { $_->{issues}{itemnumber} } @checkouts ], # TT syntax
564 else {
565 my $today = Koha::Database->new->schema->storage->datetime_parser->format_datetime( dt_from_string );
566 # Checkouts due in the future
567 my $checkouts = $pending_checkouts->search({ date_due => { '>' => $today } });
568 my @checkouts; my @overdues;
569 while ( my $c = $checkouts->next ) {
570 my $all = $c->unblessed_all_relateds;
571 push @checkouts, {
572 biblio => $all,
573 items => $all,
574 biblioitems => $all,
575 issues => $all,
579 # Checkouts due in the past are overdues
580 my $overdues = $pending_checkouts->search({ date_due => { '<=' => $today } });
581 while ( my $o = $overdues->next ) {
582 my $all = $o->unblessed_all_relateds;
583 push @overdues, {
584 biblio => $all,
585 items => $all,
586 biblioitems => $all,
587 issues => $all,
590 my $news = GetNewsToDisplay( "slip", $branch );
591 my @news = map {
592 $_->{'timestamp'} = $_->{'newdate'};
593 { opac_news => $_ }
594 } @$news;
595 $letter_code = 'ISSUESLIP';
596 %repeat = (
597 checkedout => \@checkouts,
598 overdue => \@overdues,
599 news => \@news,
601 %loops = (
602 issues => [ map { $_->{issues}{itemnumber} } @checkouts ],
603 overdues => [ map { $_->{issues}{itemnumber} } @overdues ],
604 opac_news => [ map { $_->{opac_news}{idnew} } @news ],
608 return C4::Letters::GetPreparedLetter (
609 module => 'circulation',
610 letter_code => $letter_code,
611 branchcode => $branch,
612 lang => $patron->lang,
613 tables => {
614 'branches' => $branch,
615 'borrowers' => $borrowernumber,
617 repeat => \%repeat,
618 loops => \%loops,
622 =head2 DeleteExpiredOpacRegistrations
624 Delete accounts that haven't been upgraded from the 'temporary' category
625 Returns the number of removed patrons
627 =cut
629 sub DeleteExpiredOpacRegistrations {
631 my $delay = C4::Context->preference('PatronSelfRegistrationExpireTemporaryAccountsDelay');
632 my $category_code = C4::Context->preference('PatronSelfRegistrationDefaultCategory');
634 return 0 if not $category_code or not defined $delay or $delay eq q||;
636 my $query = qq|
637 SELECT borrowernumber
638 FROM borrowers
639 WHERE categorycode = ? AND DATEDIFF( NOW(), dateenrolled ) > ? |;
641 my $dbh = C4::Context->dbh;
642 my $sth = $dbh->prepare($query);
643 $sth->execute( $category_code, $delay );
644 my $cnt=0;
645 while ( my ($borrowernumber) = $sth->fetchrow_array() ) {
646 Koha::Patrons->find($borrowernumber)->delete;
647 $cnt++;
649 return $cnt;
652 =head2 DeleteUnverifiedOpacRegistrations
654 Delete all unverified self registrations in borrower_modifications,
655 older than the specified number of days.
657 =cut
659 sub DeleteUnverifiedOpacRegistrations {
660 my ( $days ) = @_;
661 my $dbh = C4::Context->dbh;
662 my $sql=qq|
663 DELETE FROM borrower_modifications
664 WHERE borrowernumber = 0 AND DATEDIFF( NOW(), timestamp ) > ?|;
665 my $cnt=$dbh->do($sql, undef, ($days) );
666 return $cnt eq '0E0'? 0: $cnt;
669 END { } # module clean-up code here (global destructor)
673 __END__
675 =head1 AUTHOR
677 Koha Team
679 =cut