Bug 19303: Move C4::Members::GetFirstValidEmailAddress to Koha::Patron->first_valid_e...
[koha.git] / C4 / Members.pm
blobecd9f59833a61b97a269a92a15f94437387ff029
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 C4::Log; # logaction
30 use C4::Overdues;
31 use C4::Reserves;
32 use C4::Accounts;
33 use C4::Biblio;
34 use C4::Letters;
35 use C4::Members::Attributes qw(SearchIdMatchingAttribute UpdateBorrowerAttribute);
36 use C4::NewsChannels; #get slip news
37 use DateTime;
38 use Koha::Database;
39 use Koha::DateUtils;
40 use Text::Unaccent qw( unac_string );
41 use Koha::AuthUtils qw(hash_password);
42 use Koha::Database;
43 use Koha::Holds;
44 use Koha::List::Patron;
45 use Koha::Patrons;
46 use Koha::Patron::Categories;
47 use Koha::Schema;
49 our (@ISA,@EXPORT,@EXPORT_OK,$debug);
51 use Module::Load::Conditional qw( can_load );
52 if ( ! can_load( modules => { 'Koha::NorwegianPatronDB' => undef } ) ) {
53 $debug && warn "Unable to load Koha::NorwegianPatronDB";
57 BEGIN {
58 $debug = $ENV{DEBUG} || 0;
59 require Exporter;
60 @ISA = qw(Exporter);
61 #Get data
62 push @EXPORT, qw(
64 &GetPendingIssues
65 &GetAllIssues
67 &GetNoticeEmailAddress
69 &GetMemberAccountRecords
71 &GetBorrowersToExpunge
73 &IssueSlip
75 GetOverduesForPatron
78 #Modify data
79 push @EXPORT, qw(
80 &ModMember
81 &changepassword
84 #Insert data
85 push @EXPORT, qw(
86 &AddMember
87 &AddMember_Auto
88 &AddMember_Opac
91 #Check data
92 push @EXPORT, qw(
93 &checkuniquemember
94 &checkuserpassword
95 &Check_Userid
96 &Generate_Userid
97 &fixup_cardnumber
98 &checkcardnumber
102 =head1 NAME
104 C4::Members - Perl Module containing convenience functions for member handling
106 =head1 SYNOPSIS
108 use C4::Members;
110 =head1 DESCRIPTION
112 This module contains routines for adding, modifying and deleting members/patrons/borrowers
114 =head1 FUNCTIONS
116 =head2 patronflags
118 $flags = &patronflags($patron);
120 This function is not exported.
122 The following will be set where applicable:
123 $flags->{CHARGES}->{amount} Amount of debt
124 $flags->{CHARGES}->{noissues} Set if debt amount >$5.00 (or syspref noissuescharge)
125 $flags->{CHARGES}->{message} Message -- deprecated
127 $flags->{CREDITS}->{amount} Amount of credit
128 $flags->{CREDITS}->{message} Message -- deprecated
130 $flags->{ GNA } Patron has no valid address
131 $flags->{ GNA }->{noissues} Set for each GNA
132 $flags->{ GNA }->{message} "Borrower has no valid address" -- deprecated
134 $flags->{ LOST } Patron's card reported lost
135 $flags->{ LOST }->{noissues} Set for each LOST
136 $flags->{ LOST }->{message} Message -- deprecated
138 $flags->{DBARRED} Set if patron debarred, no access
139 $flags->{DBARRED}->{noissues} Set for each DBARRED
140 $flags->{DBARRED}->{message} Message -- deprecated
142 $flags->{ NOTES }
143 $flags->{ NOTES }->{message} The note itself. NOT deprecated
145 $flags->{ ODUES } Set if patron has overdue books.
146 $flags->{ ODUES }->{message} "Yes" -- deprecated
147 $flags->{ ODUES }->{itemlist} ref-to-array: list of overdue books
148 $flags->{ ODUES }->{itemlisttext} Text list of overdue items -- deprecated
150 $flags->{WAITING} Set if any of patron's reserves are available
151 $flags->{WAITING}->{message} Message -- deprecated
152 $flags->{WAITING}->{itemlist} ref-to-array: list of available items
154 =over
156 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlist}> is a reference-to-array listing the
157 overdue items. Its elements are references-to-hash, each describing an
158 overdue item. The keys are selected fields from the issues, biblio,
159 biblioitems, and items tables of the Koha database.
161 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlisttext}> is a string giving a text listing of
162 the overdue items, one per line. Deprecated.
164 =item C<$flags-E<gt>{WAITING}-E<gt>{itemlist}> is a reference-to-array listing the
165 available items. Each element is a reference-to-hash whose keys are
166 fields from the reserves table of the Koha database.
168 =back
170 All the "message" fields that include language generated in this function are deprecated,
171 because such strings belong properly in the display layer.
173 The "message" field that comes from the DB is OK.
175 =cut
177 # TODO: use {anonymous => hashes} instead of a dozen %flaginfo
178 # FIXME rename this function.
179 sub patronflags {
180 my %flags;
181 my ( $patroninformation) = @_;
182 my $dbh=C4::Context->dbh;
183 my ($balance, $owing) = GetMemberAccountBalance( $patroninformation->{'borrowernumber'});
184 if ( $owing > 0 ) {
185 my %flaginfo;
186 my $noissuescharge = C4::Context->preference("noissuescharge") || 5;
187 $flaginfo{'message'} = sprintf 'Patron owes %.02f', $owing;
188 $flaginfo{'amount'} = sprintf "%.02f", $owing;
189 if ( $owing > $noissuescharge && !C4::Context->preference("AllowFineOverride") ) {
190 $flaginfo{'noissues'} = 1;
192 $flags{'CHARGES'} = \%flaginfo;
194 elsif ( $balance < 0 ) {
195 my %flaginfo;
196 $flaginfo{'message'} = sprintf 'Patron has credit of %.02f', -$balance;
197 $flaginfo{'amount'} = sprintf "%.02f", $balance;
198 $flags{'CREDITS'} = \%flaginfo;
201 # Check the debt of the guarntees of this patron
202 my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
203 $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
204 if ( defined $no_issues_charge_guarantees ) {
205 my $p = Koha::Patrons->find( $patroninformation->{borrowernumber} );
206 my @guarantees = $p->guarantees();
207 my $guarantees_non_issues_charges;
208 foreach my $g ( @guarantees ) {
209 my ( $b, $n, $o ) = C4::Members::GetMemberAccountBalance( $g->id );
210 $guarantees_non_issues_charges += $n;
213 if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees ) {
214 my %flaginfo;
215 $flaginfo{'message'} = sprintf 'patron guarantees owe %.02f', $guarantees_non_issues_charges;
216 $flaginfo{'amount'} = $guarantees_non_issues_charges;
217 $flaginfo{'noissues'} = 1 unless C4::Context->preference("allowfineoverride");
218 $flags{'CHARGES_GUARANTEES'} = \%flaginfo;
222 if ( $patroninformation->{'gonenoaddress'}
223 && $patroninformation->{'gonenoaddress'} == 1 )
225 my %flaginfo;
226 $flaginfo{'message'} = 'Borrower has no valid address.';
227 $flaginfo{'noissues'} = 1;
228 $flags{'GNA'} = \%flaginfo;
230 if ( $patroninformation->{'lost'} && $patroninformation->{'lost'} == 1 ) {
231 my %flaginfo;
232 $flaginfo{'message'} = 'Borrower\'s card reported lost.';
233 $flaginfo{'noissues'} = 1;
234 $flags{'LOST'} = \%flaginfo;
236 if ( $patroninformation->{'debarred'} && check_date( split( /-/, $patroninformation->{'debarred'} ) ) ) {
237 if ( Date_to_Days(Date::Calc::Today) < Date_to_Days( split( /-/, $patroninformation->{'debarred'} ) ) ) {
238 my %flaginfo;
239 $flaginfo{'debarredcomment'} = $patroninformation->{'debarredcomment'};
240 $flaginfo{'message'} = $patroninformation->{'debarredcomment'};
241 $flaginfo{'noissues'} = 1;
242 $flaginfo{'dateend'} = $patroninformation->{'debarred'};
243 $flags{'DBARRED'} = \%flaginfo;
246 if ( $patroninformation->{'borrowernotes'}
247 && $patroninformation->{'borrowernotes'} )
249 my %flaginfo;
250 $flaginfo{'message'} = $patroninformation->{'borrowernotes'};
251 $flags{'NOTES'} = \%flaginfo;
253 my ( $odues, $itemsoverdue ) = C4::Overdues::checkoverdues($patroninformation->{'borrowernumber'});
254 if ( $odues && $odues > 0 ) {
255 my %flaginfo;
256 $flaginfo{'message'} = "Yes";
257 $flaginfo{'itemlist'} = $itemsoverdue;
258 foreach ( sort { $a->{'date_due'} cmp $b->{'date_due'} }
259 @$itemsoverdue )
261 $flaginfo{'itemlisttext'} .=
262 "$_->{'date_due'} $_->{'barcode'} $_->{'title'} \n"; # newline is display layer
264 $flags{'ODUES'} = \%flaginfo;
267 my $patron = Koha::Patrons->find( $patroninformation->{borrowernumber} );
268 my $waiting_holds = $patron->holds->search({ found => 'W' });
269 my $nowaiting = $waiting_holds->count;
270 if ( $nowaiting > 0 ) {
271 my %flaginfo;
272 $flaginfo{'message'} = "Reserved items available";
273 $flaginfo{'itemlist'} = $waiting_holds->unblessed;
274 $flags{'WAITING'} = \%flaginfo;
276 return ( \%flags );
280 =head2 ModMember
282 my $success = ModMember(borrowernumber => $borrowernumber,
283 [ field => value ]... );
285 Modify borrower's data. All date fields should ALREADY be in ISO format.
287 return :
288 true on success, or false on failure
290 =cut
292 sub ModMember {
293 my (%data) = @_;
295 # trim whitespace from data which has some non-whitespace in it.
296 foreach my $field_name (keys(%data)) {
297 if ( defined $data{$field_name} && $data{$field_name} =~ /\S/ ) {
298 $data{$field_name} =~ s/^\s*|\s*$//g;
302 # test to know if you must update or not the borrower password
303 if (exists $data{password}) {
304 if ($data{password} eq '****' or $data{password} eq '') {
305 delete $data{password};
306 } else {
307 if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
308 # Update the hashed PIN in borrower_sync.hashed_pin, before Koha hashes it
309 Koha::NorwegianPatronDB::NLUpdateHashedPIN( $data{'borrowernumber'}, $data{password} );
311 $data{password} = hash_password($data{password});
315 my $old_categorycode = Koha::Patrons->find( $data{borrowernumber} )->categorycode;
317 # get only the columns of a borrower
318 my $schema = Koha::Database->new()->schema;
319 my @columns = $schema->source('Borrower')->columns;
320 my $new_borrower = { map { join(' ', @columns) =~ /$_/ ? ( $_ => $data{$_} ) : () } keys(%data) };
322 $new_borrower->{dateofbirth} ||= undef if exists $new_borrower->{dateofbirth};
323 $new_borrower->{dateenrolled} ||= undef if exists $new_borrower->{dateenrolled};
324 $new_borrower->{dateexpiry} ||= undef if exists $new_borrower->{dateexpiry};
325 $new_borrower->{debarred} ||= undef if exists $new_borrower->{debarred};
326 $new_borrower->{sms_provider_id} ||= undef if exists $new_borrower->{sms_provider_id};
327 $new_borrower->{guarantorid} ||= undef if exists $new_borrower->{guarantorid};
329 my $patron = Koha::Patrons->find( $new_borrower->{borrowernumber} );
331 delete $new_borrower->{userid} if exists $new_borrower->{userid} and not $new_borrower->{userid};
333 my $execute_success = $patron->store if $patron->set($new_borrower);
335 if ($execute_success) { # only proceed if the update was a success
336 # If the patron changes to a category with enrollment fee, we add a fee
337 if ( $data{categorycode} and $data{categorycode} ne $old_categorycode ) {
338 if ( C4::Context->preference('FeeOnChangePatronCategory') ) {
339 $patron->add_enrolment_fee_if_needed;
343 # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
344 # cronjob will use for syncing with NL
345 if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
346 my $borrowersync = Koha::Database->new->schema->resultset('BorrowerSync')->find({
347 'synctype' => 'norwegianpatrondb',
348 'borrowernumber' => $data{'borrowernumber'}
350 # Do not set to "edited" if syncstatus is "new". We need to sync as new before
351 # we can sync as changed. And the "new sync" will pick up all changes since
352 # the patron was created anyway.
353 if ( $borrowersync->syncstatus ne 'new' && $borrowersync->syncstatus ne 'delete' ) {
354 $borrowersync->update( { 'syncstatus' => 'edited' } );
356 # Set the value of 'sync'
357 $borrowersync->update( { 'sync' => $data{'sync'} } );
358 # Try to do the live sync
359 Koha::NorwegianPatronDB::NLSync({ 'borrowernumber' => $data{'borrowernumber'} });
362 logaction("MEMBERS", "MODIFY", $data{'borrowernumber'}, "UPDATE (executed w/ arg: $data{'borrowernumber'})") if C4::Context->preference("BorrowersLog");
364 return $execute_success;
367 =head2 AddMember
369 $borrowernumber = &AddMember(%borrower);
371 insert new borrower into table
373 (%borrower keys are database columns. Database columns could be
374 different in different versions. Please look into database for correct
375 column names.)
377 Returns the borrowernumber upon success
379 Returns as undef upon any db error without further processing
381 =cut
384 sub AddMember {
385 my (%data) = @_;
386 my $dbh = C4::Context->dbh;
387 my $schema = Koha::Database->new()->schema;
389 my $category = Koha::Patron::Categories->find( $data{categorycode} );
390 unless ($category) {
391 Koha::Exceptions::BadParameter->throw(
392 error => 'Invalid parameter passed',
393 parameter => 'categorycode'
397 # trim whitespace from data which has some non-whitespace in it.
398 foreach my $field_name (keys(%data)) {
399 if ( defined $data{$field_name} && $data{$field_name} =~ /\S/ ) {
400 $data{$field_name} =~ s/^\s*|\s*$//g;
404 # generate a proper login if none provided
405 $data{'userid'} = Generate_Userid( $data{'borrowernumber'}, $data{'firstname'}, $data{'surname'} )
406 if ( $data{'userid'} eq '' || !Check_Userid( $data{'userid'} ) );
408 # add expiration date if it isn't already there
409 $data{dateexpiry} ||= $category->get_expiry_date;
411 # add enrollment date if it isn't already there
412 unless ( $data{'dateenrolled'} ) {
413 $data{'dateenrolled'} = output_pref( { dt => dt_from_string, dateonly => 1, dateformat => 'iso' } );
416 if ( C4::Context->preference("autoMemberNum") ) {
417 if ( not exists $data{cardnumber} or not defined $data{cardnumber} or $data{cardnumber} eq '' ) {
418 $data{cardnumber} = fixup_cardnumber( $data{cardnumber} );
422 $data{'privacy'} =
423 $category->default_privacy() eq 'default' ? 1
424 : $category->default_privacy() eq 'never' ? 2
425 : $category->default_privacy() eq 'forever' ? 0
426 : undef;
428 $data{'privacy_guarantor_checkouts'} = 0 unless defined( $data{'privacy_guarantor_checkouts'} );
430 # Make a copy of the plain text password for later use
431 my $plain_text_password = $data{'password'};
433 # create a disabled account if no password provided
434 $data{'password'} = ($data{'password'})? hash_password($data{'password'}) : '!';
436 # we don't want invalid dates in the db (mysql has a bad habit of inserting 0000-00-00
437 $data{'dateofbirth'} = undef if ( not $data{'dateofbirth'} );
438 $data{'debarred'} = undef if ( not $data{'debarred'} );
439 $data{'sms_provider_id'} = undef if ( not $data{'sms_provider_id'} );
440 $data{'guarantorid'} = undef if ( not $data{'guarantorid'} );
442 # get only the columns of Borrower
443 # FIXME Do we really need this check?
444 my @columns = $schema->source('Borrower')->columns;
445 my $new_member = { map { join(' ',@columns) =~ /$_/ ? ( $_ => $data{$_} ) : () } keys(%data) } ;
447 delete $new_member->{borrowernumber};
449 my $patron = Koha::Patron->new( $new_member )->store;
450 $data{borrowernumber} = $patron->borrowernumber;
452 # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
453 # cronjob will use for syncing with NL
454 if ( exists $data{'borrowernumber'} && C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
455 Koha::Database->new->schema->resultset('BorrowerSync')->create({
456 'borrowernumber' => $data{'borrowernumber'},
457 'synctype' => 'norwegianpatrondb',
458 'sync' => 1,
459 'syncstatus' => 'new',
460 'hashed_pin' => Koha::NorwegianPatronDB::NLEncryptPIN( $plain_text_password ),
464 logaction("MEMBERS", "CREATE", $data{'borrowernumber'}, "") if C4::Context->preference("BorrowersLog");
466 $patron->add_enrolment_fee_if_needed;
468 return $data{borrowernumber};
471 =head2 Check_Userid
473 my $uniqueness = Check_Userid($userid,$borrowernumber);
475 $borrowernumber is optional (i.e. it can contain a blank value). If $userid is passed with a blank $borrowernumber variable, the database will be checked for all instances of that userid (i.e. userid=? AND borrowernumber != '').
477 If $borrowernumber is provided, the database will be checked for every instance of that userid coupled with a different borrower(number) than the one provided.
479 return :
480 0 for not unique (i.e. this $userid already exists)
481 1 for unique (i.e. this $userid does not exist, or this $userid/$borrowernumber combination already exists)
483 =cut
485 sub Check_Userid {
486 my ( $uid, $borrowernumber ) = @_;
488 return 0 unless ($uid); # userid is a unique column, we should assume NULL is not unique
490 return 0 if ( $uid eq C4::Context->config('user') );
492 my $rs = Koha::Database->new()->schema()->resultset('Borrower');
494 my $params;
495 $params->{userid} = $uid;
496 $params->{borrowernumber} = { '!=' => $borrowernumber } if ($borrowernumber);
498 my $count = $rs->count( $params );
500 return $count ? 0 : 1;
503 =head2 Generate_Userid
505 my $newuid = Generate_Userid($borrowernumber, $firstname, $surname);
507 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
509 $borrowernumber is optional (i.e. it can contain a blank value). A value is passed when generating a new userid for an existing borrower. When a new userid is created for a new borrower, a blank value is passed to this sub.
511 return :
512 new userid ($firstname.$surname if there is a $firstname, or $surname if there is no value in $firstname) plus offset (0 if the $newuid is unique, or a higher numeric value if Check_Userid finds an existing match for the $newuid in the database).
514 =cut
516 sub Generate_Userid {
517 my ($borrowernumber, $firstname, $surname) = @_;
518 my $newuid;
519 my $offset = 0;
520 #The script will "do" the following code and increment the $offset until Check_Userid = 1 (i.e. until $newuid comes back as unique)
521 do {
522 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
523 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
524 $newuid = lc(($firstname)? "$firstname.$surname" : $surname);
525 $newuid = unac_string('utf-8',$newuid);
526 $newuid .= $offset unless $offset == 0;
527 $offset++;
529 } while (!Check_Userid($newuid,$borrowernumber));
531 return $newuid;
534 =head2 fixup_cardnumber
536 Warning: The caller is responsible for locking the members table in write
537 mode, to avoid database corruption.
539 =cut
541 use vars qw( @weightings );
542 my @weightings = ( 8, 4, 6, 3, 5, 2, 1 );
544 sub fixup_cardnumber {
545 my ($cardnumber) = @_;
546 my $autonumber_members = C4::Context->boolean_preference('autoMemberNum') || 0;
548 # Find out whether member numbers should be generated
549 # automatically. Should be either "1" or something else.
550 # Defaults to "0", which is interpreted as "no".
552 # if ($cardnumber !~ /\S/ && $autonumber_members) {
553 ($autonumber_members) or return $cardnumber;
554 my $checkdigit = C4::Context->preference('checkdigit');
555 my $dbh = C4::Context->dbh;
556 if ( $checkdigit and $checkdigit eq 'katipo' ) {
558 # if checkdigit is selected, calculate katipo-style cardnumber.
559 # otherwise, just use the max()
560 # purpose: generate checksum'd member numbers.
561 # We'll assume we just got the max value of digits 2-8 of member #'s
562 # from the database and our job is to increment that by one,
563 # determine the 1st and 9th digits and return the full string.
564 my $sth = $dbh->prepare(
565 "select max(substring(borrowers.cardnumber,2,7)) as new_num from borrowers"
567 $sth->execute;
568 my $data = $sth->fetchrow_hashref;
569 $cardnumber = $data->{new_num};
570 if ( !$cardnumber ) { # If DB has no values,
571 $cardnumber = 1000000; # start at 1000000
572 } else {
573 $cardnumber += 1;
576 my $sum = 0;
577 for ( my $i = 0 ; $i < 8 ; $i += 1 ) {
578 # read weightings, left to right, 1 char at a time
579 my $temp1 = $weightings[$i];
581 # sequence left to right, 1 char at a time
582 my $temp2 = substr( $cardnumber, $i, 1 );
584 # mult each char 1-7 by its corresponding weighting
585 $sum += $temp1 * $temp2;
588 my $rem = ( $sum % 11 );
589 $rem = 'X' if $rem == 10;
591 return "V$cardnumber$rem";
592 } else {
594 my $sth = $dbh->prepare(
595 'SELECT MAX( CAST( cardnumber AS SIGNED ) ) FROM borrowers WHERE cardnumber REGEXP "^-?[0-9]+$"'
597 $sth->execute;
598 my ($result) = $sth->fetchrow;
599 return $result + 1;
601 return $cardnumber; # just here as a fallback/reminder
604 =head2 GetPendingIssues
606 my $issues = &GetPendingIssues(@borrowernumber);
608 Looks up what the patron with the given borrowernumber has borrowed.
610 C<&GetPendingIssues> returns a
611 reference-to-array where each element is a reference-to-hash; the
612 keys are the fields from the C<issues>, C<biblio>, and C<items> tables.
613 The keys include C<biblioitems> fields.
615 =cut
617 sub GetPendingIssues {
618 my @borrowernumbers = @_;
620 unless (@borrowernumbers ) { # return a ref_to_array
621 return \@borrowernumbers; # to not cause surprise to caller
624 # Borrowers part of the query
625 my $bquery = '';
626 for (my $i = 0; $i < @borrowernumbers; $i++) {
627 $bquery .= ' issues.borrowernumber = ?';
628 if ($i < $#borrowernumbers ) {
629 $bquery .= ' OR';
633 # FIXME: namespace collision: each table has "timestamp" fields. Which one is "timestamp" ?
634 # FIXME: circ/ciculation.pl tries to sort by timestamp!
635 # FIXME: namespace collision: other collisions possible.
636 # FIXME: most of this data isn't really being used by callers.
637 my $query =
638 "SELECT issues.*,
639 items.*,
640 biblio.*,
641 biblioitems.volume,
642 biblioitems.number,
643 biblioitems.itemtype,
644 biblioitems.isbn,
645 biblioitems.issn,
646 biblioitems.publicationyear,
647 biblioitems.publishercode,
648 biblioitems.volumedate,
649 biblioitems.volumedesc,
650 biblioitems.lccn,
651 biblioitems.url,
652 borrowers.firstname,
653 borrowers.surname,
654 borrowers.cardnumber,
655 issues.timestamp AS timestamp,
656 issues.renewals AS renewals,
657 issues.borrowernumber AS borrowernumber,
658 items.renewals AS totalrenewals
659 FROM issues
660 LEFT JOIN items ON items.itemnumber = issues.itemnumber
661 LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
662 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
663 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
664 WHERE
665 $bquery
666 ORDER BY issues.issuedate"
669 my $sth = C4::Context->dbh->prepare($query);
670 $sth->execute(@borrowernumbers);
671 my $data = $sth->fetchall_arrayref({});
672 my $today = dt_from_string;
673 foreach (@{$data}) {
674 if ($_->{issuedate}) {
675 $_->{issuedate} = dt_from_string($_->{issuedate}, 'sql');
677 $_->{date_due_sql} = $_->{date_due};
678 # FIXME no need to have this value
679 $_->{date_due} or next;
680 $_->{date_due_sql} = $_->{date_due};
681 # FIXME no need to have this value
682 $_->{date_due} = dt_from_string($_->{date_due}, 'sql');
683 if ( DateTime->compare($_->{date_due}, $today) == -1 ) {
684 $_->{overdue} = 1;
687 return $data;
690 =head2 GetAllIssues
692 $issues = &GetAllIssues($borrowernumber, $sortkey, $limit);
694 Looks up what the patron with the given borrowernumber has borrowed,
695 and sorts the results.
697 C<$sortkey> is the name of a field on which to sort the results. This
698 should be the name of a field in the C<issues>, C<biblio>,
699 C<biblioitems>, or C<items> table in the Koha database.
701 C<$limit> is the maximum number of results to return.
703 C<&GetAllIssues> an arrayref, C<$issues>, of hashrefs, the keys of which
704 are the fields from the C<issues>, C<biblio>, C<biblioitems>, and
705 C<items> tables of the Koha database.
707 =cut
710 sub GetAllIssues {
711 my ( $borrowernumber, $order, $limit ) = @_;
713 return unless $borrowernumber;
714 $order = 'date_due desc' unless $order;
716 my $dbh = C4::Context->dbh;
717 my $query =
718 'SELECT *, issues.timestamp as issuestimestamp, issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
719 FROM issues
720 LEFT JOIN items on items.itemnumber=issues.itemnumber
721 LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
722 LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
723 WHERE borrowernumber=?
724 UNION ALL
725 SELECT *, old_issues.timestamp as issuestimestamp, old_issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
726 FROM old_issues
727 LEFT JOIN items on items.itemnumber=old_issues.itemnumber
728 LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
729 LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
730 WHERE borrowernumber=? AND old_issues.itemnumber IS NOT NULL
731 order by ' . $order;
732 if ($limit) {
733 $query .= " limit $limit";
736 my $sth = $dbh->prepare($query);
737 $sth->execute( $borrowernumber, $borrowernumber );
738 return $sth->fetchall_arrayref( {} );
742 =head2 GetMemberAccountRecords
744 ($total, $acctlines, $count) = &GetMemberAccountRecords($borrowernumber);
746 Looks up accounting data for the patron with the given borrowernumber.
748 C<&GetMemberAccountRecords> returns a three-element array. C<$acctlines> is a
749 reference-to-array, where each element is a reference-to-hash; the
750 keys are the fields of the C<accountlines> table in the Koha database.
751 C<$count> is the number of elements in C<$acctlines>. C<$total> is the
752 total amount outstanding for all of the account lines.
754 =cut
756 sub GetMemberAccountRecords {
757 my ($borrowernumber) = @_;
758 my $dbh = C4::Context->dbh;
759 my @acctlines;
760 my $numlines = 0;
761 my $strsth = qq(
762 SELECT *
763 FROM accountlines
764 WHERE borrowernumber=?);
765 $strsth.=" ORDER BY accountlines_id desc";
766 my $sth= $dbh->prepare( $strsth );
767 $sth->execute( $borrowernumber );
769 my $total = 0;
770 while ( my $data = $sth->fetchrow_hashref ) {
771 if ( $data->{itemnumber} ) {
772 my $item = Koha::Items->find( $data->{itemnumber} );
773 my $biblio = $item->biblio;
774 $data->{biblionumber} = $biblio->biblionumber;
775 $data->{title} = $biblio->title;
777 $acctlines[$numlines] = $data;
778 $numlines++;
779 $total += sprintf "%.0f", 1000*$data->{amountoutstanding}; # convert float to integer to avoid round-off errors
781 $total /= 1000;
782 return ( $total, \@acctlines,$numlines);
785 =head2 GetMemberAccountBalance
787 ($total_balance, $non_issue_balance, $other_charges) = &GetMemberAccountBalance($borrowernumber);
789 Calculates amount immediately owing by the patron - non-issue charges.
790 Based on GetMemberAccountRecords.
791 Charges exempt from non-issue are:
792 * Res (reserves)
793 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
794 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
796 =cut
798 sub GetMemberAccountBalance {
799 my ($borrowernumber) = @_;
801 my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
803 my @not_fines;
804 push @not_fines, 'Res' unless C4::Context->preference('HoldsInNoissuesCharge');
805 push @not_fines, 'Rent' unless C4::Context->preference('RentalsInNoissuesCharge');
806 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
807 my $dbh = C4::Context->dbh;
808 my $man_inv_types = $dbh->selectcol_arrayref(qq{SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'});
809 push @not_fines, map substr($_, 0, $ACCOUNT_TYPE_LENGTH), @$man_inv_types;
811 my %not_fine = map {$_ => 1} @not_fines;
813 my ($total, $acctlines) = GetMemberAccountRecords($borrowernumber);
814 my $other_charges = 0;
815 foreach (@$acctlines) {
816 $other_charges += $_->{amountoutstanding} if $not_fine{ substr($_->{accounttype}, 0, $ACCOUNT_TYPE_LENGTH) };
819 return ( $total, $total - $other_charges, $other_charges);
822 sub checkcardnumber {
823 my ( $cardnumber, $borrowernumber ) = @_;
825 # If cardnumber is null, we assume they're allowed.
826 return 0 unless defined $cardnumber;
828 my $dbh = C4::Context->dbh;
829 my $query = "SELECT * FROM borrowers WHERE cardnumber=?";
830 $query .= " AND borrowernumber <> ?" if ($borrowernumber);
831 my $sth = $dbh->prepare($query);
832 $sth->execute(
833 $cardnumber,
834 ( $borrowernumber ? $borrowernumber : () )
837 return 1 if $sth->fetchrow_hashref;
839 my ( $min_length, $max_length ) = get_cardnumber_length();
840 return 2
841 if length $cardnumber > $max_length
842 or length $cardnumber < $min_length;
844 return 0;
847 =head2 get_cardnumber_length
849 my ($min, $max) = C4::Members::get_cardnumber_length()
851 Returns the minimum and maximum length for patron cardnumbers as
852 determined by the CardnumberLength system preference, the
853 BorrowerMandatoryField system preference, and the width of the
854 database column.
856 =cut
858 sub get_cardnumber_length {
859 my $borrower = Koha::Schema->resultset('Borrower');
860 my $field_size = $borrower->result_source->column_info('cardnumber')->{size};
861 my ( $min, $max ) = ( 0, $field_size ); # borrowers.cardnumber is a nullable varchar(20)
862 $min = 1 if C4::Context->preference('BorrowerMandatoryField') =~ /cardnumber/;
863 if ( my $cardnumber_length = C4::Context->preference('CardnumberLength') ) {
864 # Is integer and length match
865 if ( $cardnumber_length =~ m|^\d+$| ) {
866 $min = $max = $cardnumber_length
867 if $cardnumber_length >= $min
868 and $cardnumber_length <= $max;
870 # Else assuming it is a range
871 elsif ( $cardnumber_length =~ m|(\d*),(\d*)| ) {
872 $min = $1 if $1 and $min < $1;
873 $max = $2 if $2 and $max > $2;
877 $min = $max if $min > $max;
878 return ( $min, $max );
881 =head2 GetNoticeEmailAddress
883 $email = GetNoticeEmailAddress($borrowernumber);
885 Return the email address of borrower used for notices, given the borrowernumber.
886 Returns the empty string if no email address.
888 =cut
890 sub GetNoticeEmailAddress {
891 my $borrowernumber = shift;
893 my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
894 # if syspref is set to 'first valid' (value == OFF), look up email address
895 if ( $which_address eq 'OFF' ) {
896 my $patron = Koha::Patrons->find( $borrowernumber );
897 return $patron->first_valid_email_address();
899 # specified email address field
900 my $dbh = C4::Context->dbh;
901 my $sth = $dbh->prepare( qq{
902 SELECT $which_address AS primaryemail
903 FROM borrowers
904 WHERE borrowernumber=?
905 } );
906 $sth->execute($borrowernumber);
907 my $data = $sth->fetchrow_hashref;
908 return $data->{'primaryemail'} || '';
911 =head2 GetBorrowersToExpunge
913 $borrowers = &GetBorrowersToExpunge(
914 not_borrowed_since => $not_borrowed_since,
915 expired_before => $expired_before,
916 category_code => $category_code,
917 patron_list_id => $patron_list_id,
918 branchcode => $branchcode
921 This function get all borrowers based on the given criteria.
923 =cut
925 sub GetBorrowersToExpunge {
927 my $params = shift;
928 my $filterdate = $params->{'not_borrowed_since'};
929 my $filterexpiry = $params->{'expired_before'};
930 my $filterlastseen = $params->{'last_seen'};
931 my $filtercategory = $params->{'category_code'};
932 my $filterbranch = $params->{'branchcode'} ||
933 ((C4::Context->preference('IndependentBranches')
934 && C4::Context->userenv
935 && !C4::Context->IsSuperLibrarian()
936 && C4::Context->userenv->{branch})
937 ? C4::Context->userenv->{branch}
938 : "");
939 my $filterpatronlist = $params->{'patron_list_id'};
941 my $dbh = C4::Context->dbh;
942 my $query = q|
943 SELECT borrowers.borrowernumber,
944 MAX(old_issues.timestamp) AS latestissue,
945 MAX(issues.timestamp) AS currentissue
946 FROM borrowers
947 JOIN categories USING (categorycode)
948 LEFT JOIN (
949 SELECT guarantorid
950 FROM borrowers
951 WHERE guarantorid IS NOT NULL
952 AND guarantorid <> 0
953 ) as tmp ON borrowers.borrowernumber=tmp.guarantorid
954 LEFT JOIN old_issues USING (borrowernumber)
955 LEFT JOIN issues USING (borrowernumber)|;
956 if ( $filterpatronlist ){
957 $query .= q| LEFT JOIN patron_list_patrons USING (borrowernumber)|;
959 $query .= q| WHERE category_type <> 'S'
960 AND tmp.guarantorid IS NULL
962 my @query_params;
963 if ( $filterbranch && $filterbranch ne "" ) {
964 $query.= " AND borrowers.branchcode = ? ";
965 push( @query_params, $filterbranch );
967 if ( $filterexpiry ) {
968 $query .= " AND dateexpiry < ? ";
969 push( @query_params, $filterexpiry );
971 if ( $filterlastseen ) {
972 $query .= ' AND lastseen < ? ';
973 push @query_params, $filterlastseen;
975 if ( $filtercategory ) {
976 $query .= " AND categorycode = ? ";
977 push( @query_params, $filtercategory );
979 if ( $filterpatronlist ){
980 $query.=" AND patron_list_id = ? ";
981 push( @query_params, $filterpatronlist );
983 $query.=" GROUP BY borrowers.borrowernumber HAVING currentissue IS NULL ";
984 if ( $filterdate ) {
985 $query.=" AND ( latestissue < ? OR latestissue IS NULL ) ";
986 push @query_params,$filterdate;
988 warn $query if $debug;
990 my $sth = $dbh->prepare($query);
991 if (scalar(@query_params)>0){
992 $sth->execute(@query_params);
994 else {
995 $sth->execute;
998 my @results;
999 while ( my $data = $sth->fetchrow_hashref ) {
1000 push @results, $data;
1002 return \@results;
1005 =head2 IssueSlip
1007 IssueSlip($branchcode, $borrowernumber, $quickslip)
1009 Returns letter hash ( see C4::Letters::GetPreparedLetter )
1011 $quickslip is boolean, to indicate whether we want a quick slip
1013 IssueSlip populates ISSUESLIP and ISSUEQSLIP, and will make the following expansions:
1015 Both slips:
1017 <<branches.*>>
1018 <<borrowers.*>>
1020 ISSUESLIP:
1022 <checkedout>
1023 <<biblio.*>>
1024 <<items.*>>
1025 <<biblioitems.*>>
1026 <<issues.*>>
1027 </checkedout>
1029 <overdue>
1030 <<biblio.*>>
1031 <<items.*>>
1032 <<biblioitems.*>>
1033 <<issues.*>>
1034 </overdue>
1036 <news>
1037 <<opac_news.*>>
1038 </news>
1040 ISSUEQSLIP:
1042 <checkedout>
1043 <<biblio.*>>
1044 <<items.*>>
1045 <<biblioitems.*>>
1046 <<issues.*>>
1047 </checkedout>
1049 NOTE: Not all table fields are available, pleasee see GetPendingIssues for a list of available fields.
1051 =cut
1053 sub IssueSlip {
1054 my ($branch, $borrowernumber, $quickslip) = @_;
1056 # FIXME Check callers before removing this statement
1057 #return unless $borrowernumber;
1059 my $patron = Koha::Patrons->find( $borrowernumber );
1060 return unless $patron;
1062 my @issues = @{ GetPendingIssues($borrowernumber) };
1064 for my $issue (@issues) {
1065 $issue->{date_due} = $issue->{date_due_sql};
1066 if ($quickslip) {
1067 my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
1068 if ( substr( $issue->{issuedate}, 0, 10 ) eq $today
1069 or substr( $issue->{lastreneweddate}, 0, 10 ) eq $today ) {
1070 $issue->{now} = 1;
1075 # Sort on timestamp then on issuedate then on issue_id
1076 # useful for tests and could be if modified in a batch
1077 @issues = sort {
1078 $b->{timestamp} <=> $a->{timestamp}
1079 or $b->{issuedate} <=> $a->{issuedate}
1080 or $b->{issue_id} <=> $a->{issue_id}
1081 } @issues;
1083 my ($letter_code, %repeat, %loops);
1084 if ( $quickslip ) {
1085 $letter_code = 'ISSUEQSLIP';
1086 my @checkouts = map {
1087 'biblio' => $_,
1088 'items' => $_,
1089 'biblioitems' => $_,
1090 'issues' => $_,
1091 }, grep { $_->{'now'} } @issues;
1092 %repeat = (
1093 checkedout => \@checkouts, # History syntax
1095 %loops = (
1096 issues => [ map { $_->{issues}{itemnumber} } @checkouts ], # TT syntax
1099 else {
1100 my @checkouts = map {
1101 'biblio' => $_,
1102 'items' => $_,
1103 'biblioitems' => $_,
1104 'issues' => $_,
1105 }, grep { !$_->{'overdue'} } @issues;
1106 my @overdues = map {
1107 'biblio' => $_,
1108 'items' => $_,
1109 'biblioitems' => $_,
1110 'issues' => $_,
1111 }, grep { $_->{'overdue'} } @issues;
1112 my $news = GetNewsToDisplay( "slip", $branch );
1113 my @news = map {
1114 $_->{'timestamp'} = $_->{'newdate'};
1115 { opac_news => $_ }
1116 } @$news;
1117 $letter_code = 'ISSUESLIP';
1118 %repeat = (
1119 checkedout => \@checkouts,
1120 overdue => \@overdues,
1121 news => \@news,
1123 %loops = (
1124 issues => [ map { $_->{issues}{itemnumber} } @checkouts ],
1125 overdues => [ map { $_->{issues}{itemnumber} } @overdues ],
1126 opac_news => [ map { $_->{opac_news}{idnew} } @news ],
1130 return C4::Letters::GetPreparedLetter (
1131 module => 'circulation',
1132 letter_code => $letter_code,
1133 branchcode => $branch,
1134 lang => $patron->lang,
1135 tables => {
1136 'branches' => $branch,
1137 'borrowers' => $borrowernumber,
1139 repeat => \%repeat,
1140 loops => \%loops,
1144 =head2 AddMember_Auto
1146 =cut
1148 sub AddMember_Auto {
1149 my ( %borrower ) = @_;
1151 $borrower{'cardnumber'} ||= fixup_cardnumber();
1153 $borrower{'borrowernumber'} = AddMember(%borrower);
1155 return ( %borrower );
1158 =head2 AddMember_Opac
1160 =cut
1162 sub AddMember_Opac {
1163 my ( %borrower ) = @_;
1165 $borrower{'categorycode'} //= C4::Context->preference('PatronSelfRegistrationDefaultCategory');
1166 if (not defined $borrower{'password'}){
1167 my $sr = new String::Random;
1168 $sr->{'A'} = [ 'A'..'Z', 'a'..'z' ];
1169 my $password = $sr->randpattern("AAAAAAAAAA");
1170 $borrower{'password'} = $password;
1173 %borrower = AddMember_Auto(%borrower);
1175 return ( $borrower{'borrowernumber'}, $borrower{'password'} );
1178 =head2 DeleteExpiredOpacRegistrations
1180 Delete accounts that haven't been upgraded from the 'temporary' category
1181 Returns the number of removed patrons
1183 =cut
1185 sub DeleteExpiredOpacRegistrations {
1187 my $delay = C4::Context->preference('PatronSelfRegistrationExpireTemporaryAccountsDelay');
1188 my $category_code = C4::Context->preference('PatronSelfRegistrationDefaultCategory');
1190 return 0 if not $category_code or not defined $delay or $delay eq q||;
1192 my $query = qq|
1193 SELECT borrowernumber
1194 FROM borrowers
1195 WHERE categorycode = ? AND DATEDIFF( NOW(), dateenrolled ) > ? |;
1197 my $dbh = C4::Context->dbh;
1198 my $sth = $dbh->prepare($query);
1199 $sth->execute( $category_code, $delay );
1200 my $cnt=0;
1201 while ( my ($borrowernumber) = $sth->fetchrow_array() ) {
1202 Koha::Patrons->find($borrowernumber)->delete;
1203 $cnt++;
1205 return $cnt;
1208 =head2 DeleteUnverifiedOpacRegistrations
1210 Delete all unverified self registrations in borrower_modifications,
1211 older than the specified number of days.
1213 =cut
1215 sub DeleteUnverifiedOpacRegistrations {
1216 my ( $days ) = @_;
1217 my $dbh = C4::Context->dbh;
1218 my $sql=qq|
1219 DELETE FROM borrower_modifications
1220 WHERE borrowernumber = 0 AND DATEDIFF( NOW(), timestamp ) > ?|;
1221 my $cnt=$dbh->do($sql, undef, ($days) );
1222 return $cnt eq '0E0'? 0: $cnt;
1225 sub GetOverduesForPatron {
1226 my ( $borrowernumber ) = @_;
1228 my $sql = "
1229 SELECT *
1230 FROM issues, items, biblio, biblioitems
1231 WHERE items.itemnumber=issues.itemnumber
1232 AND biblio.biblionumber = items.biblionumber
1233 AND biblio.biblionumber = biblioitems.biblionumber
1234 AND issues.borrowernumber = ?
1235 AND date_due < NOW()
1238 my $sth = C4::Context->dbh->prepare( $sql );
1239 $sth->execute( $borrowernumber );
1241 return $sth->fetchall_arrayref({});
1244 END { } # module clean-up code here (global destructor)
1248 __END__
1250 =head1 AUTHOR
1252 Koha Team
1254 =cut