3 # Copyright 2000-2003 Katipo Communications
4 # Copyright 2010 BibLibre
5 # Parts Copyright 2010 Catalyst IT
7 # This file is part of Koha.
9 # Koha is free software; you can redistribute it and/or modify it
10 # under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
14 # Koha is distributed in the hope that it will be useful, but
15 # WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with Koha; if not, see <http://www.gnu.org/licenses>.
24 #use warnings; FIXME - Bug 2505
26 use String
::Random
qw( random_string );
27 use Scalar
::Util
qw( looks_like_number );
28 use Date
::Calc qw
/Today check_date Date_to_Days/;
29 use C4
::Log
; # logaction
35 use C4
::Members
::Attributes
qw(SearchIdMatchingAttribute UpdateBorrowerAttribute);
36 use C4
::NewsChannels
; #get slip news
40 use Text
::Unaccent
qw( unac_string );
41 use Koha
::AuthUtils
qw(hash_password);
44 use Koha
::List
::Patron
;
46 use Koha
::Patron
::Categories
;
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";
58 $debug = $ENV{DEBUG
} || 0;
67 &GetFirstValidEmailAddress
68 &GetNoticeEmailAddress
70 &GetMemberAccountRecords
71 &GetBorNotifyAcctRecord
73 &GetBorrowersToExpunge
106 C4::Members - Perl Module containing convenience functions for member handling
114 This module contains routines for adding, modifying and deleting members/patrons/borrowers
120 $flags = &patronflags($patron);
122 This function is not exported.
124 The following will be set where applicable:
125 $flags->{CHARGES}->{amount} Amount of debt
126 $flags->{CHARGES}->{noissues} Set if debt amount >$5.00 (or syspref noissuescharge)
127 $flags->{CHARGES}->{message} Message -- deprecated
129 $flags->{CREDITS}->{amount} Amount of credit
130 $flags->{CREDITS}->{message} Message -- deprecated
132 $flags->{ GNA } Patron has no valid address
133 $flags->{ GNA }->{noissues} Set for each GNA
134 $flags->{ GNA }->{message} "Borrower has no valid address" -- deprecated
136 $flags->{ LOST } Patron's card reported lost
137 $flags->{ LOST }->{noissues} Set for each LOST
138 $flags->{ LOST }->{message} Message -- deprecated
140 $flags->{DBARRED} Set if patron debarred, no access
141 $flags->{DBARRED}->{noissues} Set for each DBARRED
142 $flags->{DBARRED}->{message} Message -- deprecated
145 $flags->{ NOTES }->{message} The note itself. NOT deprecated
147 $flags->{ ODUES } Set if patron has overdue books.
148 $flags->{ ODUES }->{message} "Yes" -- deprecated
149 $flags->{ ODUES }->{itemlist} ref-to-array: list of overdue books
150 $flags->{ ODUES }->{itemlisttext} Text list of overdue items -- deprecated
152 $flags->{WAITING} Set if any of patron's reserves are available
153 $flags->{WAITING}->{message} Message -- deprecated
154 $flags->{WAITING}->{itemlist} ref-to-array: list of available items
158 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlist}> is a reference-to-array listing the
159 overdue items. Its elements are references-to-hash, each describing an
160 overdue item. The keys are selected fields from the issues, biblio,
161 biblioitems, and items tables of the Koha database.
163 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlisttext}> is a string giving a text listing of
164 the overdue items, one per line. Deprecated.
166 =item C<$flags-E<gt>{WAITING}-E<gt>{itemlist}> is a reference-to-array listing the
167 available items. Each element is a reference-to-hash whose keys are
168 fields from the reserves table of the Koha database.
172 All the "message" fields that include language generated in this function are deprecated,
173 because such strings belong properly in the display layer.
175 The "message" field that comes from the DB is OK.
179 # TODO: use {anonymous => hashes} instead of a dozen %flaginfo
180 # FIXME rename this function.
183 my ( $patroninformation) = @_;
184 my $dbh=C4
::Context
->dbh;
185 my ($balance, $owing) = GetMemberAccountBalance
( $patroninformation->{'borrowernumber'});
188 my $noissuescharge = C4
::Context
->preference("noissuescharge") || 5;
189 $flaginfo{'message'} = sprintf 'Patron owes %.02f', $owing;
190 $flaginfo{'amount'} = sprintf "%.02f", $owing;
191 if ( $owing > $noissuescharge && !C4
::Context
->preference("AllowFineOverride") ) {
192 $flaginfo{'noissues'} = 1;
194 $flags{'CHARGES'} = \
%flaginfo;
196 elsif ( $balance < 0 ) {
198 $flaginfo{'message'} = sprintf 'Patron has credit of %.02f', -$balance;
199 $flaginfo{'amount'} = sprintf "%.02f", $balance;
200 $flags{'CREDITS'} = \
%flaginfo;
203 # Check the debt of the guarntees of this patron
204 my $no_issues_charge_guarantees = C4
::Context
->preference("NoIssuesChargeGuarantees");
205 $no_issues_charge_guarantees = undef unless looks_like_number
( $no_issues_charge_guarantees );
206 if ( defined $no_issues_charge_guarantees ) {
207 my $p = Koha
::Patrons
->find( $patroninformation->{borrowernumber
} );
208 my @guarantees = $p->guarantees();
209 my $guarantees_non_issues_charges;
210 foreach my $g ( @guarantees ) {
211 my ( $b, $n, $o ) = C4
::Members
::GetMemberAccountBalance
( $g->id );
212 $guarantees_non_issues_charges += $n;
215 if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees ) {
217 $flaginfo{'message'} = sprintf 'patron guarantees owe %.02f', $guarantees_non_issues_charges;
218 $flaginfo{'amount'} = $guarantees_non_issues_charges;
219 $flaginfo{'noissues'} = 1 unless C4
::Context
->preference("allowfineoverride");
220 $flags{'CHARGES_GUARANTEES'} = \
%flaginfo;
224 if ( $patroninformation->{'gonenoaddress'}
225 && $patroninformation->{'gonenoaddress'} == 1 )
228 $flaginfo{'message'} = 'Borrower has no valid address.';
229 $flaginfo{'noissues'} = 1;
230 $flags{'GNA'} = \
%flaginfo;
232 if ( $patroninformation->{'lost'} && $patroninformation->{'lost'} == 1 ) {
234 $flaginfo{'message'} = 'Borrower\'s card reported lost.';
235 $flaginfo{'noissues'} = 1;
236 $flags{'LOST'} = \
%flaginfo;
238 if ( $patroninformation->{'debarred'} && check_date
( split( /-/, $patroninformation->{'debarred'} ) ) ) {
239 if ( Date_to_Days
(Date
::Calc
::Today
) < Date_to_Days
( split( /-/, $patroninformation->{'debarred'} ) ) ) {
241 $flaginfo{'debarredcomment'} = $patroninformation->{'debarredcomment'};
242 $flaginfo{'message'} = $patroninformation->{'debarredcomment'};
243 $flaginfo{'noissues'} = 1;
244 $flaginfo{'dateend'} = $patroninformation->{'debarred'};
245 $flags{'DBARRED'} = \
%flaginfo;
248 if ( $patroninformation->{'borrowernotes'}
249 && $patroninformation->{'borrowernotes'} )
252 $flaginfo{'message'} = $patroninformation->{'borrowernotes'};
253 $flags{'NOTES'} = \
%flaginfo;
255 my ( $odues, $itemsoverdue ) = C4
::Overdues
::checkoverdues
($patroninformation->{'borrowernumber'});
256 if ( $odues && $odues > 0 ) {
258 $flaginfo{'message'} = "Yes";
259 $flaginfo{'itemlist'} = $itemsoverdue;
260 foreach ( sort { $a->{'date_due'} cmp $b->{'date_due'} }
263 $flaginfo{'itemlisttext'} .=
264 "$_->{'date_due'} $_->{'barcode'} $_->{'title'} \n"; # newline is display layer
266 $flags{'ODUES'} = \
%flaginfo;
269 my $patron = Koha
::Patrons
->find( $patroninformation->{borrowernumber
} );
270 my $waiting_holds = $patron->holds->search({ found
=> 'W' });
271 my $nowaiting = $waiting_holds->count;
272 if ( $nowaiting > 0 ) {
274 $flaginfo{'message'} = "Reserved items available";
275 $flaginfo{'itemlist'} = $waiting_holds->unblessed;
276 $flags{'WAITING'} = \
%flaginfo;
284 my $success = ModMember(borrowernumber => $borrowernumber,
285 [ field => value ]... );
287 Modify borrower's data. All date fields should ALREADY be in ISO format.
290 true on success, or false on failure
297 # trim whitespace from data which has some non-whitespace in it.
298 foreach my $field_name (keys(%data)) {
299 if ( defined $data{$field_name} && $data{$field_name} =~ /\S/ ) {
300 $data{$field_name} =~ s/^\s*|\s*$//g;
304 # test to know if you must update or not the borrower password
305 if (exists $data{password
}) {
306 if ($data{password
} eq '****' or $data{password
} eq '') {
307 delete $data{password
};
309 if ( C4
::Context
->preference('NorwegianPatronDBEnable') && C4
::Context
->preference('NorwegianPatronDBEnable') == 1 ) {
310 # Update the hashed PIN in borrower_sync.hashed_pin, before Koha hashes it
311 Koha
::NorwegianPatronDB
::NLUpdateHashedPIN
( $data{'borrowernumber'}, $data{password
} );
313 $data{password
} = hash_password
($data{password
});
317 my $old_categorycode = Koha
::Patrons
->find( $data{borrowernumber
} )->categorycode;
319 # get only the columns of a borrower
320 my $schema = Koha
::Database
->new()->schema;
321 my @columns = $schema->source('Borrower')->columns;
322 my $new_borrower = { map { join(' ', @columns) =~ /$_/ ?
( $_ => $data{$_} ) : () } keys(%data) };
324 $new_borrower->{dateofbirth
} ||= undef if exists $new_borrower->{dateofbirth
};
325 $new_borrower->{dateenrolled
} ||= undef if exists $new_borrower->{dateenrolled
};
326 $new_borrower->{dateexpiry
} ||= undef if exists $new_borrower->{dateexpiry
};
327 $new_borrower->{debarred
} ||= undef if exists $new_borrower->{debarred
};
328 $new_borrower->{sms_provider_id
} ||= undef if exists $new_borrower->{sms_provider_id
};
329 $new_borrower->{guarantorid
} ||= undef if exists $new_borrower->{guarantorid
};
331 my $patron = Koha
::Patrons
->find( $new_borrower->{borrowernumber
} );
333 delete $new_borrower->{userid
} if exists $new_borrower->{userid
} and not $new_borrower->{userid
};
335 my $execute_success = $patron->store if $patron->set($new_borrower);
337 if ($execute_success) { # only proceed if the update was a success
338 # If the patron changes to a category with enrollment fee, we add a fee
339 if ( $data{categorycode
} and $data{categorycode
} ne $old_categorycode ) {
340 if ( C4
::Context
->preference('FeeOnChangePatronCategory') ) {
341 $patron->add_enrolment_fee_if_needed;
345 # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
346 # cronjob will use for syncing with NL
347 if ( C4
::Context
->preference('NorwegianPatronDBEnable') && C4
::Context
->preference('NorwegianPatronDBEnable') == 1 ) {
348 my $borrowersync = Koha
::Database
->new->schema->resultset('BorrowerSync')->find({
349 'synctype' => 'norwegianpatrondb',
350 'borrowernumber' => $data{'borrowernumber'}
352 # Do not set to "edited" if syncstatus is "new". We need to sync as new before
353 # we can sync as changed. And the "new sync" will pick up all changes since
354 # the patron was created anyway.
355 if ( $borrowersync->syncstatus ne 'new' && $borrowersync->syncstatus ne 'delete' ) {
356 $borrowersync->update( { 'syncstatus' => 'edited' } );
358 # Set the value of 'sync'
359 $borrowersync->update( { 'sync' => $data{'sync'} } );
360 # Try to do the live sync
361 Koha
::NorwegianPatronDB
::NLSync
({ 'borrowernumber' => $data{'borrowernumber'} });
364 logaction
("MEMBERS", "MODIFY", $data{'borrowernumber'}, "UPDATE (executed w/ arg: $data{'borrowernumber'})") if C4
::Context
->preference("BorrowersLog");
366 return $execute_success;
371 $borrowernumber = &AddMember(%borrower);
373 insert new borrower into table
375 (%borrower keys are database columns. Database columns could be
376 different in different versions. Please look into database for correct
379 Returns the borrowernumber upon success
381 Returns as undef upon any db error without further processing
388 my $dbh = C4
::Context
->dbh;
389 my $schema = Koha
::Database
->new()->schema;
391 # trim whitespace from data which has some non-whitespace in it.
392 foreach my $field_name (keys(%data)) {
393 if ( defined $data{$field_name} && $data{$field_name} =~ /\S/ ) {
394 $data{$field_name} =~ s/^\s*|\s*$//g;
398 # generate a proper login if none provided
399 $data{'userid'} = Generate_Userid
( $data{'borrowernumber'}, $data{'firstname'}, $data{'surname'} )
400 if ( $data{'userid'} eq '' || !Check_Userid
( $data{'userid'} ) );
402 # add expiration date if it isn't already there
403 $data{dateexpiry
} ||= Koha
::Patron
::Categories
->find( $data{categorycode
} )->get_expiry_date;
405 # add enrollment date if it isn't already there
406 unless ( $data{'dateenrolled'} ) {
407 $data{'dateenrolled'} = output_pref
( { dt
=> dt_from_string
, dateonly
=> 1, dateformat
=> 'iso' } );
410 if ( C4
::Context
->preference("autoMemberNum") ) {
411 if ( not exists $data{cardnumber
} or not defined $data{cardnumber
} or $data{cardnumber
} eq '' ) {
412 $data{cardnumber
} = fixup_cardnumber
( $data{cardnumber
} );
416 my $patron_category = $schema->resultset('Category')->find( $data{'categorycode'} );
418 $patron_category->default_privacy() eq 'default' ?
1
419 : $patron_category->default_privacy() eq 'never' ?
2
420 : $patron_category->default_privacy() eq 'forever' ?
0
423 $data{'privacy_guarantor_checkouts'} = 0 unless defined( $data{'privacy_guarantor_checkouts'} );
425 # Make a copy of the plain text password for later use
426 my $plain_text_password = $data{'password'};
428 # create a disabled account if no password provided
429 $data{'password'} = ($data{'password'})? hash_password
($data{'password'}) : '!';
431 # we don't want invalid dates in the db (mysql has a bad habit of inserting 0000-00-00
432 $data{'dateofbirth'} = undef if ( not $data{'dateofbirth'} );
433 $data{'debarred'} = undef if ( not $data{'debarred'} );
434 $data{'sms_provider_id'} = undef if ( not $data{'sms_provider_id'} );
435 $data{'guarantorid'} = undef if ( not $data{'guarantorid'} );
437 # get only the columns of Borrower
438 # FIXME Do we really need this check?
439 my @columns = $schema->source('Borrower')->columns;
440 my $new_member = { map { join(' ',@columns) =~ /$_/ ?
( $_ => $data{$_} ) : () } keys(%data) } ;
442 delete $new_member->{borrowernumber
};
444 my $patron = Koha
::Patron
->new( $new_member )->store;
445 $data{borrowernumber
} = $patron->borrowernumber;
447 # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
448 # cronjob will use for syncing with NL
449 if ( exists $data{'borrowernumber'} && C4
::Context
->preference('NorwegianPatronDBEnable') && C4
::Context
->preference('NorwegianPatronDBEnable') == 1 ) {
450 Koha
::Database
->new->schema->resultset('BorrowerSync')->create({
451 'borrowernumber' => $data{'borrowernumber'},
452 'synctype' => 'norwegianpatrondb',
454 'syncstatus' => 'new',
455 'hashed_pin' => Koha
::NorwegianPatronDB
::NLEncryptPIN
( $plain_text_password ),
459 logaction
("MEMBERS", "CREATE", $data{'borrowernumber'}, "") if C4
::Context
->preference("BorrowersLog");
461 $patron->add_enrolment_fee_if_needed;
463 return $data{borrowernumber
};
468 my $uniqueness = Check_Userid($userid,$borrowernumber);
470 $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 != '').
472 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.
475 0 for not unique (i.e. this $userid already exists)
476 1 for unique (i.e. this $userid does not exist, or this $userid/$borrowernumber combination already exists)
481 my ( $uid, $borrowernumber ) = @_;
483 return 0 unless ($uid); # userid is a unique column, we should assume NULL is not unique
485 return 0 if ( $uid eq C4
::Context
->config('user') );
487 my $rs = Koha
::Database
->new()->schema()->resultset('Borrower');
490 $params->{userid
} = $uid;
491 $params->{borrowernumber
} = { '!=' => $borrowernumber } if ($borrowernumber);
493 my $count = $rs->count( $params );
495 return $count ?
0 : 1;
498 =head2 Generate_Userid
500 my $newuid = Generate_Userid($borrowernumber, $firstname, $surname);
502 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
504 $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.
507 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).
511 sub Generate_Userid
{
512 my ($borrowernumber, $firstname, $surname) = @_;
515 #The script will "do" the following code and increment the $offset until Check_Userid = 1 (i.e. until $newuid comes back as unique)
517 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
518 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
519 $newuid = lc(($firstname)?
"$firstname.$surname" : $surname);
520 $newuid = unac_string
('utf-8',$newuid);
521 $newuid .= $offset unless $offset == 0;
524 } while (!Check_Userid
($newuid,$borrowernumber));
529 =head2 fixup_cardnumber
531 Warning: The caller is responsible for locking the members table in write
532 mode, to avoid database corruption.
536 use vars
qw( @weightings );
537 my @weightings = ( 8, 4, 6, 3, 5, 2, 1 );
539 sub fixup_cardnumber {
540 my ($cardnumber) = @_;
541 my $autonumber_members = C4::Context->boolean_preference('autoMemberNum') || 0;
543 # Find out whether member numbers should be generated
544 # automatically. Should be either "1" or something else.
545 # Defaults to "0", which is interpreted as "no".
547 # if ($cardnumber !~ /\S/ && $autonumber_members) {
548 ($autonumber_members) or return $cardnumber;
549 my $checkdigit = C4::Context->preference('checkdigit');
550 my $dbh = C4::Context->dbh;
551 if ( $checkdigit and $checkdigit eq 'katipo' ) {
553 # if checkdigit is selected, calculate katipo-style cardnumber.
554 # otherwise, just use the max()
555 # purpose: generate checksum'd member numbers.
556 # We'll assume we just got the max value of digits 2-8 of member #'s
557 # from the database and our job is to increment that by one,
558 # determine the 1st and 9th digits and return the full string.
559 my $sth = $dbh->prepare(
560 "select max(substring(borrowers.cardnumber,2,7)) as new_num from borrowers"
563 my $data = $sth->fetchrow_hashref;
564 $cardnumber = $data->{new_num};
565 if ( !$cardnumber ) { # If DB has no values,
566 $cardnumber = 1000000; # start at 1000000
572 for ( my $i = 0 ; $i < 8 ; $i += 1 ) {
573 # read weightings, left to right, 1 char at a time
574 my $temp1 = $weightings[$i];
576 # sequence left to right, 1 char at a time
577 my $temp2 = substr( $cardnumber, $i, 1 );
579 # mult each char 1-7 by its corresponding weighting
580 $sum += $temp1 * $temp2;
583 my $rem = ( $sum % 11 );
584 $rem = 'X' if $rem == 10;
586 return "V$cardnumber$rem";
589 my $sth = $dbh->prepare(
590 'SELECT MAX( CAST( cardnumber AS SIGNED ) ) FROM borrowers WHERE cardnumber REGEXP "^-?[0-9]+$"'
593 my ($result) = $sth->fetchrow;
596 return $cardnumber; # just here as a fallback/reminder
599 =head2 GetPendingIssues
601 my $issues = &GetPendingIssues(@borrowernumber);
603 Looks up what the patron with the given borrowernumber has borrowed.
605 C<&GetPendingIssues> returns a
606 reference-to-array where each element is a reference-to-hash; the
607 keys are the fields from the C<issues>, C<biblio>, and C<items> tables.
608 The keys include C<biblioitems> fields.
612 sub GetPendingIssues {
613 my @borrowernumbers = @_;
615 unless (@borrowernumbers ) { # return a ref_to_array
616 return \@borrowernumbers; # to not cause surprise to caller
619 # Borrowers part of the query
621 for (my $i = 0; $i < @borrowernumbers; $i++) {
622 $bquery .= ' issues.borrowernumber = ?';
623 if ($i < $#borrowernumbers ) {
628 # FIXME: namespace collision: each table has "timestamp" fields. Which one is "timestamp" ?
629 # FIXME: circ/ciculation.pl tries to sort by timestamp!
630 # FIXME: namespace collision: other collisions possible.
631 # FIXME: most of this data isn't really being used by callers.
638 biblioitems.itemtype,
641 biblioitems.publicationyear,
642 biblioitems.publishercode,
643 biblioitems.volumedate,
644 biblioitems.volumedesc,
649 borrowers.cardnumber,
650 issues.timestamp AS timestamp,
651 issues.renewals AS renewals,
652 issues.borrowernumber AS borrowernumber,
653 items.renewals AS totalrenewals
655 LEFT JOIN items ON items.itemnumber = issues.itemnumber
656 LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
657 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
658 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
661 ORDER BY issues.issuedate"
664 my $sth = C4::Context->dbh->prepare($query);
665 $sth->execute(@borrowernumbers);
666 my $data = $sth->fetchall_arrayref({});
667 my $today = dt_from_string;
669 if ($_->{issuedate}) {
670 $_->{issuedate} = dt_from_string($_->{issuedate}, 'sql');
672 $_->{date_due_sql} = $_->{date_due};
673 # FIXME no need to have this value
674 $_->{date_due} or next;
675 $_->{date_due_sql} = $_->{date_due};
676 # FIXME no need to have this value
677 $_->{date_due} = dt_from_string($_->{date_due}, 'sql');
678 if ( DateTime->compare($_->{date_due}, $today) == -1 ) {
687 $issues = &GetAllIssues($borrowernumber, $sortkey, $limit);
689 Looks up what the patron with the given borrowernumber has borrowed,
690 and sorts the results.
692 C<$sortkey> is the name of a field on which to sort the results. This
693 should be the name of a field in the C<issues>, C<biblio>,
694 C<biblioitems>, or C<items> table in the Koha database.
696 C<$limit> is the maximum number of results to return.
698 C<&GetAllIssues> an arrayref, C<$issues>, of hashrefs, the keys of which
699 are the fields from the C<issues>, C<biblio>, C<biblioitems>, and
700 C<items> tables of the Koha database.
706 my ( $borrowernumber, $order, $limit ) = @_;
708 return unless $borrowernumber;
709 $order = 'date_due desc' unless $order;
711 my $dbh = C4::Context->dbh;
713 'SELECT *, issues.timestamp as issuestimestamp, issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
715 LEFT JOIN items on items.itemnumber=issues.itemnumber
716 LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
717 LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
718 WHERE borrowernumber=?
720 SELECT *, old_issues.timestamp as issuestimestamp, old_issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
722 LEFT JOIN items on items.itemnumber=old_issues.itemnumber
723 LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
724 LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
725 WHERE borrowernumber=? AND old_issues.itemnumber IS NOT NULL
728 $query .= " limit $limit";
731 my $sth = $dbh->prepare($query);
732 $sth->execute( $borrowernumber, $borrowernumber );
733 return $sth->fetchall_arrayref( {} );
737 =head2 GetMemberAccountRecords
739 ($total, $acctlines, $count) = &GetMemberAccountRecords($borrowernumber);
741 Looks up accounting data for the patron with the given borrowernumber.
743 C<&GetMemberAccountRecords> returns a three-element array. C<$acctlines> is a
744 reference-to-array, where each element is a reference-to-hash; the
745 keys are the fields of the C<accountlines> table in the Koha database.
746 C<$count> is the number of elements in C<$acctlines>. C<$total> is the
747 total amount outstanding for all of the account lines.
751 sub GetMemberAccountRecords {
752 my ($borrowernumber) = @_;
753 my $dbh = C4::Context->dbh;
759 WHERE borrowernumber=?);
760 $strsth.=" ORDER BY accountlines_id desc";
761 my $sth= $dbh->prepare( $strsth );
762 $sth->execute( $borrowernumber );
765 while ( my $data = $sth->fetchrow_hashref ) {
766 if ( $data->{itemnumber} ) {
767 my $item = Koha::Items->find( $data->{itemnumber} );
768 my $biblio = $item->biblio;
769 $data->{biblionumber} = $biblio->biblionumber;
770 $data->{title} = $biblio->title;
772 $acctlines[$numlines] = $data;
774 $total += sprintf "%.0f", 1000*$data->{amountoutstanding}; # convert float to integer to avoid round-off errors
777 return ( $total, \@acctlines,$numlines);
780 =head2 GetMemberAccountBalance
782 ($total_balance, $non_issue_balance, $other_charges) = &GetMemberAccountBalance($borrowernumber);
784 Calculates amount immediately owing by the patron - non-issue charges.
785 Based on GetMemberAccountRecords.
786 Charges exempt from non-issue are:
788 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
789 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
793 sub GetMemberAccountBalance {
794 my ($borrowernumber) = @_;
796 my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
799 push @not_fines, 'Res' unless C4::Context->preference('HoldsInNoissuesCharge');
800 push @not_fines, 'Rent' unless C4::Context->preference('RentalsInNoissuesCharge');
801 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
802 my $dbh = C4::Context->dbh;
803 my $man_inv_types = $dbh->selectcol_arrayref(qq{SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'});
804 push @not_fines, map substr($_, 0, $ACCOUNT_TYPE_LENGTH), @$man_inv_types;
806 my %not_fine = map {$_ => 1} @not_fines;
808 my ($total, $acctlines) = GetMemberAccountRecords($borrowernumber);
809 my $other_charges = 0;
810 foreach (@$acctlines) {
811 $other_charges += $_->{amountoutstanding} if $not_fine{ substr($_->{accounttype}, 0, $ACCOUNT_TYPE_LENGTH) };
814 return ( $total, $total - $other_charges, $other_charges);
817 =head2 GetBorNotifyAcctRecord
819 ($total, $acctlines, $count) = &GetBorNotifyAcctRecord($params,$notifyid);
821 Looks up accounting data for the patron with the given borrowernumber per file number.
823 C<&GetBorNotifyAcctRecord> returns a three-element array. C<$acctlines> is a
824 reference-to-array, where each element is a reference-to-hash; the
825 keys are the fields of the C<accountlines> table in the Koha database.
826 C<$count> is the number of elements in C<$acctlines>. C<$total> is the
827 total amount outstanding for all of the account lines.
831 sub GetBorNotifyAcctRecord {
832 my ( $borrowernumber, $notifyid ) = @_;
833 my $dbh = C4::Context->dbh;
836 my $sth = $dbh->prepare(
839 WHERE borrowernumber=?
841 AND amountoutstanding != '0'
842 ORDER BY notify_id,accounttype
845 $sth->execute( $borrowernumber, $notifyid );
847 while ( my $data = $sth->fetchrow_hashref ) {
848 if ( $data->{itemnumber} ) {
849 my $item = Koha::Items->find( $data->{itemnumber} );
850 my $biblio = $item->biblio;
851 $data->{biblionumber} = $biblio->biblionumber;
852 $data->{title} = $biblio->title;
854 $acctlines[$numlines] = $data;
856 $total += int(100 * $data->{'amountoutstanding'});
859 return ( $total, \@acctlines, $numlines );
862 sub checkcardnumber {
863 my ( $cardnumber, $borrowernumber ) = @_;
865 # If cardnumber is null, we assume they're allowed.
866 return 0 unless defined $cardnumber;
868 my $dbh = C4::Context->dbh;
869 my $query = "SELECT * FROM borrowers WHERE cardnumber=?";
870 $query .= " AND borrowernumber <> ?" if ($borrowernumber);
871 my $sth = $dbh->prepare($query);
874 ( $borrowernumber ? $borrowernumber : () )
877 return 1 if $sth->fetchrow_hashref;
879 my ( $min_length, $max_length ) = get_cardnumber_length();
881 if length $cardnumber > $max_length
882 or length $cardnumber < $min_length;
887 =head2 get_cardnumber_length
889 my ($min, $max) = C4::Members::get_cardnumber_length()
891 Returns the minimum and maximum length for patron cardnumbers as
892 determined by the CardnumberLength system preference, the
893 BorrowerMandatoryField system preference, and the width of the
898 sub get_cardnumber_length {
899 my ( $min, $max ) = ( 0, 16 ); # borrowers.cardnumber is a nullable varchar(16)
900 $min = 1 if C4::Context->preference('BorrowerMandatoryField') =~ /cardnumber/;
901 if ( my $cardnumber_length = C4::Context->preference('CardnumberLength') ) {
902 # Is integer and length match
903 if ( $cardnumber_length =~ m|^\d+$| ) {
904 $min = $max = $cardnumber_length
905 if $cardnumber_length >= $min
906 and $cardnumber_length <= $max;
908 # Else assuming it is a range
909 elsif ( $cardnumber_length =~ m|(\d*),(\d*)| ) {
910 $min = $1 if $1 and $min < $1;
911 $max = $2 if $2 and $max > $2;
915 my $borrower = Koha::Schema->resultset('Borrower');
916 my $field_size = $borrower->result_source->column_info('cardnumber')->{size};
917 $min = $field_size if $min > $field_size;
918 return ( $min, $max );
921 =head2 GetFirstValidEmailAddress
923 $email = GetFirstValidEmailAddress($borrowernumber);
925 Return the first valid email address for a borrower, given the borrowernumber. For now, the order
926 is defined as email, emailpro, B_email. Returns the empty string if the borrower has no email
931 sub GetFirstValidEmailAddress {
932 my $borrowernumber = shift;
934 my $borrower = Koha::Patrons->find( $borrowernumber );
936 return $borrower->first_valid_email_address();
939 =head2 GetNoticeEmailAddress
941 $email = GetNoticeEmailAddress($borrowernumber);
943 Return the email address of borrower used for notices, given the borrowernumber.
944 Returns the empty string if no email address.
948 sub GetNoticeEmailAddress {
949 my $borrowernumber = shift;
951 my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
952 # if syspref is set to 'first valid' (value == OFF), look up email address
953 if ( $which_address eq 'OFF' ) {
954 return GetFirstValidEmailAddress($borrowernumber);
956 # specified email address field
957 my $dbh = C4::Context->dbh;
958 my $sth = $dbh->prepare( qq{
959 SELECT $which_address AS primaryemail
961 WHERE borrowernumber=?
963 $sth->execute($borrowernumber);
964 my $data = $sth->fetchrow_hashref;
965 return $data->{'primaryemail'} || '';
968 =head2 GetBorrowersToExpunge
970 $borrowers = &GetBorrowersToExpunge(
971 not_borrowed_since => $not_borrowed_since,
972 expired_before => $expired_before,
973 category_code => $category_code,
974 patron_list_id => $patron_list_id,
975 branchcode => $branchcode
978 This function get all borrowers based on the given criteria.
982 sub GetBorrowersToExpunge {
985 my $filterdate = $params->{'not_borrowed_since'};
986 my $filterexpiry = $params->{'expired_before'};
987 my $filterlastseen = $params->{'last_seen'};
988 my $filtercategory = $params->{'category_code'};
989 my $filterbranch = $params->{'branchcode'} ||
990 ((C4::Context->preference('IndependentBranches')
991 && C4::Context->userenv
992 && !C4::Context->IsSuperLibrarian()
993 && C4::Context->userenv->{branch})
994 ? C4::Context->userenv->{branch}
996 my $filterpatronlist = $params->{'patron_list_id'};
998 my $dbh = C4::Context->dbh;
1000 SELECT borrowers.borrowernumber,
1001 MAX(old_issues.timestamp) AS latestissue,
1002 MAX(issues.timestamp) AS currentissue
1004 JOIN categories USING (categorycode)
1008 WHERE guarantorid IS NOT NULL
1009 AND guarantorid <> 0
1010 ) as tmp ON borrowers.borrowernumber=tmp.guarantorid
1011 LEFT JOIN old_issues USING (borrowernumber)
1012 LEFT JOIN issues USING (borrowernumber)|;
1013 if ( $filterpatronlist ){
1014 $query .= q| LEFT JOIN patron_list_patrons USING (borrowernumber)|;
1016 $query .= q| WHERE category_type <> 'S'
1017 AND tmp.guarantorid IS NULL
1020 if ( $filterbranch && $filterbranch ne "" ) {
1021 $query.= " AND borrowers.branchcode = ? ";
1022 push( @query_params, $filterbranch );
1024 if ( $filterexpiry ) {
1025 $query .= " AND dateexpiry < ? ";
1026 push( @query_params, $filterexpiry );
1028 if ( $filterlastseen ) {
1029 $query .= ' AND lastseen < ? ';
1030 push @query_params, $filterlastseen;
1032 if ( $filtercategory ) {
1033 $query .= " AND categorycode = ? ";
1034 push( @query_params, $filtercategory );
1036 if ( $filterpatronlist ){
1037 $query.=" AND patron_list_id = ? ";
1038 push( @query_params, $filterpatronlist );
1040 $query.=" GROUP BY borrowers.borrowernumber HAVING currentissue IS NULL ";
1041 if ( $filterdate ) {
1042 $query.=" AND ( latestissue < ? OR latestissue IS NULL ) ";
1043 push @query_params,$filterdate;
1045 warn $query if $debug;
1047 my $sth = $dbh->prepare($query);
1048 if (scalar(@query_params)>0){
1049 $sth->execute(@query_params);
1056 while ( my $data = $sth->fetchrow_hashref ) {
1057 push @results, $data;
1064 IssueSlip($branchcode, $borrowernumber, $quickslip)
1066 Returns letter hash ( see C4::Letters::GetPreparedLetter )
1068 $quickslip is boolean, to indicate whether we want a quick slip
1070 IssueSlip populates ISSUESLIP and ISSUEQSLIP, and will make the following expansions:
1106 NOTE: Not all table fields are available, pleasee see GetPendingIssues for a list of available fields.
1111 my ($branch, $borrowernumber, $quickslip) = @_;
1113 # FIXME Check callers before removing this statement
1114 #return unless $borrowernumber;
1116 my $patron = Koha::Patrons->find( $borrowernumber );
1117 return unless $patron;
1119 my @issues = @{ GetPendingIssues($borrowernumber) };
1121 for my $issue (@issues) {
1122 $issue->{date_due} = $issue->{date_due_sql};
1124 my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
1125 if ( substr( $issue->{issuedate}, 0, 10 ) eq $today
1126 or substr( $issue->{lastreneweddate}, 0, 10 ) eq $today ) {
1132 # Sort on timestamp then on issuedate (useful for tests and could be if modified in a batch
1134 my $s = $b->{timestamp} <=> $a->{timestamp};
1136 $b->{issuedate} <=> $a->{issuedate} : $s;
1139 my ($letter_code, %repeat);
1141 $letter_code = 'ISSUEQSLIP';
1143 'checkedout' => [ map {
1146 'biblioitems' => $_,
1148 }, grep { $_->{'now
'} } @issues ],
1152 $letter_code = 'ISSUESLIP
';
1154 'checkedout
' => [ map {
1157 'biblioitems
' => $_,
1159 }, grep { !$_->{'overdue
'} } @issues ],
1161 'overdue
' => [ map {
1164 'biblioitems
' => $_,
1166 }, grep { $_->{'overdue
'} } @issues ],
1169 $_->{'timestamp
'} = $_->{'newdate
'};
1171 } @{ GetNewsToDisplay("slip",$branch) } ],
1175 return C4::Letters::GetPreparedLetter (
1176 module => 'circulation
',
1177 letter_code => $letter_code,
1178 branchcode => $branch,
1179 lang => $patron->lang,
1181 'branches
' => $branch,
1182 'borrowers
' => $borrowernumber,
1188 =head2 AddMember_Auto
1192 sub AddMember_Auto {
1193 my ( %borrower ) = @_;
1195 $borrower{'cardnumber
'} ||= fixup_cardnumber();
1197 $borrower{'borrowernumber
'} = AddMember(%borrower);
1199 return ( %borrower );
1202 =head2 AddMember_Opac
1206 sub AddMember_Opac {
1207 my ( %borrower ) = @_;
1209 $borrower{'categorycode
'} //= C4::Context->preference('PatronSelfRegistrationDefaultCategory
');
1210 if (not defined $borrower{'password
'}){
1211 my $sr = new String::Random;
1212 $sr->{'A
'} = [ 'A
'..'Z
', 'a
'..'z
' ];
1213 my $password = $sr->randpattern("AAAAAAAAAA");
1214 $borrower{'password
'} = $password;
1217 %borrower = AddMember_Auto(%borrower);
1219 return ( $borrower{'borrowernumber
'}, $borrower{'password
'} );
1222 =head2 DeleteExpiredOpacRegistrations
1224 Delete accounts that haven't been upgraded from the
'temporary' category
1225 Returns the number of removed patrons
1229 sub DeleteExpiredOpacRegistrations
{
1231 my $delay = C4
::Context
->preference('PatronSelfRegistrationExpireTemporaryAccountsDelay');
1232 my $category_code = C4
::Context
->preference('PatronSelfRegistrationDefaultCategory');
1234 return 0 if not $category_code or not defined $delay or $delay eq q
||;
1237 SELECT borrowernumber
1239 WHERE categorycode
= ? AND DATEDIFF
( NOW
(), dateenrolled
) > ?
|;
1241 my $dbh = C4
::Context
->dbh;
1242 my $sth = $dbh->prepare($query);
1243 $sth->execute( $category_code, $delay );
1245 while ( my ($borrowernumber) = $sth->fetchrow_array() ) {
1246 Koha
::Patrons
->find($borrowernumber)->delete;
1252 =head2 DeleteUnverifiedOpacRegistrations
1254 Delete all unverified self registrations in borrower_modifications,
1255 older than the specified number of days.
1259 sub DeleteUnverifiedOpacRegistrations
{
1261 my $dbh = C4
::Context
->dbh;
1263 DELETE FROM borrower_modifications
1264 WHERE borrowernumber
= 0 AND DATEDIFF
( NOW
(), timestamp
) > ?
|;
1265 my $cnt=$dbh->do($sql, undef, ($days) );
1266 return $cnt eq '0E0'?
0: $cnt;
1269 sub GetOverduesForPatron
{
1270 my ( $borrowernumber ) = @_;
1274 FROM issues, items, biblio, biblioitems
1275 WHERE items.itemnumber=issues.itemnumber
1276 AND biblio.biblionumber = items.biblionumber
1277 AND biblio.biblionumber = biblioitems.biblionumber
1278 AND issues.borrowernumber = ?
1279 AND date_due < NOW()
1282 my $sth = C4
::Context
->dbh->prepare( $sql );
1283 $sth->execute( $borrowernumber );
1285 return $sth->fetchall_arrayref({});
1288 END { } # module clean-up code here (global destructor)