Bug 23584: Add spec for PUT /public/patron/:patron_id/guarantors
[koha.git] / C4 / Members.pm
blob6f2cb3756253c0ae0d23875ccc21aac9a02f9d33
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 Koha::AuthUtils qw(hash_password);
43 use Koha::Database;
44 use Koha::Holds;
45 use Koha::List::Patron;
46 use Koha::Patrons;
47 use Koha::Patron::Categories;
49 our (@ISA,@EXPORT,@EXPORT_OK,$debug);
51 BEGIN {
52 $debug = $ENV{DEBUG} || 0;
53 require Exporter;
54 @ISA = qw(Exporter);
55 #Get data
56 push @EXPORT, qw(
58 &GetAllIssues
60 &GetBorrowersToExpunge
62 &IssueSlip
65 #Check data
66 push @EXPORT, qw(
67 &checkuserpassword
68 &checkcardnumber
72 =head1 NAME
74 C4::Members - Perl Module containing convenience functions for member handling
76 =head1 SYNOPSIS
78 use C4::Members;
80 =head1 DESCRIPTION
82 This module contains routines for adding, modifying and deleting members/patrons/borrowers
84 =head1 FUNCTIONS
86 =head2 patronflags
88 $flags = &patronflags($patron);
90 This function is not exported.
92 The following will be set where applicable:
93 $flags->{CHARGES}->{amount} Amount of debt
94 $flags->{CHARGES}->{noissues} Set if debt amount >$5.00 (or syspref noissuescharge)
95 $flags->{CHARGES}->{message} Message -- deprecated
97 $flags->{CREDITS}->{amount} Amount of credit
98 $flags->{CREDITS}->{message} Message -- deprecated
100 $flags->{ GNA } Patron has no valid address
101 $flags->{ GNA }->{noissues} Set for each GNA
102 $flags->{ GNA }->{message} "Borrower has no valid address" -- deprecated
104 $flags->{ LOST } Patron's card reported lost
105 $flags->{ LOST }->{noissues} Set for each LOST
106 $flags->{ LOST }->{message} Message -- deprecated
108 $flags->{DBARRED} Set if patron debarred, no access
109 $flags->{DBARRED}->{noissues} Set for each DBARRED
110 $flags->{DBARRED}->{message} Message -- deprecated
112 $flags->{ NOTES }
113 $flags->{ NOTES }->{message} The note itself. NOT deprecated
115 $flags->{ ODUES } Set if patron has overdue books.
116 $flags->{ ODUES }->{message} "Yes" -- deprecated
117 $flags->{ ODUES }->{itemlist} ref-to-array: list of overdue books
118 $flags->{ ODUES }->{itemlisttext} Text list of overdue items -- deprecated
120 $flags->{WAITING} Set if any of patron's reserves are available
121 $flags->{WAITING}->{message} Message -- deprecated
122 $flags->{WAITING}->{itemlist} ref-to-array: list of available items
124 =over
126 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlist}> is a reference-to-array listing the
127 overdue items. Its elements are references-to-hash, each describing an
128 overdue item. The keys are selected fields from the issues, biblio,
129 biblioitems, and items tables of the Koha database.
131 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlisttext}> is a string giving a text listing of
132 the overdue items, one per line. Deprecated.
134 =item C<$flags-E<gt>{WAITING}-E<gt>{itemlist}> is a reference-to-array listing the
135 available items. Each element is a reference-to-hash whose keys are
136 fields from the reserves table of the Koha database.
138 =back
140 All the "message" fields that include language generated in this function are deprecated,
141 because such strings belong properly in the display layer.
143 The "message" field that comes from the DB is OK.
145 =cut
147 # TODO: use {anonymous => hashes} instead of a dozen %flaginfo
148 # FIXME rename this function.
149 # DEPRECATED Do not use this subroutine!
150 sub patronflags {
151 my %flags;
152 my ( $patroninformation) = @_;
153 my $dbh=C4::Context->dbh;
154 my $patron = Koha::Patrons->find( $patroninformation->{borrowernumber} );
155 my $account = $patron->account;
156 my $owing = $account->non_issues_charges;
157 if ( $owing > 0 ) {
158 my %flaginfo;
159 my $noissuescharge = C4::Context->preference("noissuescharge") || 5;
160 $flaginfo{'message'} = sprintf 'Patron owes %.02f', $owing;
161 $flaginfo{'amount'} = sprintf "%.02f", $owing;
162 if ( $owing > $noissuescharge && !C4::Context->preference("AllowFineOverride") ) {
163 $flaginfo{'noissues'} = 1;
165 $flags{'CHARGES'} = \%flaginfo;
167 elsif ( ( my $balance = $account->balance ) < 0 ) {
168 my %flaginfo;
169 $flaginfo{'message'} = sprintf 'Patron has credit of %.02f', -$balance;
170 $flaginfo{'amount'} = sprintf "%.02f", $balance;
171 $flags{'CREDITS'} = \%flaginfo;
174 # Check the debt of the guarntees of this patron
175 my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
176 $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
177 if ( defined $no_issues_charge_guarantees ) {
178 my $p = Koha::Patrons->find( $patroninformation->{borrowernumber} );
179 my @guarantees = map { $_->guarantee } $p->guarantee_relationships;
180 my $guarantees_non_issues_charges;
181 foreach my $g ( @guarantees ) {
182 $guarantees_non_issues_charges += $g->account->non_issues_charges;
185 if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees ) {
186 my %flaginfo;
187 $flaginfo{'message'} = sprintf 'patron guarantees owe %.02f', $guarantees_non_issues_charges;
188 $flaginfo{'amount'} = $guarantees_non_issues_charges;
189 $flaginfo{'noissues'} = 1 unless C4::Context->preference("allowfineoverride");
190 $flags{'CHARGES_GUARANTEES'} = \%flaginfo;
194 if ( $patroninformation->{'gonenoaddress'}
195 && $patroninformation->{'gonenoaddress'} == 1 )
197 my %flaginfo;
198 $flaginfo{'message'} = 'Borrower has no valid address.';
199 $flaginfo{'noissues'} = 1;
200 $flags{'GNA'} = \%flaginfo;
202 if ( $patroninformation->{'lost'} && $patroninformation->{'lost'} == 1 ) {
203 my %flaginfo;
204 $flaginfo{'message'} = 'Borrower\'s card reported lost.';
205 $flaginfo{'noissues'} = 1;
206 $flags{'LOST'} = \%flaginfo;
208 if ( $patroninformation->{'debarred'} && check_date( split( /-/, $patroninformation->{'debarred'} ) ) ) {
209 if ( Date_to_Days(Date::Calc::Today) < Date_to_Days( split( /-/, $patroninformation->{'debarred'} ) ) ) {
210 my %flaginfo;
211 $flaginfo{'debarredcomment'} = $patroninformation->{'debarredcomment'};
212 $flaginfo{'message'} = $patroninformation->{'debarredcomment'};
213 $flaginfo{'noissues'} = 1;
214 $flaginfo{'dateend'} = $patroninformation->{'debarred'};
215 $flags{'DBARRED'} = \%flaginfo;
218 if ( $patroninformation->{'borrowernotes'}
219 && $patroninformation->{'borrowernotes'} )
221 my %flaginfo;
222 $flaginfo{'message'} = $patroninformation->{'borrowernotes'};
223 $flags{'NOTES'} = \%flaginfo;
225 my ( $odues, $itemsoverdue ) = C4::Overdues::checkoverdues($patroninformation->{'borrowernumber'});
226 if ( $odues && $odues > 0 ) {
227 my %flaginfo;
228 $flaginfo{'message'} = "Yes";
229 $flaginfo{'itemlist'} = $itemsoverdue;
230 foreach ( sort { $a->{'date_due'} cmp $b->{'date_due'} }
231 @$itemsoverdue )
233 $flaginfo{'itemlisttext'} .=
234 "$_->{'date_due'} $_->{'barcode'} $_->{'title'} \n"; # newline is display layer
236 $flags{'ODUES'} = \%flaginfo;
239 my $waiting_holds = $patron->holds->search({ found => 'W' });
240 my $nowaiting = $waiting_holds->count;
241 if ( $nowaiting > 0 ) {
242 my %flaginfo;
243 $flaginfo{'message'} = "Reserved items available";
244 $flaginfo{'itemlist'} = $waiting_holds->unblessed;
245 $flags{'WAITING'} = \%flaginfo;
247 return ( \%flags );
250 =head2 GetAllIssues
252 $issues = &GetAllIssues($borrowernumber, $sortkey, $limit);
254 Looks up what the patron with the given borrowernumber has borrowed,
255 and sorts the results.
257 C<$sortkey> is the name of a field on which to sort the results. This
258 should be the name of a field in the C<issues>, C<biblio>,
259 C<biblioitems>, or C<items> table in the Koha database.
261 C<$limit> is the maximum number of results to return.
263 C<&GetAllIssues> an arrayref, C<$issues>, of hashrefs, the keys of which
264 are the fields from the C<issues>, C<biblio>, C<biblioitems>, and
265 C<items> tables of the Koha database.
267 =cut
270 sub GetAllIssues {
271 my ( $borrowernumber, $order, $limit ) = @_;
273 return unless $borrowernumber;
274 $order = 'date_due desc' unless $order;
276 my $dbh = C4::Context->dbh;
277 my $query =
278 'SELECT *, issues.timestamp as issuestimestamp, issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
279 FROM issues
280 LEFT JOIN items on items.itemnumber=issues.itemnumber
281 LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
282 LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
283 WHERE borrowernumber=?
284 UNION ALL
285 SELECT *, old_issues.timestamp as issuestimestamp, old_issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
286 FROM old_issues
287 LEFT JOIN items on items.itemnumber=old_issues.itemnumber
288 LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
289 LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
290 WHERE borrowernumber=? AND old_issues.itemnumber IS NOT NULL
291 order by ' . $order;
292 if ($limit) {
293 $query .= " limit $limit";
296 my $sth = $dbh->prepare($query);
297 $sth->execute( $borrowernumber, $borrowernumber );
298 return $sth->fetchall_arrayref( {} );
301 sub checkcardnumber {
302 my ( $cardnumber, $borrowernumber ) = @_;
304 # If cardnumber is null, we assume they're allowed.
305 return 0 unless defined $cardnumber;
307 my $dbh = C4::Context->dbh;
308 my $query = "SELECT * FROM borrowers WHERE cardnumber=?";
309 $query .= " AND borrowernumber <> ?" if ($borrowernumber);
310 my $sth = $dbh->prepare($query);
311 $sth->execute(
312 $cardnumber,
313 ( $borrowernumber ? $borrowernumber : () )
316 return 1 if $sth->fetchrow_hashref;
318 my ( $min_length, $max_length ) = get_cardnumber_length();
319 return 2
320 if length $cardnumber > $max_length
321 or length $cardnumber < $min_length;
323 return 0;
326 =head2 get_cardnumber_length
328 my ($min, $max) = C4::Members::get_cardnumber_length()
330 Returns the minimum and maximum length for patron cardnumbers as
331 determined by the CardnumberLength system preference, the
332 BorrowerMandatoryField system preference, and the width of the
333 database column.
335 =cut
337 sub get_cardnumber_length {
338 my $borrower = Koha::Database->new->schema->resultset('Borrower');
339 my $field_size = $borrower->result_source->column_info('cardnumber')->{size};
340 my ( $min, $max ) = ( 0, $field_size ); # borrowers.cardnumber is a nullable varchar(20)
341 $min = 1 if C4::Context->preference('BorrowerMandatoryField') =~ /cardnumber/;
342 if ( my $cardnumber_length = C4::Context->preference('CardnumberLength') ) {
343 # Is integer and length match
344 if ( $cardnumber_length =~ m|^\d+$| ) {
345 $min = $max = $cardnumber_length
346 if $cardnumber_length >= $min
347 and $cardnumber_length <= $max;
349 # Else assuming it is a range
350 elsif ( $cardnumber_length =~ m|(\d*),(\d*)| ) {
351 $min = $1 if $1 and $min < $1;
352 $max = $2 if $2 and $max > $2;
356 $min = $max if $min > $max;
357 return ( $min, $max );
360 =head2 GetBorrowersToExpunge
362 $borrowers = &GetBorrowersToExpunge(
363 not_borrowed_since => $not_borrowed_since,
364 expired_before => $expired_before,
365 category_code => $category_code,
366 patron_list_id => $patron_list_id,
367 branchcode => $branchcode
370 This function get all borrowers based on the given criteria.
372 =cut
374 sub GetBorrowersToExpunge {
376 my $params = shift;
377 my $filterdate = $params->{'not_borrowed_since'};
378 my $filterexpiry = $params->{'expired_before'};
379 my $filterlastseen = $params->{'last_seen'};
380 my $filtercategory = $params->{'category_code'};
381 my $filterbranch = $params->{'branchcode'} ||
382 ((C4::Context->preference('IndependentBranches')
383 && C4::Context->userenv
384 && !C4::Context->IsSuperLibrarian()
385 && C4::Context->userenv->{branch})
386 ? C4::Context->userenv->{branch}
387 : "");
388 my $filterpatronlist = $params->{'patron_list_id'};
390 my $dbh = C4::Context->dbh;
391 my $query = q|
392 SELECT *
393 FROM (
394 SELECT borrowers.borrowernumber,
395 MAX(old_issues.timestamp) AS latestissue,
396 MAX(issues.timestamp) AS currentissue
397 FROM borrowers
398 JOIN categories USING (categorycode)
399 LEFT JOIN (
400 SELECT guarantor_id
401 FROM borrower_relationships
402 WHERE guarantor_id IS NOT NULL
403 AND guarantor_id <> 0
404 ) as tmp ON borrowers.borrowernumber=tmp.guarantor_id
405 LEFT JOIN old_issues USING (borrowernumber)
406 LEFT JOIN issues USING (borrowernumber)|;
407 if ( $filterpatronlist ){
408 $query .= q| LEFT JOIN patron_list_patrons USING (borrowernumber)|;
410 $query .= q| WHERE category_type <> 'S'
411 AND tmp.guarantor_id IS NULL
413 my @query_params;
414 if ( $filterbranch && $filterbranch ne "" ) {
415 $query.= " AND borrowers.branchcode = ? ";
416 push( @query_params, $filterbranch );
418 if ( $filterexpiry ) {
419 $query .= " AND dateexpiry < ? ";
420 push( @query_params, $filterexpiry );
422 if ( $filterlastseen ) {
423 $query .= ' AND lastseen < ? ';
424 push @query_params, $filterlastseen;
426 if ( $filtercategory ) {
427 $query .= " AND categorycode = ? ";
428 push( @query_params, $filtercategory );
430 if ( $filterpatronlist ){
431 $query.=" AND patron_list_id = ? ";
432 push( @query_params, $filterpatronlist );
434 $query .= " GROUP BY borrowers.borrowernumber";
435 $query .= q|
436 ) xxx WHERE currentissue IS NULL|;
437 if ( $filterdate ) {
438 $query.=" AND ( latestissue < ? OR latestissue IS NULL ) ";
439 push @query_params,$filterdate;
442 warn $query if $debug;
444 my $sth = $dbh->prepare($query);
445 if (scalar(@query_params)>0){
446 $sth->execute(@query_params);
448 else {
449 $sth->execute;
452 my @results;
453 while ( my $data = $sth->fetchrow_hashref ) {
454 push @results, $data;
456 return \@results;
459 =head2 IssueSlip
461 IssueSlip($branchcode, $borrowernumber, $quickslip)
463 Returns letter hash ( see C4::Letters::GetPreparedLetter )
465 $quickslip is boolean, to indicate whether we want a quick slip
467 IssueSlip populates ISSUESLIP and ISSUEQSLIP, and will make the following expansions:
469 Both slips:
471 <<branches.*>>
472 <<borrowers.*>>
474 ISSUESLIP:
476 <checkedout>
477 <<biblio.*>>
478 <<items.*>>
479 <<biblioitems.*>>
480 <<issues.*>>
481 </checkedout>
483 <overdue>
484 <<biblio.*>>
485 <<items.*>>
486 <<biblioitems.*>>
487 <<issues.*>>
488 </overdue>
490 <news>
491 <<opac_news.*>>
492 </news>
494 ISSUEQSLIP:
496 <checkedout>
497 <<biblio.*>>
498 <<items.*>>
499 <<biblioitems.*>>
500 <<issues.*>>
501 </checkedout>
503 NOTE: Fields from tables issues, items, biblio and biblioitems are available
505 =cut
507 sub IssueSlip {
508 my ($branch, $borrowernumber, $quickslip) = @_;
510 # FIXME Check callers before removing this statement
511 #return unless $borrowernumber;
513 my $patron = Koha::Patrons->find( $borrowernumber );
514 return unless $patron;
516 my $pending_checkouts = $patron->pending_checkouts; # Should be $patron->checkouts->pending?
518 my ($letter_code, %repeat, %loops);
519 if ( $quickslip ) {
520 my $today_start = dt_from_string->set( hour => 0, minute => 0, second => 0 );
521 my $today_end = dt_from_string->set( hour => 23, minute => 59, second => 0 );
522 $today_start = Koha::Database->new->schema->storage->datetime_parser->format_datetime( $today_start );
523 $today_end = Koha::Database->new->schema->storage->datetime_parser->format_datetime( $today_end );
524 $letter_code = 'ISSUEQSLIP';
526 # issue date or lastreneweddate is today
527 my $todays_checkouts = $pending_checkouts->search(
529 -or => {
530 issuedate => {
531 '>=' => $today_start,
532 '<=' => $today_end,
534 lastreneweddate =>
535 { '>=' => $today_start, '<=' => $today_end, }
539 my @checkouts;
540 while ( my $c = $todays_checkouts->next ) {
541 my $all = $c->unblessed_all_relateds;
542 push @checkouts, {
543 biblio => $all,
544 items => $all,
545 biblioitems => $all,
546 issues => $all,
550 %repeat = (
551 checkedout => \@checkouts, # Historical syntax
553 %loops = (
554 issues => [ map { $_->{issues}{itemnumber} } @checkouts ], # TT syntax
557 else {
558 my $today = Koha::Database->new->schema->storage->datetime_parser->format_datetime( dt_from_string );
559 # Checkouts due in the future
560 my $checkouts = $pending_checkouts->search({ date_due => { '>' => $today } });
561 my @checkouts; my @overdues;
562 while ( my $c = $checkouts->next ) {
563 my $all = $c->unblessed_all_relateds;
564 push @checkouts, {
565 biblio => $all,
566 items => $all,
567 biblioitems => $all,
568 issues => $all,
572 # Checkouts due in the past are overdues
573 my $overdues = $pending_checkouts->search({ date_due => { '<=' => $today } });
574 while ( my $o = $overdues->next ) {
575 my $all = $o->unblessed_all_relateds;
576 push @overdues, {
577 biblio => $all,
578 items => $all,
579 biblioitems => $all,
580 issues => $all,
583 my $news = GetNewsToDisplay( "slip", $branch );
584 my @news = map {
585 $_->{'timestamp'} = $_->{'newdate'};
586 { opac_news => $_ }
587 } @$news;
588 $letter_code = 'ISSUESLIP';
589 %repeat = (
590 checkedout => \@checkouts,
591 overdue => \@overdues,
592 news => \@news,
594 %loops = (
595 issues => [ map { $_->{issues}{itemnumber} } @checkouts ],
596 overdues => [ map { $_->{issues}{itemnumber} } @overdues ],
597 opac_news => [ map { $_->{opac_news}{idnew} } @news ],
601 return C4::Letters::GetPreparedLetter (
602 module => 'circulation',
603 letter_code => $letter_code,
604 branchcode => $branch,
605 lang => $patron->lang,
606 tables => {
607 'branches' => $branch,
608 'borrowers' => $borrowernumber,
610 repeat => \%repeat,
611 loops => \%loops,
615 =head2 DeleteExpiredOpacRegistrations
617 Delete accounts that haven't been upgraded from the 'temporary' category
618 Returns the number of removed patrons
620 =cut
622 sub DeleteExpiredOpacRegistrations {
624 my $delay = C4::Context->preference('PatronSelfRegistrationExpireTemporaryAccountsDelay');
625 my $category_code = C4::Context->preference('PatronSelfRegistrationDefaultCategory');
627 return 0 if not $category_code or not defined $delay or $delay eq q||;
628 my $date_enrolled = dt_from_string();
629 $date_enrolled->subtract( days => $delay );
631 my $registrations_to_del = Koha::Patrons->search({
632 dateenrolled => {'<=' => $date_enrolled->ymd},
633 categorycode => $category_code,
636 my $cnt=0;
637 while ( my $registration = $registrations_to_del->next() ) {
638 next if $registration->checkouts->count || $registration->account->balance;
639 $registration->delete;
640 $cnt++;
642 return $cnt;
645 =head2 DeleteUnverifiedOpacRegistrations
647 Delete all unverified self registrations in borrower_modifications,
648 older than the specified number of days.
650 =cut
652 sub DeleteUnverifiedOpacRegistrations {
653 my ( $days ) = @_;
654 my $dbh = C4::Context->dbh;
655 my $sql=qq|
656 DELETE FROM borrower_modifications
657 WHERE borrowernumber = 0 AND DATEDIFF( NOW(), timestamp ) > ?|;
658 my $cnt=$dbh->do($sql, undef, ($days) );
659 return $cnt eq '0E0'? 0: $cnt;
662 END { } # module clean-up code here (global destructor)
666 __END__
668 =head1 AUTHOR
670 Koha Team
672 =cut