Bug 20287: Replace occurrences of AddMember with Koha::Patron->new->store->borrowernumber
[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 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;
50
51 our (@ISA,@EXPORT,@EXPORT_OK,$debug);
52
53 use Module::Load::Conditional qw( can_load );
54 if ( ! can_load( modules => { 'Koha::NorwegianPatronDB' => undef } ) ) {
55    $debug && warn "Unable to load Koha::NorwegianPatronDB";
56 }
57
58
59 BEGIN {
60     $debug = $ENV{DEBUG} || 0;
61     require Exporter;
62     @ISA = qw(Exporter);
63     #Get data
64     push @EXPORT, qw(
65
66         &GetAllIssues
67
68         &GetBorrowersToExpunge
69
70         &IssueSlip
71     );
72
73     #Modify data
74     push @EXPORT, qw(
75         &ModMember
76         &changepassword
77     );
78
79     #Insert data
80     push @EXPORT, qw(
81     &AddMember_Auto
82         &AddMember_Opac
83     );
84
85     #Check data
86     push @EXPORT, qw(
87         &checkuserpassword
88         &checkcardnumber
89     );
90 }
91
92 =head1 NAME
93
94 C4::Members - Perl Module containing convenience functions for member handling
95
96 =head1 SYNOPSIS
97
98 use C4::Members;
99
100 =head1 DESCRIPTION
101
102 This module contains routines for adding, modifying and deleting members/patrons/borrowers 
103
104 =head1 FUNCTIONS
105
106 =head2 patronflags
107
108  $flags = &patronflags($patron);
109
110 This function is not exported.
111
112 The following will be set where applicable:
113  $flags->{CHARGES}->{amount}        Amount of debt
114  $flags->{CHARGES}->{noissues}      Set if debt amount >$5.00 (or syspref noissuescharge)
115  $flags->{CHARGES}->{message}       Message -- deprecated
116
117  $flags->{CREDITS}->{amount}        Amount of credit
118  $flags->{CREDITS}->{message}       Message -- deprecated
119
120  $flags->{  GNA  }                  Patron has no valid address
121  $flags->{  GNA  }->{noissues}      Set for each GNA
122  $flags->{  GNA  }->{message}       "Borrower has no valid address" -- deprecated
123
124  $flags->{ LOST  }                  Patron's card reported lost
125  $flags->{ LOST  }->{noissues}      Set for each LOST
126  $flags->{ LOST  }->{message}       Message -- deprecated
127
128  $flags->{DBARRED}                  Set if patron debarred, no access
129  $flags->{DBARRED}->{noissues}      Set for each DBARRED
130  $flags->{DBARRED}->{message}       Message -- deprecated
131
132  $flags->{ NOTES }
133  $flags->{ NOTES }->{message}       The note itself.  NOT deprecated
134
135  $flags->{ ODUES }                  Set if patron has overdue books.
136  $flags->{ ODUES }->{message}       "Yes"  -- deprecated
137  $flags->{ ODUES }->{itemlist}      ref-to-array: list of overdue books
138  $flags->{ ODUES }->{itemlisttext}  Text list of overdue items -- deprecated
139
140  $flags->{WAITING}                  Set if any of patron's reserves are available
141  $flags->{WAITING}->{message}       Message -- deprecated
142  $flags->{WAITING}->{itemlist}      ref-to-array: list of available items
143
144 =over 
145
146 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlist}> is a reference-to-array listing the
147 overdue items. Its elements are references-to-hash, each describing an
148 overdue item. The keys are selected fields from the issues, biblio,
149 biblioitems, and items tables of the Koha database.
150
151 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlisttext}> is a string giving a text listing of
152 the overdue items, one per line.  Deprecated.
153
154 =item C<$flags-E<gt>{WAITING}-E<gt>{itemlist}> is a reference-to-array listing the
155 available items. Each element is a reference-to-hash whose keys are
156 fields from the reserves table of the Koha database.
157
158 =back
159
160 All the "message" fields that include language generated in this function are deprecated, 
161 because such strings belong properly in the display layer.
162
163 The "message" field that comes from the DB is OK.
164
165 =cut
166
167 # TODO: use {anonymous => hashes} instead of a dozen %flaginfo
168 # FIXME rename this function.
169 # DEPRECATED Do not use this subroutine!
170 sub patronflags {
171     my %flags;
172     my ( $patroninformation) = @_;
173     my $dbh=C4::Context->dbh;
174     my $patron = Koha::Patrons->find( $patroninformation->{borrowernumber} );
175     my $account = $patron->account;
176     my $owing = $account->non_issues_charges;
177     if ( $owing > 0 ) {
178         my %flaginfo;
179         my $noissuescharge = C4::Context->preference("noissuescharge") || 5;
180         $flaginfo{'message'} = sprintf 'Patron owes %.02f', $owing;
181         $flaginfo{'amount'}  = sprintf "%.02f", $owing;
182         if ( $owing > $noissuescharge && !C4::Context->preference("AllowFineOverride") ) {
183             $flaginfo{'noissues'} = 1;
184         }
185         $flags{'CHARGES'} = \%flaginfo;
186     }
187     elsif ( ( my $balance = $account->balance ) < 0 ) {
188         my %flaginfo;
189         $flaginfo{'message'} = sprintf 'Patron has credit of %.02f', -$balance;
190         $flaginfo{'amount'}  = sprintf "%.02f", $balance;
191         $flags{'CREDITS'} = \%flaginfo;
192     }
193
194     # Check the debt of the guarntees of this patron
195     my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
196     $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
197     if ( defined $no_issues_charge_guarantees ) {
198         my $p = Koha::Patrons->find( $patroninformation->{borrowernumber} );
199         my @guarantees = $p->guarantees();
200         my $guarantees_non_issues_charges;
201         foreach my $g ( @guarantees ) {
202             $guarantees_non_issues_charges += $g->account->non_issues_charges;
203         }
204
205         if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees ) {
206             my %flaginfo;
207             $flaginfo{'message'} = sprintf 'patron guarantees owe %.02f', $guarantees_non_issues_charges;
208             $flaginfo{'amount'}  = $guarantees_non_issues_charges;
209             $flaginfo{'noissues'} = 1 unless C4::Context->preference("allowfineoverride");
210             $flags{'CHARGES_GUARANTEES'} = \%flaginfo;
211         }
212     }
213
214     if (   $patroninformation->{'gonenoaddress'}
215         && $patroninformation->{'gonenoaddress'} == 1 )
216     {
217         my %flaginfo;
218         $flaginfo{'message'}  = 'Borrower has no valid address.';
219         $flaginfo{'noissues'} = 1;
220         $flags{'GNA'}         = \%flaginfo;
221     }
222     if ( $patroninformation->{'lost'} && $patroninformation->{'lost'} == 1 ) {
223         my %flaginfo;
224         $flaginfo{'message'}  = 'Borrower\'s card reported lost.';
225         $flaginfo{'noissues'} = 1;
226         $flags{'LOST'}        = \%flaginfo;
227     }
228     if ( $patroninformation->{'debarred'} && check_date( split( /-/, $patroninformation->{'debarred'} ) ) ) {
229         if ( Date_to_Days(Date::Calc::Today) < Date_to_Days( split( /-/, $patroninformation->{'debarred'} ) ) ) {
230             my %flaginfo;
231             $flaginfo{'debarredcomment'} = $patroninformation->{'debarredcomment'};
232             $flaginfo{'message'}         = $patroninformation->{'debarredcomment'};
233             $flaginfo{'noissues'}        = 1;
234             $flaginfo{'dateend'}         = $patroninformation->{'debarred'};
235             $flags{'DBARRED'}           = \%flaginfo;
236         }
237     }
238     if (   $patroninformation->{'borrowernotes'}
239         && $patroninformation->{'borrowernotes'} )
240     {
241         my %flaginfo;
242         $flaginfo{'message'} = $patroninformation->{'borrowernotes'};
243         $flags{'NOTES'}      = \%flaginfo;
244     }
245     my ( $odues, $itemsoverdue ) = C4::Overdues::checkoverdues($patroninformation->{'borrowernumber'});
246     if ( $odues && $odues > 0 ) {
247         my %flaginfo;
248         $flaginfo{'message'}  = "Yes";
249         $flaginfo{'itemlist'} = $itemsoverdue;
250         foreach ( sort { $a->{'date_due'} cmp $b->{'date_due'} }
251             @$itemsoverdue )
252         {
253             $flaginfo{'itemlisttext'} .=
254               "$_->{'date_due'} $_->{'barcode'} $_->{'title'} \n";  # newline is display layer
255         }
256         $flags{'ODUES'} = \%flaginfo;
257     }
258
259     my $waiting_holds = $patron->holds->search({ found => 'W' });
260     my $nowaiting = $waiting_holds->count;
261     if ( $nowaiting > 0 ) {
262         my %flaginfo;
263         $flaginfo{'message'}  = "Reserved items available";
264         $flaginfo{'itemlist'} = $waiting_holds->unblessed;
265         $flags{'WAITING'}     = \%flaginfo;
266     }
267     return ( \%flags );
268 }
269
270
271 =head2 ModMember
272
273   my $success = ModMember(borrowernumber => $borrowernumber,
274                                             [ field => value ]... );
275
276 Modify borrower's data.  All date fields should ALREADY be in ISO format.
277
278 return :
279 true on success, or false on failure
280
281 =cut
282
283 sub ModMember {
284     my (%data) = @_;
285
286     # trim whitespace from data which has some non-whitespace in it.
287     foreach my $field_name (keys(%data)) {
288         if ( defined $data{$field_name} && $data{$field_name} =~ /\S/ ) {
289             $data{$field_name} =~ s/^\s*|\s*$//g;
290         }
291     }
292
293     # test to know if you must update or not the borrower password
294     if (exists $data{password}) {
295         if ($data{password} eq '****' or $data{password} eq '') {
296             delete $data{password};
297         } else {
298             if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
299                 # Update the hashed PIN in borrower_sync.hashed_pin, before Koha hashes it
300                 Koha::NorwegianPatronDB::NLUpdateHashedPIN( $data{'borrowernumber'}, $data{password} );
301             }
302             $data{password} = hash_password($data{password});
303         }
304     }
305
306     my $old_categorycode = Koha::Patrons->find( $data{borrowernumber} )->categorycode;
307
308     # get only the columns of a borrower
309     my $schema = Koha::Database->new()->schema;
310     my @columns = $schema->source('Borrower')->columns;
311     my $new_borrower = { map { join(' ', @columns) =~ /$_/ ? ( $_ => $data{$_} ) : () } keys(%data) };
312
313     $new_borrower->{dateofbirth}     ||= undef if exists $new_borrower->{dateofbirth};
314     $new_borrower->{dateenrolled}    ||= undef if exists $new_borrower->{dateenrolled};
315     $new_borrower->{dateexpiry}      ||= undef if exists $new_borrower->{dateexpiry};
316     $new_borrower->{debarred}        ||= undef if exists $new_borrower->{debarred};
317     $new_borrower->{sms_provider_id} ||= undef if exists $new_borrower->{sms_provider_id};
318     $new_borrower->{guarantorid}     ||= undef if exists $new_borrower->{guarantorid};
319
320     my $patron = Koha::Patrons->find( $new_borrower->{borrowernumber} );
321
322     my $borrowers_log = C4::Context->preference("BorrowersLog");
323     if ( $borrowers_log && $patron->cardnumber ne $new_borrower->{cardnumber} )
324     {
325         logaction(
326             "MEMBERS",
327             "MODIFY",
328             $data{'borrowernumber'},
329             to_json(
330                 {
331                     cardnumber_replaced => {
332                         previous_cardnumber => $patron->cardnumber,
333                         new_cardnumber      => $new_borrower->{cardnumber},
334                     }
335                 },
336                 { utf8 => 1, pretty => 1 }
337             )
338         );
339     }
340
341     delete $new_borrower->{userid} if exists $new_borrower->{userid} and not $new_borrower->{userid};
342
343     my $execute_success = $patron->store if $patron->set($new_borrower);
344
345     if ($execute_success) { # only proceed if the update was a success
346         # If the patron changes to a category with enrollment fee, we add a fee
347         if ( $data{categorycode} and $data{categorycode} ne $old_categorycode ) {
348             if ( C4::Context->preference('FeeOnChangePatronCategory') ) {
349                 $patron->add_enrolment_fee_if_needed;
350             }
351         }
352
353         # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
354         # cronjob will use for syncing with NL
355         if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
356             my $borrowersync = Koha::Database->new->schema->resultset('BorrowerSync')->find({
357                 'synctype'       => 'norwegianpatrondb',
358                 'borrowernumber' => $data{'borrowernumber'}
359             });
360             # Do not set to "edited" if syncstatus is "new". We need to sync as new before
361             # we can sync as changed. And the "new sync" will pick up all changes since
362             # the patron was created anyway.
363             if ( $borrowersync->syncstatus ne 'new' && $borrowersync->syncstatus ne 'delete' ) {
364                 $borrowersync->update( { 'syncstatus' => 'edited' } );
365             }
366             # Set the value of 'sync'
367             $borrowersync->update( { 'sync' => $data{'sync'} } );
368             # Try to do the live sync
369             Koha::NorwegianPatronDB::NLSync({ 'borrowernumber' => $data{'borrowernumber'} });
370         }
371
372         logaction("MEMBERS", "MODIFY", $data{'borrowernumber'}, "UPDATE (executed w/ arg: $data{'borrowernumber'})") if $borrowers_log;
373     }
374     return $execute_success;
375 }
376
377 =head2 GetAllIssues
378
379   $issues = &GetAllIssues($borrowernumber, $sortkey, $limit);
380
381 Looks up what the patron with the given borrowernumber has borrowed,
382 and sorts the results.
383
384 C<$sortkey> is the name of a field on which to sort the results. This
385 should be the name of a field in the C<issues>, C<biblio>,
386 C<biblioitems>, or C<items> table in the Koha database.
387
388 C<$limit> is the maximum number of results to return.
389
390 C<&GetAllIssues> an arrayref, C<$issues>, of hashrefs, the keys of which
391 are the fields from the C<issues>, C<biblio>, C<biblioitems>, and
392 C<items> tables of the Koha database.
393
394 =cut
395
396 #'
397 sub GetAllIssues {
398     my ( $borrowernumber, $order, $limit ) = @_;
399
400     return unless $borrowernumber;
401     $order = 'date_due desc' unless $order;
402
403     my $dbh = C4::Context->dbh;
404     my $query =
405 'SELECT *, issues.timestamp as issuestimestamp, issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
406   FROM issues 
407   LEFT JOIN items on items.itemnumber=issues.itemnumber
408   LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
409   LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
410   WHERE borrowernumber=? 
411   UNION ALL
412   SELECT *, old_issues.timestamp as issuestimestamp, old_issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp 
413   FROM old_issues 
414   LEFT JOIN items on items.itemnumber=old_issues.itemnumber
415   LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
416   LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
417   WHERE borrowernumber=? AND old_issues.itemnumber IS NOT NULL
418   order by ' . $order;
419     if ($limit) {
420         $query .= " limit $limit";
421     }
422
423     my $sth = $dbh->prepare($query);
424     $sth->execute( $borrowernumber, $borrowernumber );
425     return $sth->fetchall_arrayref( {} );
426 }
427
428 sub checkcardnumber {
429     my ( $cardnumber, $borrowernumber ) = @_;
430
431     # If cardnumber is null, we assume they're allowed.
432     return 0 unless defined $cardnumber;
433
434     my $dbh = C4::Context->dbh;
435     my $query = "SELECT * FROM borrowers WHERE cardnumber=?";
436     $query .= " AND borrowernumber <> ?" if ($borrowernumber);
437     my $sth = $dbh->prepare($query);
438     $sth->execute(
439         $cardnumber,
440         ( $borrowernumber ? $borrowernumber : () )
441     );
442
443     return 1 if $sth->fetchrow_hashref;
444
445     my ( $min_length, $max_length ) = get_cardnumber_length();
446     return 2
447         if length $cardnumber > $max_length
448         or length $cardnumber < $min_length;
449
450     return 0;
451 }
452
453 =head2 get_cardnumber_length
454
455     my ($min, $max) = C4::Members::get_cardnumber_length()
456
457 Returns the minimum and maximum length for patron cardnumbers as
458 determined by the CardnumberLength system preference, the
459 BorrowerMandatoryField system preference, and the width of the
460 database column.
461
462 =cut
463
464 sub get_cardnumber_length {
465     my $borrower = Koha::Schema->resultset('Borrower');
466     my $field_size = $borrower->result_source->column_info('cardnumber')->{size};
467     my ( $min, $max ) = ( 0, $field_size ); # borrowers.cardnumber is a nullable varchar(20)
468     $min = 1 if C4::Context->preference('BorrowerMandatoryField') =~ /cardnumber/;
469     if ( my $cardnumber_length = C4::Context->preference('CardnumberLength') ) {
470         # Is integer and length match
471         if ( $cardnumber_length =~ m|^\d+$| ) {
472             $min = $max = $cardnumber_length
473                 if $cardnumber_length >= $min
474                     and $cardnumber_length <= $max;
475         }
476         # Else assuming it is a range
477         elsif ( $cardnumber_length =~ m|(\d*),(\d*)| ) {
478             $min = $1 if $1 and $min < $1;
479             $max = $2 if $2 and $max > $2;
480         }
481
482     }
483     $min = $max if $min > $max;
484     return ( $min, $max );
485 }
486
487 =head2 GetBorrowersToExpunge
488
489   $borrowers = &GetBorrowersToExpunge(
490       not_borrowed_since => $not_borrowed_since,
491       expired_before       => $expired_before,
492       category_code        => $category_code,
493       patron_list_id       => $patron_list_id,
494       branchcode           => $branchcode
495   );
496
497   This function get all borrowers based on the given criteria.
498
499 =cut
500
501 sub GetBorrowersToExpunge {
502
503     my $params = shift;
504     my $filterdate       = $params->{'not_borrowed_since'};
505     my $filterexpiry     = $params->{'expired_before'};
506     my $filterlastseen   = $params->{'last_seen'};
507     my $filtercategory   = $params->{'category_code'};
508     my $filterbranch     = $params->{'branchcode'} ||
509                         ((C4::Context->preference('IndependentBranches')
510                              && C4::Context->userenv 
511                              && !C4::Context->IsSuperLibrarian()
512                              && C4::Context->userenv->{branch})
513                          ? C4::Context->userenv->{branch}
514                          : "");  
515     my $filterpatronlist = $params->{'patron_list_id'};
516
517     my $dbh   = C4::Context->dbh;
518     my $query = q|
519         SELECT *
520         FROM (
521             SELECT borrowers.borrowernumber,
522                    MAX(old_issues.timestamp) AS latestissue,
523                    MAX(issues.timestamp) AS currentissue
524             FROM   borrowers
525             JOIN   categories USING (categorycode)
526             LEFT JOIN (
527                 SELECT guarantorid
528                 FROM borrowers
529                 WHERE guarantorid IS NOT NULL
530                     AND guarantorid <> 0
531             ) as tmp ON borrowers.borrowernumber=tmp.guarantorid
532             LEFT JOIN old_issues USING (borrowernumber)
533             LEFT JOIN issues USING (borrowernumber)|;
534     if ( $filterpatronlist  ){
535         $query .= q| LEFT JOIN patron_list_patrons USING (borrowernumber)|;
536     }
537     $query .= q| WHERE  category_type <> 'S'
538         AND tmp.guarantorid IS NULL
539     |;
540     my @query_params;
541     if ( $filterbranch && $filterbranch ne "" ) {
542         $query.= " AND borrowers.branchcode = ? ";
543         push( @query_params, $filterbranch );
544     }
545     if ( $filterexpiry ) {
546         $query .= " AND dateexpiry < ? ";
547         push( @query_params, $filterexpiry );
548     }
549     if ( $filterlastseen ) {
550         $query .= ' AND lastseen < ? ';
551         push @query_params, $filterlastseen;
552     }
553     if ( $filtercategory ) {
554         $query .= " AND categorycode = ? ";
555         push( @query_params, $filtercategory );
556     }
557     if ( $filterpatronlist ){
558         $query.=" AND patron_list_id = ? ";
559         push( @query_params, $filterpatronlist );
560     }
561     $query .= " GROUP BY borrowers.borrowernumber";
562     $query .= q|
563         ) xxx WHERE currentissue IS NULL|;
564     if ( $filterdate ) {
565         $query.=" AND ( latestissue < ? OR latestissue IS NULL ) ";
566         push @query_params,$filterdate;
567     }
568
569     warn $query if $debug;
570
571     my $sth = $dbh->prepare($query);
572     if (scalar(@query_params)>0){  
573         $sth->execute(@query_params);
574     }
575     else {
576         $sth->execute;
577     }
578     
579     my @results;
580     while ( my $data = $sth->fetchrow_hashref ) {
581         push @results, $data;
582     }
583     return \@results;
584 }
585
586 =head2 IssueSlip
587
588   IssueSlip($branchcode, $borrowernumber, $quickslip)
589
590   Returns letter hash ( see C4::Letters::GetPreparedLetter )
591
592   $quickslip is boolean, to indicate whether we want a quick slip
593
594   IssueSlip populates ISSUESLIP and ISSUEQSLIP, and will make the following expansions:
595
596   Both slips:
597
598       <<branches.*>>
599       <<borrowers.*>>
600
601   ISSUESLIP:
602
603       <checkedout>
604          <<biblio.*>>
605          <<items.*>>
606          <<biblioitems.*>>
607          <<issues.*>>
608       </checkedout>
609
610       <overdue>
611          <<biblio.*>>
612          <<items.*>>
613          <<biblioitems.*>>
614          <<issues.*>>
615       </overdue>
616
617       <news>
618          <<opac_news.*>>
619       </news>
620
621   ISSUEQSLIP:
622
623       <checkedout>
624          <<biblio.*>>
625          <<items.*>>
626          <<biblioitems.*>>
627          <<issues.*>>
628       </checkedout>
629
630   NOTE: Fields from tables issues, items, biblio and biblioitems are available
631
632 =cut
633
634 sub IssueSlip {
635     my ($branch, $borrowernumber, $quickslip) = @_;
636
637     # FIXME Check callers before removing this statement
638     #return unless $borrowernumber;
639
640     my $patron = Koha::Patrons->find( $borrowernumber );
641     return unless $patron;
642
643     my $pending_checkouts = $patron->pending_checkouts; # Should be $patron->checkouts->pending?
644
645     my ($letter_code, %repeat, %loops);
646     if ( $quickslip ) {
647         my $today_start = dt_from_string->set( hour => 0, minute => 0, second => 0 );
648         my $today_end = dt_from_string->set( hour => 23, minute => 59, second => 0 );
649         $today_start = Koha::Database->new->schema->storage->datetime_parser->format_datetime( $today_start );
650         $today_end = Koha::Database->new->schema->storage->datetime_parser->format_datetime( $today_end );
651         $letter_code = 'ISSUEQSLIP';
652
653         # issue date or lastreneweddate is today
654         my $todays_checkouts = $pending_checkouts->search(
655             {
656                 -or => {
657                     issuedate => {
658                         '>=' => $today_start,
659                         '<=' => $today_end,
660                     },
661                     lastreneweddate =>
662                       { '>=' => $today_start, '<=' => $today_end, }
663                 }
664             }
665         );
666         my @checkouts;
667         while ( my $c = $todays_checkouts->next ) {
668             my $all = $c->unblessed_all_relateds;
669             push @checkouts, {
670                 biblio      => $all,
671                 items       => $all,
672                 biblioitems => $all,
673                 issues      => $all,
674             };
675         }
676
677         %repeat =  (
678             checkedout => \@checkouts, # Historical syntax
679         );
680         %loops = (
681             issues => [ map { $_->{issues}{itemnumber} } @checkouts ], # TT syntax
682         );
683     }
684     else {
685         my $today = Koha::Database->new->schema->storage->datetime_parser->format_datetime( dt_from_string );
686         # Checkouts due in the future
687         my $checkouts = $pending_checkouts->search({ date_due => { '>' => $today } });
688         my @checkouts; my @overdues;
689         while ( my $c = $checkouts->next ) {
690             my $all = $c->unblessed_all_relateds;
691             push @checkouts, {
692                 biblio      => $all,
693                 items       => $all,
694                 biblioitems => $all,
695                 issues      => $all,
696             };
697         }
698
699         # Checkouts due in the past are overdues
700         my $overdues = $pending_checkouts->search({ date_due => { '<=' => $today } });
701         while ( my $o = $overdues->next ) {
702             my $all = $o->unblessed_all_relateds;
703             push @overdues, {
704                 biblio      => $all,
705                 items       => $all,
706                 biblioitems => $all,
707                 issues      => $all,
708             };
709         }
710         my $news = GetNewsToDisplay( "slip", $branch );
711         my @news = map {
712             $_->{'timestamp'} = $_->{'newdate'};
713             { opac_news => $_ }
714         } @$news;
715         $letter_code = 'ISSUESLIP';
716         %repeat      = (
717             checkedout => \@checkouts,
718             overdue    => \@overdues,
719             news       => \@news,
720         );
721         %loops = (
722             issues => [ map { $_->{issues}{itemnumber} } @checkouts ],
723             overdues   => [ map { $_->{issues}{itemnumber} } @overdues ],
724             opac_news => [ map { $_->{opac_news}{idnew} } @news ],
725         );
726     }
727
728     return  C4::Letters::GetPreparedLetter (
729         module => 'circulation',
730         letter_code => $letter_code,
731         branchcode => $branch,
732         lang => $patron->lang,
733         tables => {
734             'branches'    => $branch,
735             'borrowers'   => $borrowernumber,
736         },
737         repeat => \%repeat,
738         loops => \%loops,
739     );
740 }
741
742 =head2 AddMember_Auto
743
744 =cut
745
746 sub AddMember_Auto {
747     my ( %borrower ) = @_;
748
749     my $patron = Koha::Patron->new(\%borrower)->store;
750
751     return %{ $patron->unblessed };
752 }
753
754 =head2 AddMember_Opac
755
756 =cut
757
758 sub AddMember_Opac {
759     my ( %borrower ) = @_;
760
761     $borrower{'categorycode'} //= C4::Context->preference('PatronSelfRegistrationDefaultCategory');
762     my $password = $borrower{password};
763     if (not defined $password){
764         my $sr = new String::Random;
765         $sr->{'A'} = [ 'A'..'Z', 'a'..'z' ];
766         $password = $sr->randpattern("AAAAAAAAAA");
767         $borrower{'password'} = $password;
768     }
769
770     %borrower = AddMember_Auto(%borrower);
771
772     return ( $borrower{'borrowernumber'}, $password );
773 }
774
775 =head2 DeleteExpiredOpacRegistrations
776
777     Delete accounts that haven't been upgraded from the 'temporary' category
778     Returns the number of removed patrons
779
780 =cut
781
782 sub DeleteExpiredOpacRegistrations {
783
784     my $delay = C4::Context->preference('PatronSelfRegistrationExpireTemporaryAccountsDelay');
785     my $category_code = C4::Context->preference('PatronSelfRegistrationDefaultCategory');
786
787     return 0 if not $category_code or not defined $delay or $delay eq q||;
788
789     my $query = qq|
790 SELECT borrowernumber
791 FROM borrowers
792 WHERE categorycode = ? AND DATEDIFF( NOW(), dateenrolled ) > ? |;
793
794     my $dbh = C4::Context->dbh;
795     my $sth = $dbh->prepare($query);
796     $sth->execute( $category_code, $delay );
797     my $cnt=0;
798     while ( my ($borrowernumber) = $sth->fetchrow_array() ) {
799         Koha::Patrons->find($borrowernumber)->delete;
800         $cnt++;
801     }
802     return $cnt;
803 }
804
805 =head2 DeleteUnverifiedOpacRegistrations
806
807     Delete all unverified self registrations in borrower_modifications,
808     older than the specified number of days.
809
810 =cut
811
812 sub DeleteUnverifiedOpacRegistrations {
813     my ( $days ) = @_;
814     my $dbh = C4::Context->dbh;
815     my $sql=qq|
816 DELETE FROM borrower_modifications
817 WHERE borrowernumber = 0 AND DATEDIFF( NOW(), timestamp ) > ?|;
818     my $cnt=$dbh->do($sql, undef, ($days) );
819     return $cnt eq '0E0'? 0: $cnt;
820 }
821
822 END { }    # module clean-up code here (global destructor)
823
824 1;
825
826 __END__
827
828 =head1 AUTHOR
829
830 Koha Team
831
832 =cut