Bug 27889: Adjust responsive width of OPAC advanced search form
[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 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::NewsChannels; #get slip news
37 use DateTime;
38 use Koha::Database;
39 use Koha::DateUtils;
40 use Koha::AuthUtils qw(hash_password);
41 use Koha::Database;
42 use Koha::Holds;
43 use Koha::List::Patron;
44 use Koha::Patrons;
45 use Koha::Patron::Categories;
46
47 our (@ISA,@EXPORT,@EXPORT_OK,$debug);
48
49 BEGIN {
50     $debug = $ENV{DEBUG} || 0;
51     require Exporter;
52     @ISA = qw(Exporter);
53     #Get data
54     push @EXPORT, qw(
55
56         &GetAllIssues
57
58         &GetBorrowersToExpunge
59
60         &IssueSlip
61     );
62
63     #Check data
64     push @EXPORT, qw(
65         &checkuserpassword
66         &checkcardnumber
67     );
68 }
69
70 =head1 NAME
71
72 C4::Members - Perl Module containing convenience functions for member handling
73
74 =head1 SYNOPSIS
75
76 use C4::Members;
77
78 =head1 DESCRIPTION
79
80 This module contains routines for adding, modifying and deleting members/patrons/borrowers
81
82 =head1 FUNCTIONS
83
84 =head2 patronflags
85
86  $flags = &patronflags($patron);
87
88 This function is not exported.
89
90 The following will be set where applicable:
91  $flags->{CHARGES}->{amount}        Amount of debt
92  $flags->{CHARGES}->{noissues}      Set if debt amount >$5.00 (or syspref noissuescharge)
93  $flags->{CHARGES}->{message}       Message -- deprecated
94
95  $flags->{CREDITS}->{amount}        Amount of credit
96  $flags->{CREDITS}->{message}       Message -- deprecated
97
98  $flags->{  GNA  }                  Patron has no valid address
99  $flags->{  GNA  }->{noissues}      Set for each GNA
100  $flags->{  GNA  }->{message}       "Borrower has no valid address" -- deprecated
101
102  $flags->{ LOST  }                  Patron's card reported lost
103  $flags->{ LOST  }->{noissues}      Set for each LOST
104  $flags->{ LOST  }->{message}       Message -- deprecated
105
106  $flags->{DBARRED}                  Set if patron debarred, no access
107  $flags->{DBARRED}->{noissues}      Set for each DBARRED
108  $flags->{DBARRED}->{message}       Message -- deprecated
109
110  $flags->{ NOTES }
111  $flags->{ NOTES }->{message}       The note itself.  NOT deprecated
112
113  $flags->{ ODUES }                  Set if patron has overdue books.
114  $flags->{ ODUES }->{message}       "Yes"  -- deprecated
115  $flags->{ ODUES }->{itemlist}      ref-to-array: list of overdue books
116  $flags->{ ODUES }->{itemlisttext}  Text list of overdue items -- deprecated
117
118  $flags->{WAITING}                  Set if any of patron's reserves are available
119  $flags->{WAITING}->{message}       Message -- deprecated
120  $flags->{WAITING}->{itemlist}      ref-to-array: list of available items
121
122 =over
123
124 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlist}> is a reference-to-array listing the
125 overdue items. Its elements are references-to-hash, each describing an
126 overdue item. The keys are selected fields from the issues, biblio,
127 biblioitems, and items tables of the Koha database.
128
129 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlisttext}> is a string giving a text listing of
130 the overdue items, one per line.  Deprecated.
131
132 =item C<$flags-E<gt>{WAITING}-E<gt>{itemlist}> is a reference-to-array listing the
133 available items. Each element is a reference-to-hash whose keys are
134 fields from the reserves table of the Koha database.
135
136 =back
137
138 All the "message" fields that include language generated in this function are deprecated,
139 because such strings belong properly in the display layer.
140
141 The "message" field that comes from the DB is OK.
142
143 =cut
144
145 # TODO: use {anonymous => hashes} instead of a dozen %flaginfo
146 # FIXME rename this function.
147 # DEPRECATED Do not use this subroutine!
148 sub patronflags {
149     my %flags;
150     my ( $patroninformation) = @_;
151     my $dbh=C4::Context->dbh;
152     my $patron = Koha::Patrons->find( $patroninformation->{borrowernumber} );
153     my $account = $patron->account;
154     my $owing = $account->non_issues_charges;
155     if ( $owing > 0 ) {
156         my %flaginfo;
157         my $noissuescharge = C4::Context->preference("noissuescharge") || 5;
158         $flaginfo{'message'} = sprintf 'Patron owes %.02f', $owing;
159         $flaginfo{'amount'}  = sprintf "%.02f", $owing;
160         if ( $owing > $noissuescharge && !C4::Context->preference("AllowFineOverride") ) {
161             $flaginfo{'noissues'} = 1;
162         }
163         $flags{'CHARGES'} = \%flaginfo;
164     }
165     elsif ( ( my $balance = $account->balance ) < 0 ) {
166         my %flaginfo;
167         $flaginfo{'message'} = sprintf 'Patron has credit of %.02f', -$balance;
168         $flaginfo{'amount'}  = sprintf "%.02f", $balance;
169         $flags{'CREDITS'} = \%flaginfo;
170     }
171
172     # Check the debt of the guarntees of this patron
173     my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
174     $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
175     if ( defined $no_issues_charge_guarantees ) {
176         my $p = Koha::Patrons->find( $patroninformation->{borrowernumber} );
177         my @guarantees = map { $_->guarantee } $p->guarantee_relationships;
178         my $guarantees_non_issues_charges = 0;
179         foreach my $g ( @guarantees ) {
180             $guarantees_non_issues_charges += $g->account->non_issues_charges;
181         }
182
183         if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees ) {
184             my %flaginfo;
185             $flaginfo{'message'} = sprintf 'patron guarantees owe %.02f', $guarantees_non_issues_charges;
186             $flaginfo{'amount'}  = $guarantees_non_issues_charges;
187             $flaginfo{'noissues'} = 1 unless C4::Context->preference("allowfineoverride");
188             $flags{'CHARGES_GUARANTEES'} = \%flaginfo;
189         }
190     }
191
192     if (   $patroninformation->{'gonenoaddress'}
193         && $patroninformation->{'gonenoaddress'} == 1 )
194     {
195         my %flaginfo;
196         $flaginfo{'message'}  = 'Borrower has no valid address.';
197         $flaginfo{'noissues'} = 1;
198         $flags{'GNA'}         = \%flaginfo;
199     }
200     if ( $patroninformation->{'lost'} && $patroninformation->{'lost'} == 1 ) {
201         my %flaginfo;
202         $flaginfo{'message'}  = 'Borrower\'s card reported lost.';
203         $flaginfo{'noissues'} = 1;
204         $flags{'LOST'}        = \%flaginfo;
205     }
206     if ( $patroninformation->{'debarred'} && check_date( split( /-/, $patroninformation->{'debarred'} ) ) ) {
207         if ( Date_to_Days(Date::Calc::Today) < Date_to_Days( split( /-/, $patroninformation->{'debarred'} ) ) ) {
208             my %flaginfo;
209             $flaginfo{'debarredcomment'} = $patroninformation->{'debarredcomment'};
210             $flaginfo{'message'}         = $patroninformation->{'debarredcomment'};
211             $flaginfo{'noissues'}        = 1;
212             $flaginfo{'dateend'}         = $patroninformation->{'debarred'};
213             $flags{'DBARRED'}           = \%flaginfo;
214         }
215     }
216     if (   $patroninformation->{'borrowernotes'}
217         && $patroninformation->{'borrowernotes'} )
218     {
219         my %flaginfo;
220         $flaginfo{'message'} = $patroninformation->{'borrowernotes'};
221         $flags{'NOTES'}      = \%flaginfo;
222     }
223     my ( $odues, $itemsoverdue ) = C4::Overdues::checkoverdues($patroninformation->{'borrowernumber'});
224     if ( $odues && $odues > 0 ) {
225         my %flaginfo;
226         $flaginfo{'message'}  = "Yes";
227         $flaginfo{'itemlist'} = $itemsoverdue;
228         foreach ( sort { $a->{'date_due'} cmp $b->{'date_due'} }
229             @$itemsoverdue )
230         {
231             $flaginfo{'itemlisttext'} .=
232               "$_->{'date_due'} $_->{'barcode'} $_->{'title'} \n";  # newline is display layer
233         }
234         $flags{'ODUES'} = \%flaginfo;
235     }
236
237     my $waiting_holds = $patron->holds->search({ found => 'W' });
238     my $nowaiting = $waiting_holds->count;
239     if ( $nowaiting > 0 ) {
240         my %flaginfo;
241         $flaginfo{'message'}  = "Reserved items available";
242         $flaginfo{'itemlist'} = $waiting_holds->unblessed;
243         $flags{'WAITING'}     = \%flaginfo;
244     }
245     return ( \%flags );
246 }
247
248 =head2 GetAllIssues
249
250   $issues = &GetAllIssues($borrowernumber, $sortkey, $limit);
251
252 Looks up what the patron with the given borrowernumber has borrowed,
253 and sorts the results.
254
255 C<$sortkey> is the name of a field on which to sort the results. This
256 should be the name of a field in the C<issues>, C<biblio>,
257 C<biblioitems>, or C<items> table in the Koha database.
258
259 C<$limit> is the maximum number of results to return.
260
261 C<&GetAllIssues> an arrayref, C<$issues>, of hashrefs, the keys of which
262 are the fields from the C<issues>, C<biblio>, C<biblioitems>, and
263 C<items> tables of the Koha database.
264
265 =cut
266
267 #'
268 sub GetAllIssues {
269     my ( $borrowernumber, $order, $limit ) = @_;
270
271     return unless $borrowernumber;
272     $order = 'date_due desc' unless $order;
273
274     my $dbh = C4::Context->dbh;
275     my $query =
276 'SELECT issues.*, items.*, biblio.*, biblioitems.*, issues.timestamp as issuestimestamp, issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp,borrowers.firstname,borrowers.surname
277   FROM issues
278   LEFT JOIN items on items.itemnumber=issues.itemnumber
279   LEFT JOIN borrowers on borrowers.borrowernumber=issues.issuer_id
280   LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
281   LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
282   WHERE issues.borrowernumber=?
283   UNION ALL
284   SELECT old_issues.*, items.*, biblio.*, biblioitems.*, old_issues.timestamp as issuestimestamp, old_issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp,borrowers.firstname,borrowers.surname
285   FROM old_issues
286   LEFT JOIN items on items.itemnumber=old_issues.itemnumber
287   LEFT JOIN borrowers on borrowers.borrowernumber=old_issues.issuer_id
288   LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
289   LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
290   WHERE old_issues.borrowernumber=? AND old_issues.itemnumber IS NOT NULL
291   order by ' . $order;
292     if ($limit) {
293         $query .= " limit $limit";
294     }
295
296     my $sth = $dbh->prepare($query);
297     $sth->execute( $borrowernumber, $borrowernumber );
298     return $sth->fetchall_arrayref( {} );
299 }
300
301 sub checkcardnumber {
302     my ( $cardnumber, $borrowernumber ) = @_;
303
304     # If cardnumber is null, we assume they're allowed.
305     return 0 unless defined $cardnumber;
306
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 : () )
314     );
315
316     return 1 if $sth->fetchrow_hashref;
317
318     my ( $min_length, $max_length ) = get_cardnumber_length();
319     return 2
320         if length $cardnumber > $max_length
321         or length $cardnumber < $min_length;
322
323     return 0;
324 }
325
326 =head2 get_cardnumber_length
327
328     my ($min, $max) = C4::Members::get_cardnumber_length()
329
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.
334
335 =cut
336
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;
348         }
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;
353         }
354
355     }
356     $min = $max if $min > $max;
357     return ( $min, $max );
358 }
359
360 =head2 GetBorrowersToExpunge
361
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
368   );
369
370   This function get all borrowers based on the given criteria.
371
372 =cut
373
374 sub GetBorrowersToExpunge {
375
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'};
389
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)|;
409     }
410     $query .= q| WHERE  category_type <> 'S'
411         AND ( borrowers.flags IS NULL OR borrowers.flags = 0 )
412         AND tmp.guarantor_id IS NULL
413     |;
414     my @query_params;
415     if ( $filterbranch && $filterbranch ne "" ) {
416         $query.= " AND borrowers.branchcode = ? ";
417         push( @query_params, $filterbranch );
418     }
419     if ( $filterexpiry ) {
420         $query .= " AND dateexpiry < ? ";
421         push( @query_params, $filterexpiry );
422     }
423     if ( $filterlastseen ) {
424         $query .= ' AND lastseen < ? ';
425         push @query_params, $filterlastseen;
426     }
427     if ( $filtercategory ) {
428         if (ref($filtercategory) ne 'ARRAY' ) {
429             $filtercategory = [ $filtercategory ];
430         }
431         $query .= " AND categorycode IN (" . join(',', ('?') x @$filtercategory) . ") ";
432         push( @query_params, @$filtercategory );
433     }
434     if ( $filterpatronlist ){
435         $query.=" AND patron_list_id = ? ";
436         push( @query_params, $filterpatronlist );
437     }
438     $query .= " GROUP BY borrowers.borrowernumber";
439     $query .= q|
440         ) xxx WHERE currentissue IS NULL|;
441     if ( $filterdate ) {
442         $query.=" AND ( latestissue < ? OR latestissue IS NULL ) ";
443         push @query_params,$filterdate;
444     }
445
446     if ( my $anonymous_patron = C4::Context->preference("AnonymousPatron") ) {
447         $query .= q{ AND borrowernumber != ? };
448         push( @query_params, $anonymous_patron );
449     }
450
451     warn $query if $debug;
452
453     my $sth = $dbh->prepare($query);
454     if (scalar(@query_params)>0){
455         $sth->execute(@query_params);
456     }
457     else {
458         $sth->execute;
459     }
460
461     my @results;
462     while ( my $data = $sth->fetchrow_hashref ) {
463         push @results, $data;
464     }
465     return \@results;
466 }
467
468 =head2 IssueSlip
469
470   IssueSlip($branchcode, $borrowernumber, $quickslip)
471
472   Returns letter hash ( see C4::Letters::GetPreparedLetter )
473
474   $quickslip is boolean, to indicate whether we want a quick slip
475
476   IssueSlip populates ISSUESLIP and ISSUEQSLIP, and will make the following expansions:
477
478   Both slips:
479
480       <<branches.*>>
481       <<borrowers.*>>
482
483   ISSUESLIP:
484
485       <checkedout>
486          <<biblio.*>>
487          <<items.*>>
488          <<biblioitems.*>>
489          <<issues.*>>
490       </checkedout>
491
492       <overdue>
493          <<biblio.*>>
494          <<items.*>>
495          <<biblioitems.*>>
496          <<issues.*>>
497       </overdue>
498
499       <news>
500          <<opac_news.*>>
501       </news>
502
503   ISSUEQSLIP:
504
505       <checkedout>
506          <<biblio.*>>
507          <<items.*>>
508          <<biblioitems.*>>
509          <<issues.*>>
510       </checkedout>
511
512   NOTE: Fields from tables issues, items, biblio and biblioitems are available
513
514 =cut
515
516 sub IssueSlip {
517     my ($branch, $borrowernumber, $quickslip) = @_;
518
519     # FIXME Check callers before removing this statement
520     #return unless $borrowernumber;
521
522     my $patron = Koha::Patrons->find( $borrowernumber );
523     return unless $patron;
524
525     my $pending_checkouts = $patron->pending_checkouts; # Should be $patron->checkouts->pending?
526
527     my ($letter_code, %repeat, %loops);
528     if ( $quickslip ) {
529         my $today_start = dt_from_string->set( hour => 0, minute => 0, second => 0 );
530         my $today_end = dt_from_string->set( hour => 23, minute => 59, second => 0 );
531         $today_start = Koha::Database->new->schema->storage->datetime_parser->format_datetime( $today_start );
532         $today_end = Koha::Database->new->schema->storage->datetime_parser->format_datetime( $today_end );
533         $letter_code = 'ISSUEQSLIP';
534
535         # issue date or lastreneweddate is today
536         my $todays_checkouts = $pending_checkouts->search(
537             {
538                 -or => {
539                     issuedate => {
540                         '>=' => $today_start,
541                         '<=' => $today_end,
542                     },
543                     lastreneweddate =>
544                       { '>=' => $today_start, '<=' => $today_end, }
545                 }
546             }
547         );
548         my @checkouts;
549         while ( my $c = $todays_checkouts->next ) {
550             my $all = $c->unblessed_all_relateds;
551             push @checkouts, {
552                 biblio      => $all,
553                 items       => $all,
554                 biblioitems => $all,
555                 issues      => $all,
556             };
557         }
558
559         %repeat =  (
560             checkedout => \@checkouts, # Historical syntax
561         );
562         %loops = (
563             issues => [ map { $_->{issues}{itemnumber} } @checkouts ], # TT syntax
564         );
565     }
566     else {
567         my $today = Koha::Database->new->schema->storage->datetime_parser->format_datetime( dt_from_string );
568         # Checkouts due in the future
569         my $checkouts = $pending_checkouts->search({ date_due => { '>' => $today } });
570         my @checkouts; my @overdues;
571         while ( my $c = $checkouts->next ) {
572             my $all = $c->unblessed_all_relateds;
573             push @checkouts, {
574                 biblio      => $all,
575                 items       => $all,
576                 biblioitems => $all,
577                 issues      => $all,
578             };
579         }
580
581         # Checkouts due in the past are overdues
582         my $overdues = $pending_checkouts->search({ date_due => { '<=' => $today } });
583         while ( my $o = $overdues->next ) {
584             my $all = $o->unblessed_all_relateds;
585             push @overdues, {
586                 biblio      => $all,
587                 items       => $all,
588                 biblioitems => $all,
589                 issues      => $all,
590             };
591         }
592         my $news = GetNewsToDisplay( "slip", $branch );
593         my @news = map {
594             $_->{'timestamp'} = $_->{'newdate'};
595             { opac_news => $_ }
596         } @$news;
597         $letter_code = 'ISSUESLIP';
598         %repeat      = (
599             checkedout => \@checkouts,
600             overdue    => \@overdues,
601             news       => \@news,
602         );
603         %loops = (
604             issues => [ map { $_->{issues}{itemnumber} } @checkouts ],
605             overdues   => [ map { $_->{issues}{itemnumber} } @overdues ],
606             opac_news => [ map { $_->{opac_news}{idnew} } @news ],
607         );
608     }
609
610     return  C4::Letters::GetPreparedLetter (
611         module => 'circulation',
612         letter_code => $letter_code,
613         branchcode => $branch,
614         lang => $patron->lang,
615         tables => {
616             'branches'    => $branch,
617             'borrowers'   => $borrowernumber,
618         },
619         repeat => \%repeat,
620         loops => \%loops,
621     );
622 }
623
624 =head2 DeleteExpiredOpacRegistrations
625
626     Delete accounts that haven't been upgraded from the 'temporary' category
627     Returns the number of removed patrons
628
629 =cut
630
631 sub DeleteExpiredOpacRegistrations {
632
633     my $delay = C4::Context->preference('PatronSelfRegistrationExpireTemporaryAccountsDelay');
634     my $category_code = C4::Context->preference('PatronSelfRegistrationDefaultCategory');
635
636     return 0 if not $category_code or not defined $delay or $delay eq q||;
637     my $date_enrolled = dt_from_string();
638     $date_enrolled->subtract( days => $delay );
639
640     my $registrations_to_del = Koha::Patrons->search({
641         dateenrolled => {'<=' => $date_enrolled->ymd},
642         categorycode => $category_code,
643     });
644
645     my $cnt=0;
646     while ( my $registration = $registrations_to_del->next() ) {
647         next if $registration->checkouts->count || $registration->account->balance;
648         $registration->delete;
649         $cnt++;
650     }
651     return $cnt;
652 }
653
654 =head2 DeleteUnverifiedOpacRegistrations
655
656     Delete all unverified self registrations in borrower_modifications,
657     older than the specified number of days.
658
659 =cut
660
661 sub DeleteUnverifiedOpacRegistrations {
662     my ( $days ) = @_;
663     my $dbh = C4::Context->dbh;
664     my $sql=qq|
665 DELETE FROM borrower_modifications
666 WHERE borrowernumber = 0 AND DATEDIFF( NOW(), timestamp ) > ?|;
667     my $cnt=$dbh->do($sql, undef, ($days) );
668     return $cnt eq '0E0'? 0: $cnt;
669 }
670
671 END { }    # module clean-up code here (global destructor)
672
673 1;
674
675 __END__
676
677 =head1 AUTHOR
678
679 Koha Team
680
681 =cut