Bug 18789: Use the patron variable in circulation.[pl|tt]
[koha.git] / C4 / Members.pm
blob90ebb54afec340c673c188577e914cc15f125850
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 &GetMemberAccountRecords
69 &GetBorrowersToExpunge
71 &IssueSlip
73 GetOverduesForPatron
76 #Modify data
77 push @EXPORT, qw(
78 &ModMember
79 &changepassword
82 #Insert data
83 push @EXPORT, qw(
84 &AddMember
85 &AddMember_Auto
86 &AddMember_Opac
89 #Check data
90 push @EXPORT, qw(
91 &checkuserpassword
92 &Check_Userid
93 &Generate_Userid
94 &fixup_cardnumber
95 &checkcardnumber
99 =head1 NAME
101 C4::Members - Perl Module containing convenience functions for member handling
103 =head1 SYNOPSIS
105 use C4::Members;
107 =head1 DESCRIPTION
109 This module contains routines for adding, modifying and deleting members/patrons/borrowers
111 =head1 FUNCTIONS
113 =head2 patronflags
115 $flags = &patronflags($patron);
117 This function is not exported.
119 The following will be set where applicable:
120 $flags->{CHARGES}->{amount} Amount of debt
121 $flags->{CHARGES}->{noissues} Set if debt amount >$5.00 (or syspref noissuescharge)
122 $flags->{CHARGES}->{message} Message -- deprecated
124 $flags->{CREDITS}->{amount} Amount of credit
125 $flags->{CREDITS}->{message} Message -- deprecated
127 $flags->{ GNA } Patron has no valid address
128 $flags->{ GNA }->{noissues} Set for each GNA
129 $flags->{ GNA }->{message} "Borrower has no valid address" -- deprecated
131 $flags->{ LOST } Patron's card reported lost
132 $flags->{ LOST }->{noissues} Set for each LOST
133 $flags->{ LOST }->{message} Message -- deprecated
135 $flags->{DBARRED} Set if patron debarred, no access
136 $flags->{DBARRED}->{noissues} Set for each DBARRED
137 $flags->{DBARRED}->{message} Message -- deprecated
139 $flags->{ NOTES }
140 $flags->{ NOTES }->{message} The note itself. NOT deprecated
142 $flags->{ ODUES } Set if patron has overdue books.
143 $flags->{ ODUES }->{message} "Yes" -- deprecated
144 $flags->{ ODUES }->{itemlist} ref-to-array: list of overdue books
145 $flags->{ ODUES }->{itemlisttext} Text list of overdue items -- deprecated
147 $flags->{WAITING} Set if any of patron's reserves are available
148 $flags->{WAITING}->{message} Message -- deprecated
149 $flags->{WAITING}->{itemlist} ref-to-array: list of available items
151 =over
153 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlist}> is a reference-to-array listing the
154 overdue items. Its elements are references-to-hash, each describing an
155 overdue item. The keys are selected fields from the issues, biblio,
156 biblioitems, and items tables of the Koha database.
158 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlisttext}> is a string giving a text listing of
159 the overdue items, one per line. Deprecated.
161 =item C<$flags-E<gt>{WAITING}-E<gt>{itemlist}> is a reference-to-array listing the
162 available items. Each element is a reference-to-hash whose keys are
163 fields from the reserves table of the Koha database.
165 =back
167 All the "message" fields that include language generated in this function are deprecated,
168 because such strings belong properly in the display layer.
170 The "message" field that comes from the DB is OK.
172 =cut
174 # TODO: use {anonymous => hashes} instead of a dozen %flaginfo
175 # FIXME rename this function.
176 sub patronflags {
177 my %flags;
178 my ( $patroninformation) = @_;
179 my $dbh=C4::Context->dbh;
180 my ($balance, $owing) = GetMemberAccountBalance( $patroninformation->{'borrowernumber'});
181 if ( $owing > 0 ) {
182 my %flaginfo;
183 my $noissuescharge = C4::Context->preference("noissuescharge") || 5;
184 $flaginfo{'message'} = sprintf 'Patron owes %.02f', $owing;
185 $flaginfo{'amount'} = sprintf "%.02f", $owing;
186 if ( $owing > $noissuescharge && !C4::Context->preference("AllowFineOverride") ) {
187 $flaginfo{'noissues'} = 1;
189 $flags{'CHARGES'} = \%flaginfo;
191 elsif ( $balance < 0 ) {
192 my %flaginfo;
193 $flaginfo{'message'} = sprintf 'Patron has credit of %.02f', -$balance;
194 $flaginfo{'amount'} = sprintf "%.02f", $balance;
195 $flags{'CREDITS'} = \%flaginfo;
198 # Check the debt of the guarntees of this patron
199 my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
200 $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
201 if ( defined $no_issues_charge_guarantees ) {
202 my $p = Koha::Patrons->find( $patroninformation->{borrowernumber} );
203 my @guarantees = $p->guarantees();
204 my $guarantees_non_issues_charges;
205 foreach my $g ( @guarantees ) {
206 my ( $b, $n, $o ) = C4::Members::GetMemberAccountBalance( $g->id );
207 $guarantees_non_issues_charges += $n;
210 if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees ) {
211 my %flaginfo;
212 $flaginfo{'message'} = sprintf 'patron guarantees owe %.02f', $guarantees_non_issues_charges;
213 $flaginfo{'amount'} = $guarantees_non_issues_charges;
214 $flaginfo{'noissues'} = 1 unless C4::Context->preference("allowfineoverride");
215 $flags{'CHARGES_GUARANTEES'} = \%flaginfo;
219 if ( $patroninformation->{'gonenoaddress'}
220 && $patroninformation->{'gonenoaddress'} == 1 )
222 my %flaginfo;
223 $flaginfo{'message'} = 'Borrower has no valid address.';
224 $flaginfo{'noissues'} = 1;
225 $flags{'GNA'} = \%flaginfo;
227 if ( $patroninformation->{'lost'} && $patroninformation->{'lost'} == 1 ) {
228 my %flaginfo;
229 $flaginfo{'message'} = 'Borrower\'s card reported lost.';
230 $flaginfo{'noissues'} = 1;
231 $flags{'LOST'} = \%flaginfo;
233 if ( $patroninformation->{'debarred'} && check_date( split( /-/, $patroninformation->{'debarred'} ) ) ) {
234 if ( Date_to_Days(Date::Calc::Today) < Date_to_Days( split( /-/, $patroninformation->{'debarred'} ) ) ) {
235 my %flaginfo;
236 $flaginfo{'debarredcomment'} = $patroninformation->{'debarredcomment'};
237 $flaginfo{'message'} = $patroninformation->{'debarredcomment'};
238 $flaginfo{'noissues'} = 1;
239 $flaginfo{'dateend'} = $patroninformation->{'debarred'};
240 $flags{'DBARRED'} = \%flaginfo;
243 if ( $patroninformation->{'borrowernotes'}
244 && $patroninformation->{'borrowernotes'} )
246 my %flaginfo;
247 $flaginfo{'message'} = $patroninformation->{'borrowernotes'};
248 $flags{'NOTES'} = \%flaginfo;
250 my ( $odues, $itemsoverdue ) = C4::Overdues::checkoverdues($patroninformation->{'borrowernumber'});
251 if ( $odues && $odues > 0 ) {
252 my %flaginfo;
253 $flaginfo{'message'} = "Yes";
254 $flaginfo{'itemlist'} = $itemsoverdue;
255 foreach ( sort { $a->{'date_due'} cmp $b->{'date_due'} }
256 @$itemsoverdue )
258 $flaginfo{'itemlisttext'} .=
259 "$_->{'date_due'} $_->{'barcode'} $_->{'title'} \n"; # newline is display layer
261 $flags{'ODUES'} = \%flaginfo;
264 my $patron = Koha::Patrons->find( $patroninformation->{borrowernumber} );
265 my $waiting_holds = $patron->holds->search({ found => 'W' });
266 my $nowaiting = $waiting_holds->count;
267 if ( $nowaiting > 0 ) {
268 my %flaginfo;
269 $flaginfo{'message'} = "Reserved items available";
270 $flaginfo{'itemlist'} = $waiting_holds->unblessed;
271 $flags{'WAITING'} = \%flaginfo;
273 return ( \%flags );
277 =head2 ModMember
279 my $success = ModMember(borrowernumber => $borrowernumber,
280 [ field => value ]... );
282 Modify borrower's data. All date fields should ALREADY be in ISO format.
284 return :
285 true on success, or false on failure
287 =cut
289 sub ModMember {
290 my (%data) = @_;
292 # trim whitespace from data which has some non-whitespace in it.
293 foreach my $field_name (keys(%data)) {
294 if ( defined $data{$field_name} && $data{$field_name} =~ /\S/ ) {
295 $data{$field_name} =~ s/^\s*|\s*$//g;
299 # test to know if you must update or not the borrower password
300 if (exists $data{password}) {
301 if ($data{password} eq '****' or $data{password} eq '') {
302 delete $data{password};
303 } else {
304 if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
305 # Update the hashed PIN in borrower_sync.hashed_pin, before Koha hashes it
306 Koha::NorwegianPatronDB::NLUpdateHashedPIN( $data{'borrowernumber'}, $data{password} );
308 $data{password} = hash_password($data{password});
312 my $old_categorycode = Koha::Patrons->find( $data{borrowernumber} )->categorycode;
314 # get only the columns of a borrower
315 my $schema = Koha::Database->new()->schema;
316 my @columns = $schema->source('Borrower')->columns;
317 my $new_borrower = { map { join(' ', @columns) =~ /$_/ ? ( $_ => $data{$_} ) : () } keys(%data) };
319 $new_borrower->{dateofbirth} ||= undef if exists $new_borrower->{dateofbirth};
320 $new_borrower->{dateenrolled} ||= undef if exists $new_borrower->{dateenrolled};
321 $new_borrower->{dateexpiry} ||= undef if exists $new_borrower->{dateexpiry};
322 $new_borrower->{debarred} ||= undef if exists $new_borrower->{debarred};
323 $new_borrower->{sms_provider_id} ||= undef if exists $new_borrower->{sms_provider_id};
324 $new_borrower->{guarantorid} ||= undef if exists $new_borrower->{guarantorid};
326 my $patron = Koha::Patrons->find( $new_borrower->{borrowernumber} );
328 delete $new_borrower->{userid} if exists $new_borrower->{userid} and not $new_borrower->{userid};
330 my $execute_success = $patron->store if $patron->set($new_borrower);
332 if ($execute_success) { # only proceed if the update was a success
333 # If the patron changes to a category with enrollment fee, we add a fee
334 if ( $data{categorycode} and $data{categorycode} ne $old_categorycode ) {
335 if ( C4::Context->preference('FeeOnChangePatronCategory') ) {
336 $patron->add_enrolment_fee_if_needed;
340 # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
341 # cronjob will use for syncing with NL
342 if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
343 my $borrowersync = Koha::Database->new->schema->resultset('BorrowerSync')->find({
344 'synctype' => 'norwegianpatrondb',
345 'borrowernumber' => $data{'borrowernumber'}
347 # Do not set to "edited" if syncstatus is "new". We need to sync as new before
348 # we can sync as changed. And the "new sync" will pick up all changes since
349 # the patron was created anyway.
350 if ( $borrowersync->syncstatus ne 'new' && $borrowersync->syncstatus ne 'delete' ) {
351 $borrowersync->update( { 'syncstatus' => 'edited' } );
353 # Set the value of 'sync'
354 $borrowersync->update( { 'sync' => $data{'sync'} } );
355 # Try to do the live sync
356 Koha::NorwegianPatronDB::NLSync({ 'borrowernumber' => $data{'borrowernumber'} });
359 logaction("MEMBERS", "MODIFY", $data{'borrowernumber'}, "UPDATE (executed w/ arg: $data{'borrowernumber'})") if C4::Context->preference("BorrowersLog");
361 return $execute_success;
364 =head2 AddMember
366 $borrowernumber = &AddMember(%borrower);
368 insert new borrower into table
370 (%borrower keys are database columns. Database columns could be
371 different in different versions. Please look into database for correct
372 column names.)
374 Returns the borrowernumber upon success
376 Returns as undef upon any db error without further processing
378 =cut
381 sub AddMember {
382 my (%data) = @_;
383 my $dbh = C4::Context->dbh;
384 my $schema = Koha::Database->new()->schema;
386 my $category = Koha::Patron::Categories->find( $data{categorycode} );
387 unless ($category) {
388 Koha::Exceptions::BadParameter->throw(
389 error => 'Invalid parameter passed',
390 parameter => 'categorycode'
394 # trim whitespace from data which has some non-whitespace in it.
395 foreach my $field_name (keys(%data)) {
396 if ( defined $data{$field_name} && $data{$field_name} =~ /\S/ ) {
397 $data{$field_name} =~ s/^\s*|\s*$//g;
401 # generate a proper login if none provided
402 $data{'userid'} = Generate_Userid( $data{'borrowernumber'}, $data{'firstname'}, $data{'surname'} )
403 if ( $data{'userid'} eq '' || !Check_Userid( $data{'userid'} ) );
405 # add expiration date if it isn't already there
406 $data{dateexpiry} ||= $category->get_expiry_date;
408 # add enrollment date if it isn't already there
409 unless ( $data{'dateenrolled'} ) {
410 $data{'dateenrolled'} = output_pref( { dt => dt_from_string, dateonly => 1, dateformat => 'iso' } );
413 if ( C4::Context->preference("autoMemberNum") ) {
414 if ( not exists $data{cardnumber} or not defined $data{cardnumber} or $data{cardnumber} eq '' ) {
415 $data{cardnumber} = fixup_cardnumber( $data{cardnumber} );
419 $data{'privacy'} =
420 $category->default_privacy() eq 'default' ? 1
421 : $category->default_privacy() eq 'never' ? 2
422 : $category->default_privacy() eq 'forever' ? 0
423 : undef;
425 $data{'privacy_guarantor_checkouts'} = 0 unless defined( $data{'privacy_guarantor_checkouts'} );
427 # Make a copy of the plain text password for later use
428 my $plain_text_password = $data{'password'};
430 # create a disabled account if no password provided
431 $data{'password'} = ($data{'password'})? hash_password($data{'password'}) : '!';
433 # we don't want invalid dates in the db (mysql has a bad habit of inserting 0000-00-00
434 $data{'dateofbirth'} = undef if ( not $data{'dateofbirth'} );
435 $data{'debarred'} = undef if ( not $data{'debarred'} );
436 $data{'sms_provider_id'} = undef if ( not $data{'sms_provider_id'} );
437 $data{'guarantorid'} = undef if ( not $data{'guarantorid'} );
439 # get only the columns of Borrower
440 # FIXME Do we really need this check?
441 my @columns = $schema->source('Borrower')->columns;
442 my $new_member = { map { join(' ',@columns) =~ /$_/ ? ( $_ => $data{$_} ) : () } keys(%data) } ;
444 delete $new_member->{borrowernumber};
446 my $patron = Koha::Patron->new( $new_member )->store;
447 $data{borrowernumber} = $patron->borrowernumber;
449 # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
450 # cronjob will use for syncing with NL
451 if ( exists $data{'borrowernumber'} && C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
452 Koha::Database->new->schema->resultset('BorrowerSync')->create({
453 'borrowernumber' => $data{'borrowernumber'},
454 'synctype' => 'norwegianpatrondb',
455 'sync' => 1,
456 'syncstatus' => 'new',
457 'hashed_pin' => Koha::NorwegianPatronDB::NLEncryptPIN( $plain_text_password ),
461 logaction("MEMBERS", "CREATE", $data{'borrowernumber'}, "") if C4::Context->preference("BorrowersLog");
463 $patron->add_enrolment_fee_if_needed;
465 return $data{borrowernumber};
468 =head2 Check_Userid
470 my $uniqueness = Check_Userid($userid,$borrowernumber);
472 $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 != '').
474 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.
476 return :
477 0 for not unique (i.e. this $userid already exists)
478 1 for unique (i.e. this $userid does not exist, or this $userid/$borrowernumber combination already exists)
480 =cut
482 sub Check_Userid {
483 my ( $uid, $borrowernumber ) = @_;
485 return 0 unless ($uid); # userid is a unique column, we should assume NULL is not unique
487 return 0 if ( $uid eq C4::Context->config('user') );
489 my $rs = Koha::Database->new()->schema()->resultset('Borrower');
491 my $params;
492 $params->{userid} = $uid;
493 $params->{borrowernumber} = { '!=' => $borrowernumber } if ($borrowernumber);
495 my $count = $rs->count( $params );
497 return $count ? 0 : 1;
500 =head2 Generate_Userid
502 my $newuid = Generate_Userid($borrowernumber, $firstname, $surname);
504 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
506 $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.
508 return :
509 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 =cut
513 sub Generate_Userid {
514 my ($borrowernumber, $firstname, $surname) = @_;
515 my $newuid;
516 my $offset = 0;
517 #The script will "do" the following code and increment the $offset until Check_Userid = 1 (i.e. until $newuid comes back as unique)
518 do {
519 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
520 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
521 $newuid = lc(($firstname)? "$firstname.$surname" : $surname);
522 $newuid = unac_string('utf-8',$newuid);
523 $newuid .= $offset unless $offset == 0;
524 $offset++;
526 } while (!Check_Userid($newuid,$borrowernumber));
528 return $newuid;
531 =head2 fixup_cardnumber
533 Warning: The caller is responsible for locking the members table in write
534 mode, to avoid database corruption.
536 =cut
538 use vars qw( @weightings );
539 my @weightings = ( 8, 4, 6, 3, 5, 2, 1 );
541 sub fixup_cardnumber {
542 my ($cardnumber) = @_;
543 my $autonumber_members = C4::Context->boolean_preference('autoMemberNum') || 0;
545 # Find out whether member numbers should be generated
546 # automatically. Should be either "1" or something else.
547 # Defaults to "0", which is interpreted as "no".
549 # if ($cardnumber !~ /\S/ && $autonumber_members) {
550 ($autonumber_members) or return $cardnumber;
551 my $checkdigit = C4::Context->preference('checkdigit');
552 my $dbh = C4::Context->dbh;
553 if ( $checkdigit and $checkdigit eq 'katipo' ) {
555 # if checkdigit is selected, calculate katipo-style cardnumber.
556 # otherwise, just use the max()
557 # purpose: generate checksum'd member numbers.
558 # We'll assume we just got the max value of digits 2-8 of member #'s
559 # from the database and our job is to increment that by one,
560 # determine the 1st and 9th digits and return the full string.
561 my $sth = $dbh->prepare(
562 "select max(substring(borrowers.cardnumber,2,7)) as new_num from borrowers"
564 $sth->execute;
565 my $data = $sth->fetchrow_hashref;
566 $cardnumber = $data->{new_num};
567 if ( !$cardnumber ) { # If DB has no values,
568 $cardnumber = 1000000; # start at 1000000
569 } else {
570 $cardnumber += 1;
573 my $sum = 0;
574 for ( my $i = 0 ; $i < 8 ; $i += 1 ) {
575 # read weightings, left to right, 1 char at a time
576 my $temp1 = $weightings[$i];
578 # sequence left to right, 1 char at a time
579 my $temp2 = substr( $cardnumber, $i, 1 );
581 # mult each char 1-7 by its corresponding weighting
582 $sum += $temp1 * $temp2;
585 my $rem = ( $sum % 11 );
586 $rem = 'X' if $rem == 10;
588 return "V$cardnumber$rem";
589 } else {
591 my $sth = $dbh->prepare(
592 'SELECT MAX( CAST( cardnumber AS SIGNED ) ) FROM borrowers WHERE cardnumber REGEXP "^-?[0-9]+$"'
594 $sth->execute;
595 my ($result) = $sth->fetchrow;
596 return $result + 1;
598 return $cardnumber; # just here as a fallback/reminder
601 =head2 GetPendingIssues
603 my $issues = &GetPendingIssues(@borrowernumber);
605 Looks up what the patron with the given borrowernumber has borrowed.
607 C<&GetPendingIssues> returns a
608 reference-to-array where each element is a reference-to-hash; the
609 keys are the fields from the C<issues>, C<biblio>, and C<items> tables.
610 The keys include C<biblioitems> fields.
612 =cut
614 sub GetPendingIssues {
615 my @borrowernumbers = @_;
617 unless (@borrowernumbers ) { # return a ref_to_array
618 return \@borrowernumbers; # to not cause surprise to caller
621 # Borrowers part of the query
622 my $bquery = '';
623 for (my $i = 0; $i < @borrowernumbers; $i++) {
624 $bquery .= ' issues.borrowernumber = ?';
625 if ($i < $#borrowernumbers ) {
626 $bquery .= ' OR';
630 # FIXME: namespace collision: each table has "timestamp" fields. Which one is "timestamp" ?
631 # FIXME: circ/ciculation.pl tries to sort by timestamp!
632 # FIXME: namespace collision: other collisions possible.
633 # FIXME: most of this data isn't really being used by callers.
634 my $query =
635 "SELECT issues.*,
636 items.*,
637 biblio.*,
638 biblioitems.volume,
639 biblioitems.number,
640 biblioitems.itemtype,
641 biblioitems.isbn,
642 biblioitems.issn,
643 biblioitems.publicationyear,
644 biblioitems.publishercode,
645 biblioitems.volumedate,
646 biblioitems.volumedesc,
647 biblioitems.lccn,
648 biblioitems.url,
649 borrowers.firstname,
650 borrowers.surname,
651 borrowers.cardnumber,
652 issues.timestamp AS timestamp,
653 issues.renewals AS renewals,
654 issues.borrowernumber AS borrowernumber,
655 items.renewals AS totalrenewals
656 FROM issues
657 LEFT JOIN items ON items.itemnumber = issues.itemnumber
658 LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
659 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
660 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
661 WHERE
662 $bquery
663 ORDER BY issues.issuedate"
666 my $sth = C4::Context->dbh->prepare($query);
667 $sth->execute(@borrowernumbers);
668 my $data = $sth->fetchall_arrayref({});
669 my $today = dt_from_string;
670 foreach (@{$data}) {
671 if ($_->{issuedate}) {
672 $_->{issuedate} = dt_from_string($_->{issuedate}, 'sql');
674 $_->{date_due_sql} = $_->{date_due};
675 # FIXME no need to have this value
676 $_->{date_due} or next;
677 $_->{date_due_sql} = $_->{date_due};
678 # FIXME no need to have this value
679 $_->{date_due} = dt_from_string($_->{date_due}, 'sql');
680 if ( DateTime->compare($_->{date_due}, $today) == -1 ) {
681 $_->{overdue} = 1;
684 return $data;
687 =head2 GetAllIssues
689 $issues = &GetAllIssues($borrowernumber, $sortkey, $limit);
691 Looks up what the patron with the given borrowernumber has borrowed,
692 and sorts the results.
694 C<$sortkey> is the name of a field on which to sort the results. This
695 should be the name of a field in the C<issues>, C<biblio>,
696 C<biblioitems>, or C<items> table in the Koha database.
698 C<$limit> is the maximum number of results to return.
700 C<&GetAllIssues> an arrayref, C<$issues>, of hashrefs, the keys of which
701 are the fields from the C<issues>, C<biblio>, C<biblioitems>, and
702 C<items> tables of the Koha database.
704 =cut
707 sub GetAllIssues {
708 my ( $borrowernumber, $order, $limit ) = @_;
710 return unless $borrowernumber;
711 $order = 'date_due desc' unless $order;
713 my $dbh = C4::Context->dbh;
714 my $query =
715 'SELECT *, issues.timestamp as issuestimestamp, issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
716 FROM issues
717 LEFT JOIN items on items.itemnumber=issues.itemnumber
718 LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
719 LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
720 WHERE borrowernumber=?
721 UNION ALL
722 SELECT *, old_issues.timestamp as issuestimestamp, old_issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
723 FROM old_issues
724 LEFT JOIN items on items.itemnumber=old_issues.itemnumber
725 LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
726 LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
727 WHERE borrowernumber=? AND old_issues.itemnumber IS NOT NULL
728 order by ' . $order;
729 if ($limit) {
730 $query .= " limit $limit";
733 my $sth = $dbh->prepare($query);
734 $sth->execute( $borrowernumber, $borrowernumber );
735 return $sth->fetchall_arrayref( {} );
739 =head2 GetMemberAccountRecords
741 ($total, $acctlines, $count) = &GetMemberAccountRecords($borrowernumber);
743 Looks up accounting data for the patron with the given borrowernumber.
745 C<&GetMemberAccountRecords> returns a three-element array. C<$acctlines> is a
746 reference-to-array, where each element is a reference-to-hash; the
747 keys are the fields of the C<accountlines> table in the Koha database.
748 C<$count> is the number of elements in C<$acctlines>. C<$total> is the
749 total amount outstanding for all of the account lines.
751 =cut
753 sub GetMemberAccountRecords {
754 my ($borrowernumber) = @_;
755 my $dbh = C4::Context->dbh;
756 my @acctlines;
757 my $numlines = 0;
758 my $strsth = qq(
759 SELECT *
760 FROM accountlines
761 WHERE borrowernumber=?);
762 $strsth.=" ORDER BY accountlines_id desc";
763 my $sth= $dbh->prepare( $strsth );
764 $sth->execute( $borrowernumber );
766 my $total = 0;
767 while ( my $data = $sth->fetchrow_hashref ) {
768 if ( $data->{itemnumber} ) {
769 my $item = Koha::Items->find( $data->{itemnumber} );
770 my $biblio = $item->biblio;
771 $data->{biblionumber} = $biblio->biblionumber;
772 $data->{title} = $biblio->title;
774 $acctlines[$numlines] = $data;
775 $numlines++;
776 $total += sprintf "%.0f", 1000*$data->{amountoutstanding}; # convert float to integer to avoid round-off errors
778 $total /= 1000;
779 return ( $total, \@acctlines,$numlines);
782 =head2 GetMemberAccountBalance
784 ($total_balance, $non_issue_balance, $other_charges) = &GetMemberAccountBalance($borrowernumber);
786 Calculates amount immediately owing by the patron - non-issue charges.
787 Based on GetMemberAccountRecords.
788 Charges exempt from non-issue are:
789 * Res (reserves)
790 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
791 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
793 =cut
795 sub GetMemberAccountBalance {
796 my ($borrowernumber) = @_;
798 my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
800 my @not_fines;
801 push @not_fines, 'Res' unless C4::Context->preference('HoldsInNoissuesCharge');
802 push @not_fines, 'Rent' unless C4::Context->preference('RentalsInNoissuesCharge');
803 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
804 my $dbh = C4::Context->dbh;
805 my $man_inv_types = $dbh->selectcol_arrayref(qq{SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'});
806 push @not_fines, map substr($_, 0, $ACCOUNT_TYPE_LENGTH), @$man_inv_types;
808 my %not_fine = map {$_ => 1} @not_fines;
810 my ($total, $acctlines) = GetMemberAccountRecords($borrowernumber);
811 my $other_charges = 0;
812 foreach (@$acctlines) {
813 $other_charges += $_->{amountoutstanding} if $not_fine{ substr($_->{accounttype}, 0, $ACCOUNT_TYPE_LENGTH) };
816 return ( $total, $total - $other_charges, $other_charges);
819 sub checkcardnumber {
820 my ( $cardnumber, $borrowernumber ) = @_;
822 # If cardnumber is null, we assume they're allowed.
823 return 0 unless defined $cardnumber;
825 my $dbh = C4::Context->dbh;
826 my $query = "SELECT * FROM borrowers WHERE cardnumber=?";
827 $query .= " AND borrowernumber <> ?" if ($borrowernumber);
828 my $sth = $dbh->prepare($query);
829 $sth->execute(
830 $cardnumber,
831 ( $borrowernumber ? $borrowernumber : () )
834 return 1 if $sth->fetchrow_hashref;
836 my ( $min_length, $max_length ) = get_cardnumber_length();
837 return 2
838 if length $cardnumber > $max_length
839 or length $cardnumber < $min_length;
841 return 0;
844 =head2 get_cardnumber_length
846 my ($min, $max) = C4::Members::get_cardnumber_length()
848 Returns the minimum and maximum length for patron cardnumbers as
849 determined by the CardnumberLength system preference, the
850 BorrowerMandatoryField system preference, and the width of the
851 database column.
853 =cut
855 sub get_cardnumber_length {
856 my $borrower = Koha::Schema->resultset('Borrower');
857 my $field_size = $borrower->result_source->column_info('cardnumber')->{size};
858 my ( $min, $max ) = ( 0, $field_size ); # borrowers.cardnumber is a nullable varchar(20)
859 $min = 1 if C4::Context->preference('BorrowerMandatoryField') =~ /cardnumber/;
860 if ( my $cardnumber_length = C4::Context->preference('CardnumberLength') ) {
861 # Is integer and length match
862 if ( $cardnumber_length =~ m|^\d+$| ) {
863 $min = $max = $cardnumber_length
864 if $cardnumber_length >= $min
865 and $cardnumber_length <= $max;
867 # Else assuming it is a range
868 elsif ( $cardnumber_length =~ m|(\d*),(\d*)| ) {
869 $min = $1 if $1 and $min < $1;
870 $max = $2 if $2 and $max > $2;
874 $min = $max if $min > $max;
875 return ( $min, $max );
878 =head2 GetBorrowersToExpunge
880 $borrowers = &GetBorrowersToExpunge(
881 not_borrowed_since => $not_borrowed_since,
882 expired_before => $expired_before,
883 category_code => $category_code,
884 patron_list_id => $patron_list_id,
885 branchcode => $branchcode
888 This function get all borrowers based on the given criteria.
890 =cut
892 sub GetBorrowersToExpunge {
894 my $params = shift;
895 my $filterdate = $params->{'not_borrowed_since'};
896 my $filterexpiry = $params->{'expired_before'};
897 my $filterlastseen = $params->{'last_seen'};
898 my $filtercategory = $params->{'category_code'};
899 my $filterbranch = $params->{'branchcode'} ||
900 ((C4::Context->preference('IndependentBranches')
901 && C4::Context->userenv
902 && !C4::Context->IsSuperLibrarian()
903 && C4::Context->userenv->{branch})
904 ? C4::Context->userenv->{branch}
905 : "");
906 my $filterpatronlist = $params->{'patron_list_id'};
908 my $dbh = C4::Context->dbh;
909 my $query = q|
910 SELECT *
911 FROM (
912 SELECT borrowers.borrowernumber,
913 MAX(old_issues.timestamp) AS latestissue,
914 MAX(issues.timestamp) AS currentissue
915 FROM borrowers
916 JOIN categories USING (categorycode)
917 LEFT JOIN (
918 SELECT guarantorid
919 FROM borrowers
920 WHERE guarantorid IS NOT NULL
921 AND guarantorid <> 0
922 ) as tmp ON borrowers.borrowernumber=tmp.guarantorid
923 LEFT JOIN old_issues USING (borrowernumber)
924 LEFT JOIN issues USING (borrowernumber)|;
925 if ( $filterpatronlist ){
926 $query .= q| LEFT JOIN patron_list_patrons USING (borrowernumber)|;
928 $query .= q| WHERE category_type <> 'S'
929 AND tmp.guarantorid IS NULL
931 my @query_params;
932 if ( $filterbranch && $filterbranch ne "" ) {
933 $query.= " AND borrowers.branchcode = ? ";
934 push( @query_params, $filterbranch );
936 if ( $filterexpiry ) {
937 $query .= " AND dateexpiry < ? ";
938 push( @query_params, $filterexpiry );
940 if ( $filterlastseen ) {
941 $query .= ' AND lastseen < ? ';
942 push @query_params, $filterlastseen;
944 if ( $filtercategory ) {
945 $query .= " AND categorycode = ? ";
946 push( @query_params, $filtercategory );
948 if ( $filterpatronlist ){
949 $query.=" AND patron_list_id = ? ";
950 push( @query_params, $filterpatronlist );
952 $query .= " GROUP BY borrowers.borrowernumber";
953 $query .= q|
954 ) xxx WHERE currentissue IS NULL|;
955 if ( $filterdate ) {
956 $query.=" AND ( latestissue < ? OR latestissue IS NULL ) ";
957 push @query_params,$filterdate;
960 warn $query if $debug;
962 my $sth = $dbh->prepare($query);
963 if (scalar(@query_params)>0){
964 $sth->execute(@query_params);
966 else {
967 $sth->execute;
970 my @results;
971 while ( my $data = $sth->fetchrow_hashref ) {
972 push @results, $data;
974 return \@results;
977 =head2 IssueSlip
979 IssueSlip($branchcode, $borrowernumber, $quickslip)
981 Returns letter hash ( see C4::Letters::GetPreparedLetter )
983 $quickslip is boolean, to indicate whether we want a quick slip
985 IssueSlip populates ISSUESLIP and ISSUEQSLIP, and will make the following expansions:
987 Both slips:
989 <<branches.*>>
990 <<borrowers.*>>
992 ISSUESLIP:
994 <checkedout>
995 <<biblio.*>>
996 <<items.*>>
997 <<biblioitems.*>>
998 <<issues.*>>
999 </checkedout>
1001 <overdue>
1002 <<biblio.*>>
1003 <<items.*>>
1004 <<biblioitems.*>>
1005 <<issues.*>>
1006 </overdue>
1008 <news>
1009 <<opac_news.*>>
1010 </news>
1012 ISSUEQSLIP:
1014 <checkedout>
1015 <<biblio.*>>
1016 <<items.*>>
1017 <<biblioitems.*>>
1018 <<issues.*>>
1019 </checkedout>
1021 NOTE: Not all table fields are available, pleasee see GetPendingIssues for a list of available fields.
1023 =cut
1025 sub IssueSlip {
1026 my ($branch, $borrowernumber, $quickslip) = @_;
1028 # FIXME Check callers before removing this statement
1029 #return unless $borrowernumber;
1031 my $patron = Koha::Patrons->find( $borrowernumber );
1032 return unless $patron;
1034 my @issues = @{ GetPendingIssues($borrowernumber) };
1036 for my $issue (@issues) {
1037 $issue->{date_due} = $issue->{date_due_sql};
1038 if ($quickslip) {
1039 my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
1040 if ( substr( $issue->{issuedate}, 0, 10 ) eq $today
1041 or substr( $issue->{lastreneweddate}, 0, 10 ) eq $today ) {
1042 $issue->{now} = 1;
1047 # Sort on timestamp then on issuedate then on issue_id
1048 # useful for tests and could be if modified in a batch
1049 @issues = sort {
1050 $b->{timestamp} <=> $a->{timestamp}
1051 or $b->{issuedate} <=> $a->{issuedate}
1052 or $b->{issue_id} <=> $a->{issue_id}
1053 } @issues;
1055 my ($letter_code, %repeat, %loops);
1056 if ( $quickslip ) {
1057 $letter_code = 'ISSUEQSLIP';
1058 my @checkouts = map {
1059 'biblio' => $_,
1060 'items' => $_,
1061 'biblioitems' => $_,
1062 'issues' => $_,
1063 }, grep { $_->{'now'} } @issues;
1064 %repeat = (
1065 checkedout => \@checkouts, # History syntax
1067 %loops = (
1068 issues => [ map { $_->{issues}{itemnumber} } @checkouts ], # TT syntax
1071 else {
1072 my @checkouts = map {
1073 'biblio' => $_,
1074 'items' => $_,
1075 'biblioitems' => $_,
1076 'issues' => $_,
1077 }, grep { !$_->{'overdue'} } @issues;
1078 my @overdues = map {
1079 'biblio' => $_,
1080 'items' => $_,
1081 'biblioitems' => $_,
1082 'issues' => $_,
1083 }, grep { $_->{'overdue'} } @issues;
1084 my $news = GetNewsToDisplay( "slip", $branch );
1085 my @news = map {
1086 $_->{'timestamp'} = $_->{'newdate'};
1087 { opac_news => $_ }
1088 } @$news;
1089 $letter_code = 'ISSUESLIP';
1090 %repeat = (
1091 checkedout => \@checkouts,
1092 overdue => \@overdues,
1093 news => \@news,
1095 %loops = (
1096 issues => [ map { $_->{issues}{itemnumber} } @checkouts ],
1097 overdues => [ map { $_->{issues}{itemnumber} } @overdues ],
1098 opac_news => [ map { $_->{opac_news}{idnew} } @news ],
1102 return C4::Letters::GetPreparedLetter (
1103 module => 'circulation',
1104 letter_code => $letter_code,
1105 branchcode => $branch,
1106 lang => $patron->lang,
1107 tables => {
1108 'branches' => $branch,
1109 'borrowers' => $borrowernumber,
1111 repeat => \%repeat,
1112 loops => \%loops,
1116 =head2 AddMember_Auto
1118 =cut
1120 sub AddMember_Auto {
1121 my ( %borrower ) = @_;
1123 $borrower{'cardnumber'} ||= fixup_cardnumber();
1125 $borrower{'borrowernumber'} = AddMember(%borrower);
1127 return ( %borrower );
1130 =head2 AddMember_Opac
1132 =cut
1134 sub AddMember_Opac {
1135 my ( %borrower ) = @_;
1137 $borrower{'categorycode'} //= C4::Context->preference('PatronSelfRegistrationDefaultCategory');
1138 if (not defined $borrower{'password'}){
1139 my $sr = new String::Random;
1140 $sr->{'A'} = [ 'A'..'Z', 'a'..'z' ];
1141 my $password = $sr->randpattern("AAAAAAAAAA");
1142 $borrower{'password'} = $password;
1145 %borrower = AddMember_Auto(%borrower);
1147 return ( $borrower{'borrowernumber'}, $borrower{'password'} );
1150 =head2 DeleteExpiredOpacRegistrations
1152 Delete accounts that haven't been upgraded from the 'temporary' category
1153 Returns the number of removed patrons
1155 =cut
1157 sub DeleteExpiredOpacRegistrations {
1159 my $delay = C4::Context->preference('PatronSelfRegistrationExpireTemporaryAccountsDelay');
1160 my $category_code = C4::Context->preference('PatronSelfRegistrationDefaultCategory');
1162 return 0 if not $category_code or not defined $delay or $delay eq q||;
1164 my $query = qq|
1165 SELECT borrowernumber
1166 FROM borrowers
1167 WHERE categorycode = ? AND DATEDIFF( NOW(), dateenrolled ) > ? |;
1169 my $dbh = C4::Context->dbh;
1170 my $sth = $dbh->prepare($query);
1171 $sth->execute( $category_code, $delay );
1172 my $cnt=0;
1173 while ( my ($borrowernumber) = $sth->fetchrow_array() ) {
1174 Koha::Patrons->find($borrowernumber)->delete;
1175 $cnt++;
1177 return $cnt;
1180 =head2 DeleteUnverifiedOpacRegistrations
1182 Delete all unverified self registrations in borrower_modifications,
1183 older than the specified number of days.
1185 =cut
1187 sub DeleteUnverifiedOpacRegistrations {
1188 my ( $days ) = @_;
1189 my $dbh = C4::Context->dbh;
1190 my $sql=qq|
1191 DELETE FROM borrower_modifications
1192 WHERE borrowernumber = 0 AND DATEDIFF( NOW(), timestamp ) > ?|;
1193 my $cnt=$dbh->do($sql, undef, ($days) );
1194 return $cnt eq '0E0'? 0: $cnt;
1197 sub GetOverduesForPatron {
1198 my ( $borrowernumber ) = @_;
1200 my $sql = "
1201 SELECT *
1202 FROM issues, items, biblio, biblioitems
1203 WHERE items.itemnumber=issues.itemnumber
1204 AND biblio.biblionumber = items.biblionumber
1205 AND biblio.biblionumber = biblioitems.biblionumber
1206 AND issues.borrowernumber = ?
1207 AND date_due < NOW()
1210 my $sth = C4::Context->dbh->prepare( $sql );
1211 $sth->execute( $borrowernumber );
1213 return $sth->fetchall_arrayref({});
1216 END { } # module clean-up code here (global destructor)
1220 __END__
1222 =head1 AUTHOR
1224 Koha Team
1226 =cut