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>.
24 #use warnings; FIXME - Bug 2505
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 );
31 use C4
::Log
; # logaction
37 use C4
::Members
::Attributes
qw(SearchIdMatchingAttribute UpdateBorrowerAttribute);
38 use C4
::NewsChannels
; #get slip news
42 use Text
::Unaccent
qw( unac_string );
43 use Koha
::AuthUtils
qw(hash_password);
46 use Koha
::List
::Patron
;
48 use Koha
::Patron
::Categories
;
51 our (@ISA,@EXPORT,@EXPORT_OK,$debug);
54 $debug = $ENV{DEBUG
} || 0;
62 &GetBorrowersToExpunge
81 C4::Members - Perl Module containing convenience functions for member handling
89 This module contains routines for adding, modifying and deleting members/patrons/borrowers
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
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
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.
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.
154 # TODO: use {anonymous => hashes} instead of a dozen %flaginfo
155 # FIXME rename this function.
156 # DEPRECATED Do not use this subroutine!
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;
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 ) {
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 ) {
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 )
205 $flaginfo{'message'} = 'Borrower has no valid address.';
206 $flaginfo{'noissues'} = 1;
207 $flags{'GNA'} = \
%flaginfo;
209 if ( $patroninformation->{'lost'} && $patroninformation->{'lost'} == 1 ) {
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'} ) ) ) {
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'} )
229 $flaginfo{'message'} = $patroninformation->{'borrowernotes'};
230 $flags{'NOTES'} = \
%flaginfo;
232 my ( $odues, $itemsoverdue ) = C4
::Overdues
::checkoverdues
($patroninformation->{'borrowernumber'});
233 if ( $odues && $odues > 0 ) {
235 $flaginfo{'message'} = "Yes";
236 $flaginfo{'itemlist'} = $itemsoverdue;
237 foreach ( sort { $a->{'date_due'} cmp $b->{'date_due'} }
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 ) {
250 $flaginfo{'message'} = "Reserved items available";
251 $flaginfo{'itemlist'} = $waiting_holds->unblessed;
252 $flags{'WAITING'} = \
%flaginfo;
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.
278 my ( $borrowernumber, $order, $limit ) = @_;
280 return unless $borrowernumber;
281 $order = 'date_due desc' unless $order;
283 my $dbh = C4
::Context
->dbh;
285 'SELECT *, issues.timestamp as issuestimestamp, issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
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=?
292 SELECT *, old_issues.timestamp as issuestimestamp, old_issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
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
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);
320 ( $borrowernumber ?
$borrowernumber : () )
323 return 1 if $sth->fetchrow_hashref;
325 my ( $min_length, $max_length ) = get_cardnumber_length
();
327 if length $cardnumber > $max_length
328 or length $cardnumber < $min_length;
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
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.
381 sub GetBorrowersToExpunge
{
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
}
395 my $filterpatronlist = $params->{'patron_list_id'};
397 my $dbh = C4
::Context
->dbh;
401 SELECT borrowers
.borrowernumber
,
402 MAX
(old_issues
.timestamp
) AS latestissue
,
403 MAX
(issues
.timestamp
) AS currentissue
405 JOIN categories USING
(categorycode
)
409 WHERE guarantorid IS NOT NULL
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
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";
443 ) xxx WHERE currentissue IS NULL
|;
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);
460 while ( my $data = $sth->fetchrow_hashref ) {
461 push @results, $data;
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:
510 NOTE: Fields from tables issues, items, biblio and biblioitems are available
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);
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(
538 '>=' => $today_start,
542 { '>=' => $today_start, '<=' => $today_end, }
547 while ( my $c = $todays_checkouts->next ) {
548 my $all = $c->unblessed_all_relateds;
558 checkedout
=> \
@checkouts, # Historical syntax
561 issues
=> [ map { $_->{issues
}{itemnumber
} } @checkouts ], # TT syntax
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;
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;
590 my $news = GetNewsToDisplay
( "slip", $branch );
592 $_->{'timestamp'} = $_->{'newdate'};
595 $letter_code = 'ISSUESLIP';
597 checkedout
=> \
@checkouts,
598 overdue
=> \
@overdues,
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,
614 'branches' => $branch,
615 'borrowers' => $borrowernumber,
622 =head2 DeleteExpiredOpacRegistrations
624 Delete accounts that haven't been upgraded from the 'temporary' category
625 Returns the number of removed patrons
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
||;
637 SELECT borrowernumber
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 );
645 while ( my ($borrowernumber) = $sth->fetchrow_array() ) {
646 Koha
::Patrons
->find($borrowernumber)->delete;
652 =head2 DeleteUnverifiedOpacRegistrations
654 Delete all unverified self registrations in borrower_modifications,
655 older than the specified number of days.
659 sub DeleteUnverifiedOpacRegistrations
{
661 my $dbh = C4
::Context
->dbh;
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)