Bug 20268: CSS regression: white gap on the top of the staff pages
[koha.git] / C4 / Members.pm
blob58f6e8a3de617af2bd184d4395edbbf4bacd769a
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 List::MoreUtils qw( uniq );
30 use C4::Log; # logaction
31 use C4::Overdues;
32 use C4::Reserves;
33 use C4::Accounts;
34 use C4::Biblio;
35 use C4::Letters;
36 use C4::Members::Attributes qw(SearchIdMatchingAttribute UpdateBorrowerAttribute);
37 use C4::NewsChannels; #get slip news
38 use DateTime;
39 use Koha::Database;
40 use Koha::DateUtils;
41 use Text::Unaccent qw( unac_string );
42 use Koha::AuthUtils qw(hash_password);
43 use Koha::Database;
44 use Koha::Holds;
45 use Koha::List::Patron;
46 use Koha::Patrons;
47 use Koha::Patron::Categories;
48 use Koha::Schema;
50 our (@ISA,@EXPORT,@EXPORT_OK,$debug);
52 use Module::Load::Conditional qw( can_load );
53 if ( ! can_load( modules => { 'Koha::NorwegianPatronDB' => undef } ) ) {
54 $debug && warn "Unable to load Koha::NorwegianPatronDB";
58 BEGIN {
59 $debug = $ENV{DEBUG} || 0;
60 require Exporter;
61 @ISA = qw(Exporter);
62 #Get data
63 push @EXPORT, qw(
65 &GetPendingIssues
66 &GetAllIssues
68 &GetBorrowersToExpunge
70 &IssueSlip
72 GetOverduesForPatron
75 #Modify data
76 push @EXPORT, qw(
77 &ModMember
78 &changepassword
81 #Insert data
82 push @EXPORT, qw(
83 &AddMember
84 &AddMember_Auto
85 &AddMember_Opac
88 #Check data
89 push @EXPORT, qw(
90 &checkuserpassword
91 &Check_Userid
92 &Generate_Userid
93 &fixup_cardnumber
94 &checkcardnumber
98 =head1 NAME
100 C4::Members - Perl Module containing convenience functions for member handling
102 =head1 SYNOPSIS
104 use C4::Members;
106 =head1 DESCRIPTION
108 This module contains routines for adding, modifying and deleting members/patrons/borrowers
110 =head1 FUNCTIONS
112 =head2 patronflags
114 $flags = &patronflags($patron);
116 This function is not exported.
118 The following will be set where applicable:
119 $flags->{CHARGES}->{amount} Amount of debt
120 $flags->{CHARGES}->{noissues} Set if debt amount >$5.00 (or syspref noissuescharge)
121 $flags->{CHARGES}->{message} Message -- deprecated
123 $flags->{CREDITS}->{amount} Amount of credit
124 $flags->{CREDITS}->{message} Message -- deprecated
126 $flags->{ GNA } Patron has no valid address
127 $flags->{ GNA }->{noissues} Set for each GNA
128 $flags->{ GNA }->{message} "Borrower has no valid address" -- deprecated
130 $flags->{ LOST } Patron's card reported lost
131 $flags->{ LOST }->{noissues} Set for each LOST
132 $flags->{ LOST }->{message} Message -- deprecated
134 $flags->{DBARRED} Set if patron debarred, no access
135 $flags->{DBARRED}->{noissues} Set for each DBARRED
136 $flags->{DBARRED}->{message} Message -- deprecated
138 $flags->{ NOTES }
139 $flags->{ NOTES }->{message} The note itself. NOT deprecated
141 $flags->{ ODUES } Set if patron has overdue books.
142 $flags->{ ODUES }->{message} "Yes" -- deprecated
143 $flags->{ ODUES }->{itemlist} ref-to-array: list of overdue books
144 $flags->{ ODUES }->{itemlisttext} Text list of overdue items -- deprecated
146 $flags->{WAITING} Set if any of patron's reserves are available
147 $flags->{WAITING}->{message} Message -- deprecated
148 $flags->{WAITING}->{itemlist} ref-to-array: list of available items
150 =over
152 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlist}> is a reference-to-array listing the
153 overdue items. Its elements are references-to-hash, each describing an
154 overdue item. The keys are selected fields from the issues, biblio,
155 biblioitems, and items tables of the Koha database.
157 =item C<$flags-E<gt>{ODUES}-E<gt>{itemlisttext}> is a string giving a text listing of
158 the overdue items, one per line. Deprecated.
160 =item C<$flags-E<gt>{WAITING}-E<gt>{itemlist}> is a reference-to-array listing the
161 available items. Each element is a reference-to-hash whose keys are
162 fields from the reserves table of the Koha database.
164 =back
166 All the "message" fields that include language generated in this function are deprecated,
167 because such strings belong properly in the display layer.
169 The "message" field that comes from the DB is OK.
171 =cut
173 # TODO: use {anonymous => hashes} instead of a dozen %flaginfo
174 # FIXME rename this function.
175 sub patronflags {
176 my %flags;
177 my ( $patroninformation) = @_;
178 my $dbh=C4::Context->dbh;
179 my $patron = Koha::Patrons->find( $patroninformation->{borrowernumber} );
180 my $account = $patron->account;
181 my $owing = $account->non_issues_charges;
182 if ( $owing > 0 ) {
183 my %flaginfo;
184 my $noissuescharge = C4::Context->preference("noissuescharge") || 5;
185 $flaginfo{'message'} = sprintf 'Patron owes %.02f', $owing;
186 $flaginfo{'amount'} = sprintf "%.02f", $owing;
187 if ( $owing > $noissuescharge && !C4::Context->preference("AllowFineOverride") ) {
188 $flaginfo{'noissues'} = 1;
190 $flags{'CHARGES'} = \%flaginfo;
192 elsif ( ( my $balance = $account->balance ) < 0 ) {
193 my %flaginfo;
194 $flaginfo{'message'} = sprintf 'Patron has credit of %.02f', -$balance;
195 $flaginfo{'amount'} = sprintf "%.02f", $balance;
196 $flags{'CREDITS'} = \%flaginfo;
199 # Check the debt of the guarntees of this patron
200 my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
201 $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
202 if ( defined $no_issues_charge_guarantees ) {
203 my $p = Koha::Patrons->find( $patroninformation->{borrowernumber} );
204 my @guarantees = $p->guarantees();
205 my $guarantees_non_issues_charges;
206 foreach my $g ( @guarantees ) {
207 $guarantees_non_issues_charges += $g->account->non_issues_charges;
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 $waiting_holds = $patron->holds->search({ found => 'W' });
265 my $nowaiting = $waiting_holds->count;
266 if ( $nowaiting > 0 ) {
267 my %flaginfo;
268 $flaginfo{'message'} = "Reserved items available";
269 $flaginfo{'itemlist'} = $waiting_holds->unblessed;
270 $flags{'WAITING'} = \%flaginfo;
272 return ( \%flags );
276 =head2 ModMember
278 my $success = ModMember(borrowernumber => $borrowernumber,
279 [ field => value ]... );
281 Modify borrower's data. All date fields should ALREADY be in ISO format.
283 return :
284 true on success, or false on failure
286 =cut
288 sub ModMember {
289 my (%data) = @_;
291 # trim whitespace from data which has some non-whitespace in it.
292 foreach my $field_name (keys(%data)) {
293 if ( defined $data{$field_name} && $data{$field_name} =~ /\S/ ) {
294 $data{$field_name} =~ s/^\s*|\s*$//g;
298 # test to know if you must update or not the borrower password
299 if (exists $data{password}) {
300 if ($data{password} eq '****' or $data{password} eq '') {
301 delete $data{password};
302 } else {
303 if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
304 # Update the hashed PIN in borrower_sync.hashed_pin, before Koha hashes it
305 Koha::NorwegianPatronDB::NLUpdateHashedPIN( $data{'borrowernumber'}, $data{password} );
307 $data{password} = hash_password($data{password});
311 my $old_categorycode = Koha::Patrons->find( $data{borrowernumber} )->categorycode;
313 # get only the columns of a borrower
314 my $schema = Koha::Database->new()->schema;
315 my @columns = $schema->source('Borrower')->columns;
316 my $new_borrower = { map { join(' ', @columns) =~ /$_/ ? ( $_ => $data{$_} ) : () } keys(%data) };
318 $new_borrower->{dateofbirth} ||= undef if exists $new_borrower->{dateofbirth};
319 $new_borrower->{dateenrolled} ||= undef if exists $new_borrower->{dateenrolled};
320 $new_borrower->{dateexpiry} ||= undef if exists $new_borrower->{dateexpiry};
321 $new_borrower->{debarred} ||= undef if exists $new_borrower->{debarred};
322 $new_borrower->{sms_provider_id} ||= undef if exists $new_borrower->{sms_provider_id};
323 $new_borrower->{guarantorid} ||= undef if exists $new_borrower->{guarantorid};
325 my $patron = Koha::Patrons->find( $new_borrower->{borrowernumber} );
327 delete $new_borrower->{userid} if exists $new_borrower->{userid} and not $new_borrower->{userid};
329 my $execute_success = $patron->store if $patron->set($new_borrower);
331 if ($execute_success) { # only proceed if the update was a success
332 # If the patron changes to a category with enrollment fee, we add a fee
333 if ( $data{categorycode} and $data{categorycode} ne $old_categorycode ) {
334 if ( C4::Context->preference('FeeOnChangePatronCategory') ) {
335 $patron->add_enrolment_fee_if_needed;
339 # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
340 # cronjob will use for syncing with NL
341 if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
342 my $borrowersync = Koha::Database->new->schema->resultset('BorrowerSync')->find({
343 'synctype' => 'norwegianpatrondb',
344 'borrowernumber' => $data{'borrowernumber'}
346 # Do not set to "edited" if syncstatus is "new". We need to sync as new before
347 # we can sync as changed. And the "new sync" will pick up all changes since
348 # the patron was created anyway.
349 if ( $borrowersync->syncstatus ne 'new' && $borrowersync->syncstatus ne 'delete' ) {
350 $borrowersync->update( { 'syncstatus' => 'edited' } );
352 # Set the value of 'sync'
353 $borrowersync->update( { 'sync' => $data{'sync'} } );
354 # Try to do the live sync
355 Koha::NorwegianPatronDB::NLSync({ 'borrowernumber' => $data{'borrowernumber'} });
358 logaction("MEMBERS", "MODIFY", $data{'borrowernumber'}, "UPDATE (executed w/ arg: $data{'borrowernumber'})") if C4::Context->preference("BorrowersLog");
360 return $execute_success;
363 =head2 AddMember
365 $borrowernumber = &AddMember(%borrower);
367 insert new borrower into table
369 (%borrower keys are database columns. Database columns could be
370 different in different versions. Please look into database for correct
371 column names.)
373 Returns the borrowernumber upon success
375 Returns as undef upon any db error without further processing
377 =cut
380 sub AddMember {
381 my (%data) = @_;
382 my $dbh = C4::Context->dbh;
383 my $schema = Koha::Database->new()->schema;
385 my $category = Koha::Patron::Categories->find( $data{categorycode} );
386 unless ($category) {
387 Koha::Exceptions::BadParameter->throw(
388 error => 'Invalid parameter passed',
389 parameter => 'categorycode'
393 # trim whitespace from data which has some non-whitespace in it.
394 foreach my $field_name (keys(%data)) {
395 if ( defined $data{$field_name} && $data{$field_name} =~ /\S/ ) {
396 $data{$field_name} =~ s/^\s*|\s*$//g;
400 # generate a proper login if none provided
401 $data{'userid'} = Generate_Userid( $data{'borrowernumber'}, $data{'firstname'}, $data{'surname'} )
402 if ( $data{'userid'} eq '' || !Check_Userid( $data{'userid'} ) );
404 # add expiration date if it isn't already there
405 $data{dateexpiry} ||= $category->get_expiry_date;
407 # add enrollment date if it isn't already there
408 unless ( $data{'dateenrolled'} ) {
409 $data{'dateenrolled'} = output_pref( { dt => dt_from_string, dateonly => 1, dateformat => 'iso' } );
412 if ( C4::Context->preference("autoMemberNum") ) {
413 if ( not exists $data{cardnumber} or not defined $data{cardnumber} or $data{cardnumber} eq '' ) {
414 $data{cardnumber} = fixup_cardnumber( $data{cardnumber} );
418 $data{'privacy'} =
419 $category->default_privacy() eq 'default' ? 1
420 : $category->default_privacy() eq 'never' ? 2
421 : $category->default_privacy() eq 'forever' ? 0
422 : undef;
424 $data{'privacy_guarantor_checkouts'} = 0 unless defined( $data{'privacy_guarantor_checkouts'} );
426 # Make a copy of the plain text password for later use
427 my $plain_text_password = $data{'password'};
429 # create a disabled account if no password provided
430 $data{'password'} = ($data{'password'})? hash_password($data{'password'}) : '!';
432 # we don't want invalid dates in the db (mysql has a bad habit of inserting 0000-00-00
433 $data{'dateofbirth'} = undef if ( not $data{'dateofbirth'} );
434 $data{'debarred'} = undef if ( not $data{'debarred'} );
435 $data{'sms_provider_id'} = undef if ( not $data{'sms_provider_id'} );
436 $data{'guarantorid'} = undef if ( not $data{'guarantorid'} );
438 # get only the columns of Borrower
439 # FIXME Do we really need this check?
440 my @columns = $schema->source('Borrower')->columns;
441 my $new_member = { map { join(' ',@columns) =~ /$_/ ? ( $_ => $data{$_} ) : () } keys(%data) } ;
443 delete $new_member->{borrowernumber};
445 my $patron = Koha::Patron->new( $new_member )->store;
446 $data{borrowernumber} = $patron->borrowernumber;
448 # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a
449 # cronjob will use for syncing with NL
450 if ( exists $data{'borrowernumber'} && C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
451 Koha::Database->new->schema->resultset('BorrowerSync')->create({
452 'borrowernumber' => $data{'borrowernumber'},
453 'synctype' => 'norwegianpatrondb',
454 'sync' => 1,
455 'syncstatus' => 'new',
456 'hashed_pin' => Koha::NorwegianPatronDB::NLEncryptPIN( $plain_text_password ),
460 logaction("MEMBERS", "CREATE", $data{'borrowernumber'}, "") if C4::Context->preference("BorrowersLog");
462 $patron->add_enrolment_fee_if_needed;
464 return $data{borrowernumber};
467 =head2 Check_Userid
469 my $uniqueness = Check_Userid($userid,$borrowernumber);
471 $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 != '').
473 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 return :
476 0 for not unique (i.e. this $userid already exists)
477 1 for unique (i.e. this $userid does not exist, or this $userid/$borrowernumber combination already exists)
479 =cut
481 sub Check_Userid {
482 my ( $uid, $borrowernumber ) = @_;
484 return 0 unless ($uid); # userid is a unique column, we should assume NULL is not unique
486 return 0 if ( $uid eq C4::Context->config('user') );
488 my $rs = Koha::Database->new()->schema()->resultset('Borrower');
490 my $params;
491 $params->{userid} = $uid;
492 $params->{borrowernumber} = { '!=' => $borrowernumber } if ($borrowernumber);
494 my $count = $rs->count( $params );
496 return $count ? 0 : 1;
499 =head2 Generate_Userid
501 my $newuid = Generate_Userid($borrowernumber, $firstname, $surname);
503 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
505 $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 return :
508 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).
510 =cut
512 sub Generate_Userid {
513 my ($borrowernumber, $firstname, $surname) = @_;
514 my $newuid;
515 my $offset = 0;
516 #The script will "do" the following code and increment the $offset until Check_Userid = 1 (i.e. until $newuid comes back as unique)
517 do {
518 $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
519 $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
520 $newuid = lc(($firstname)? "$firstname.$surname" : $surname);
521 $newuid = unac_string('utf-8',$newuid);
522 $newuid .= $offset unless $offset == 0;
523 $offset++;
525 } while (!Check_Userid($newuid,$borrowernumber));
527 return $newuid;
530 =head2 fixup_cardnumber
532 Warning: The caller is responsible for locking the members table in write
533 mode, to avoid database corruption.
535 =cut
537 use vars qw( @weightings );
538 my @weightings = ( 8, 4, 6, 3, 5, 2, 1 );
540 sub fixup_cardnumber {
541 my ($cardnumber) = @_;
542 my $autonumber_members = C4::Context->boolean_preference('autoMemberNum') || 0;
544 # Find out whether member numbers should be generated
545 # automatically. Should be either "1" or something else.
546 # Defaults to "0", which is interpreted as "no".
548 # if ($cardnumber !~ /\S/ && $autonumber_members) {
549 ($autonumber_members) or return $cardnumber;
550 my $checkdigit = C4::Context->preference('checkdigit');
551 my $dbh = C4::Context->dbh;
552 if ( $checkdigit and $checkdigit eq 'katipo' ) {
554 # if checkdigit is selected, calculate katipo-style cardnumber.
555 # otherwise, just use the max()
556 # purpose: generate checksum'd member numbers.
557 # We'll assume we just got the max value of digits 2-8 of member #'s
558 # from the database and our job is to increment that by one,
559 # determine the 1st and 9th digits and return the full string.
560 my $sth = $dbh->prepare(
561 "select max(substring(borrowers.cardnumber,2,7)) as new_num from borrowers"
563 $sth->execute;
564 my $data = $sth->fetchrow_hashref;
565 $cardnumber = $data->{new_num};
566 if ( !$cardnumber ) { # If DB has no values,
567 $cardnumber = 1000000; # start at 1000000
568 } else {
569 $cardnumber += 1;
572 my $sum = 0;
573 for ( my $i = 0 ; $i < 8 ; $i += 1 ) {
574 # read weightings, left to right, 1 char at a time
575 my $temp1 = $weightings[$i];
577 # sequence left to right, 1 char at a time
578 my $temp2 = substr( $cardnumber, $i, 1 );
580 # mult each char 1-7 by its corresponding weighting
581 $sum += $temp1 * $temp2;
584 my $rem = ( $sum % 11 );
585 $rem = 'X' if $rem == 10;
587 return "V$cardnumber$rem";
588 } else {
590 my $sth = $dbh->prepare(
591 'SELECT MAX( CAST( cardnumber AS SIGNED ) ) FROM borrowers WHERE cardnumber REGEXP "^-?[0-9]+$"'
593 $sth->execute;
594 my ($result) = $sth->fetchrow;
595 return $result + 1;
597 return $cardnumber; # just here as a fallback/reminder
600 =head2 GetPendingIssues
602 my $issues = &GetPendingIssues(@borrowernumber);
604 Looks up what the patron with the given borrowernumber has borrowed.
606 C<&GetPendingIssues> returns a
607 reference-to-array where each element is a reference-to-hash; the
608 keys are the fields from the C<issues>, C<biblio>, and C<items> tables.
609 The keys include C<biblioitems> fields.
611 =cut
613 sub GetPendingIssues {
614 my @borrowernumbers = @_;
616 unless (@borrowernumbers ) { # return a ref_to_array
617 return \@borrowernumbers; # to not cause surprise to caller
620 # Borrowers part of the query
621 my $bquery = '';
622 for (my $i = 0; $i < @borrowernumbers; $i++) {
623 $bquery .= ' issues.borrowernumber = ?';
624 if ($i < $#borrowernumbers ) {
625 $bquery .= ' OR';
629 # FIXME: namespace collision: each table has "timestamp" fields. Which one is "timestamp" ?
630 # FIXME: circ/ciculation.pl tries to sort by timestamp!
631 # FIXME: namespace collision: other collisions possible.
632 # FIXME: most of this data isn't really being used by callers.
633 my $query =
634 "SELECT issues.*,
635 items.*,
636 biblio.*,
637 biblioitems.volume,
638 biblioitems.number,
639 biblioitems.itemtype,
640 biblioitems.isbn,
641 biblioitems.issn,
642 biblioitems.publicationyear,
643 biblioitems.publishercode,
644 biblioitems.volumedate,
645 biblioitems.volumedesc,
646 biblioitems.lccn,
647 biblioitems.url,
648 borrowers.firstname,
649 borrowers.surname,
650 borrowers.cardnumber,
651 issues.timestamp AS timestamp,
652 issues.renewals AS renewals,
653 issues.borrowernumber AS borrowernumber,
654 items.renewals AS totalrenewals
655 FROM issues
656 LEFT JOIN items ON items.itemnumber = issues.itemnumber
657 LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
658 LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
659 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
660 WHERE
661 $bquery
662 ORDER BY issues.issuedate"
665 my $sth = C4::Context->dbh->prepare($query);
666 $sth->execute(@borrowernumbers);
667 my $data = $sth->fetchall_arrayref({});
668 my $today = dt_from_string;
669 foreach (@{$data}) {
670 if ($_->{issuedate}) {
671 $_->{issuedate} = dt_from_string($_->{issuedate}, 'sql');
673 $_->{date_due_sql} = $_->{date_due};
674 # FIXME no need to have this value
675 $_->{date_due} or next;
676 $_->{date_due_sql} = $_->{date_due};
677 # FIXME no need to have this value
678 $_->{date_due} = dt_from_string($_->{date_due}, 'sql');
679 if ( DateTime->compare($_->{date_due}, $today) == -1 ) {
680 $_->{overdue} = 1;
683 return $data;
686 =head2 GetAllIssues
688 $issues = &GetAllIssues($borrowernumber, $sortkey, $limit);
690 Looks up what the patron with the given borrowernumber has borrowed,
691 and sorts the results.
693 C<$sortkey> is the name of a field on which to sort the results. This
694 should be the name of a field in the C<issues>, C<biblio>,
695 C<biblioitems>, or C<items> table in the Koha database.
697 C<$limit> is the maximum number of results to return.
699 C<&GetAllIssues> an arrayref, C<$issues>, of hashrefs, the keys of which
700 are the fields from the C<issues>, C<biblio>, C<biblioitems>, and
701 C<items> tables of the Koha database.
703 =cut
706 sub GetAllIssues {
707 my ( $borrowernumber, $order, $limit ) = @_;
709 return unless $borrowernumber;
710 $order = 'date_due desc' unless $order;
712 my $dbh = C4::Context->dbh;
713 my $query =
714 'SELECT *, issues.timestamp as issuestimestamp, issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
715 FROM issues
716 LEFT JOIN items on items.itemnumber=issues.itemnumber
717 LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
718 LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
719 WHERE borrowernumber=?
720 UNION ALL
721 SELECT *, old_issues.timestamp as issuestimestamp, old_issues.renewals AS renewals,items.renewals AS totalrenewals,items.timestamp AS itemstimestamp
722 FROM old_issues
723 LEFT JOIN items on items.itemnumber=old_issues.itemnumber
724 LEFT JOIN biblio ON items.biblionumber=biblio.biblionumber
725 LEFT JOIN biblioitems ON items.biblioitemnumber=biblioitems.biblioitemnumber
726 WHERE borrowernumber=? AND old_issues.itemnumber IS NOT NULL
727 order by ' . $order;
728 if ($limit) {
729 $query .= " limit $limit";
732 my $sth = $dbh->prepare($query);
733 $sth->execute( $borrowernumber, $borrowernumber );
734 return $sth->fetchall_arrayref( {} );
737 sub checkcardnumber {
738 my ( $cardnumber, $borrowernumber ) = @_;
740 # If cardnumber is null, we assume they're allowed.
741 return 0 unless defined $cardnumber;
743 my $dbh = C4::Context->dbh;
744 my $query = "SELECT * FROM borrowers WHERE cardnumber=?";
745 $query .= " AND borrowernumber <> ?" if ($borrowernumber);
746 my $sth = $dbh->prepare($query);
747 $sth->execute(
748 $cardnumber,
749 ( $borrowernumber ? $borrowernumber : () )
752 return 1 if $sth->fetchrow_hashref;
754 my ( $min_length, $max_length ) = get_cardnumber_length();
755 return 2
756 if length $cardnumber > $max_length
757 or length $cardnumber < $min_length;
759 return 0;
762 =head2 get_cardnumber_length
764 my ($min, $max) = C4::Members::get_cardnumber_length()
766 Returns the minimum and maximum length for patron cardnumbers as
767 determined by the CardnumberLength system preference, the
768 BorrowerMandatoryField system preference, and the width of the
769 database column.
771 =cut
773 sub get_cardnumber_length {
774 my $borrower = Koha::Schema->resultset('Borrower');
775 my $field_size = $borrower->result_source->column_info('cardnumber')->{size};
776 my ( $min, $max ) = ( 0, $field_size ); # borrowers.cardnumber is a nullable varchar(20)
777 $min = 1 if C4::Context->preference('BorrowerMandatoryField') =~ /cardnumber/;
778 if ( my $cardnumber_length = C4::Context->preference('CardnumberLength') ) {
779 # Is integer and length match
780 if ( $cardnumber_length =~ m|^\d+$| ) {
781 $min = $max = $cardnumber_length
782 if $cardnumber_length >= $min
783 and $cardnumber_length <= $max;
785 # Else assuming it is a range
786 elsif ( $cardnumber_length =~ m|(\d*),(\d*)| ) {
787 $min = $1 if $1 and $min < $1;
788 $max = $2 if $2 and $max > $2;
792 $min = $max if $min > $max;
793 return ( $min, $max );
796 =head2 GetBorrowersToExpunge
798 $borrowers = &GetBorrowersToExpunge(
799 not_borrowed_since => $not_borrowed_since,
800 expired_before => $expired_before,
801 category_code => $category_code,
802 patron_list_id => $patron_list_id,
803 branchcode => $branchcode
806 This function get all borrowers based on the given criteria.
808 =cut
810 sub GetBorrowersToExpunge {
812 my $params = shift;
813 my $filterdate = $params->{'not_borrowed_since'};
814 my $filterexpiry = $params->{'expired_before'};
815 my $filterlastseen = $params->{'last_seen'};
816 my $filtercategory = $params->{'category_code'};
817 my $filterbranch = $params->{'branchcode'} ||
818 ((C4::Context->preference('IndependentBranches')
819 && C4::Context->userenv
820 && !C4::Context->IsSuperLibrarian()
821 && C4::Context->userenv->{branch})
822 ? C4::Context->userenv->{branch}
823 : "");
824 my $filterpatronlist = $params->{'patron_list_id'};
826 my $dbh = C4::Context->dbh;
827 my $query = q|
828 SELECT *
829 FROM (
830 SELECT borrowers.borrowernumber,
831 MAX(old_issues.timestamp) AS latestissue,
832 MAX(issues.timestamp) AS currentissue
833 FROM borrowers
834 JOIN categories USING (categorycode)
835 LEFT JOIN (
836 SELECT guarantorid
837 FROM borrowers
838 WHERE guarantorid IS NOT NULL
839 AND guarantorid <> 0
840 ) as tmp ON borrowers.borrowernumber=tmp.guarantorid
841 LEFT JOIN old_issues USING (borrowernumber)
842 LEFT JOIN issues USING (borrowernumber)|;
843 if ( $filterpatronlist ){
844 $query .= q| LEFT JOIN patron_list_patrons USING (borrowernumber)|;
846 $query .= q| WHERE category_type <> 'S'
847 AND tmp.guarantorid IS NULL
849 my @query_params;
850 if ( $filterbranch && $filterbranch ne "" ) {
851 $query.= " AND borrowers.branchcode = ? ";
852 push( @query_params, $filterbranch );
854 if ( $filterexpiry ) {
855 $query .= " AND dateexpiry < ? ";
856 push( @query_params, $filterexpiry );
858 if ( $filterlastseen ) {
859 $query .= ' AND lastseen < ? ';
860 push @query_params, $filterlastseen;
862 if ( $filtercategory ) {
863 $query .= " AND categorycode = ? ";
864 push( @query_params, $filtercategory );
866 if ( $filterpatronlist ){
867 $query.=" AND patron_list_id = ? ";
868 push( @query_params, $filterpatronlist );
870 $query .= " GROUP BY borrowers.borrowernumber";
871 $query .= q|
872 ) xxx WHERE currentissue IS NULL|;
873 if ( $filterdate ) {
874 $query.=" AND ( latestissue < ? OR latestissue IS NULL ) ";
875 push @query_params,$filterdate;
878 warn $query if $debug;
880 my $sth = $dbh->prepare($query);
881 if (scalar(@query_params)>0){
882 $sth->execute(@query_params);
884 else {
885 $sth->execute;
888 my @results;
889 while ( my $data = $sth->fetchrow_hashref ) {
890 push @results, $data;
892 return \@results;
895 =head2 IssueSlip
897 IssueSlip($branchcode, $borrowernumber, $quickslip)
899 Returns letter hash ( see C4::Letters::GetPreparedLetter )
901 $quickslip is boolean, to indicate whether we want a quick slip
903 IssueSlip populates ISSUESLIP and ISSUEQSLIP, and will make the following expansions:
905 Both slips:
907 <<branches.*>>
908 <<borrowers.*>>
910 ISSUESLIP:
912 <checkedout>
913 <<biblio.*>>
914 <<items.*>>
915 <<biblioitems.*>>
916 <<issues.*>>
917 </checkedout>
919 <overdue>
920 <<biblio.*>>
921 <<items.*>>
922 <<biblioitems.*>>
923 <<issues.*>>
924 </overdue>
926 <news>
927 <<opac_news.*>>
928 </news>
930 ISSUEQSLIP:
932 <checkedout>
933 <<biblio.*>>
934 <<items.*>>
935 <<biblioitems.*>>
936 <<issues.*>>
937 </checkedout>
939 NOTE: Not all table fields are available, pleasee see GetPendingIssues for a list of available fields.
941 =cut
943 sub IssueSlip {
944 my ($branch, $borrowernumber, $quickslip) = @_;
946 # FIXME Check callers before removing this statement
947 #return unless $borrowernumber;
949 my $patron = Koha::Patrons->find( $borrowernumber );
950 return unless $patron;
952 my @issues = @{ GetPendingIssues($borrowernumber) };
954 for my $issue (@issues) {
955 $issue->{date_due} = $issue->{date_due_sql};
956 if ($quickslip) {
957 my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
958 if ( substr( $issue->{issuedate}, 0, 10 ) eq $today
959 or substr( $issue->{lastreneweddate}, 0, 10 ) eq $today ) {
960 $issue->{now} = 1;
965 # Sort on timestamp then on issuedate then on issue_id
966 # useful for tests and could be if modified in a batch
967 @issues = sort {
968 $b->{timestamp} <=> $a->{timestamp}
969 or $b->{issuedate} <=> $a->{issuedate}
970 or $b->{issue_id} <=> $a->{issue_id}
971 } @issues;
973 my ($letter_code, %repeat, %loops);
974 if ( $quickslip ) {
975 $letter_code = 'ISSUEQSLIP';
976 my @checkouts = map {
977 'biblio' => $_,
978 'items' => $_,
979 'biblioitems' => $_,
980 'issues' => $_,
981 }, grep { $_->{'now'} } @issues;
982 %repeat = (
983 checkedout => \@checkouts, # History syntax
985 %loops = (
986 issues => [ map { $_->{issues}{itemnumber} } @checkouts ], # TT syntax
989 else {
990 my @checkouts = map {
991 'biblio' => $_,
992 'items' => $_,
993 'biblioitems' => $_,
994 'issues' => $_,
995 }, grep { !$_->{'overdue'} } @issues;
996 my @overdues = map {
997 'biblio' => $_,
998 'items' => $_,
999 'biblioitems' => $_,
1000 'issues' => $_,
1001 }, grep { $_->{'overdue'} } @issues;
1002 my $news = GetNewsToDisplay( "slip", $branch );
1003 my @news = map {
1004 $_->{'timestamp'} = $_->{'newdate'};
1005 { opac_news => $_ }
1006 } @$news;
1007 $letter_code = 'ISSUESLIP';
1008 %repeat = (
1009 checkedout => \@checkouts,
1010 overdue => \@overdues,
1011 news => \@news,
1013 %loops = (
1014 issues => [ map { $_->{issues}{itemnumber} } @checkouts ],
1015 overdues => [ map { $_->{issues}{itemnumber} } @overdues ],
1016 opac_news => [ map { $_->{opac_news}{idnew} } @news ],
1020 return C4::Letters::GetPreparedLetter (
1021 module => 'circulation',
1022 letter_code => $letter_code,
1023 branchcode => $branch,
1024 lang => $patron->lang,
1025 tables => {
1026 'branches' => $branch,
1027 'borrowers' => $borrowernumber,
1029 repeat => \%repeat,
1030 loops => \%loops,
1034 =head2 AddMember_Auto
1036 =cut
1038 sub AddMember_Auto {
1039 my ( %borrower ) = @_;
1041 $borrower{'cardnumber'} ||= fixup_cardnumber();
1043 $borrower{'borrowernumber'} = AddMember(%borrower);
1045 return ( %borrower );
1048 =head2 AddMember_Opac
1050 =cut
1052 sub AddMember_Opac {
1053 my ( %borrower ) = @_;
1055 $borrower{'categorycode'} //= C4::Context->preference('PatronSelfRegistrationDefaultCategory');
1056 if (not defined $borrower{'password'}){
1057 my $sr = new String::Random;
1058 $sr->{'A'} = [ 'A'..'Z', 'a'..'z' ];
1059 my $password = $sr->randpattern("AAAAAAAAAA");
1060 $borrower{'password'} = $password;
1063 %borrower = AddMember_Auto(%borrower);
1065 return ( $borrower{'borrowernumber'}, $borrower{'password'} );
1068 =head2 DeleteExpiredOpacRegistrations
1070 Delete accounts that haven't been upgraded from the 'temporary' category
1071 Returns the number of removed patrons
1073 =cut
1075 sub DeleteExpiredOpacRegistrations {
1077 my $delay = C4::Context->preference('PatronSelfRegistrationExpireTemporaryAccountsDelay');
1078 my $category_code = C4::Context->preference('PatronSelfRegistrationDefaultCategory');
1080 return 0 if not $category_code or not defined $delay or $delay eq q||;
1082 my $query = qq|
1083 SELECT borrowernumber
1084 FROM borrowers
1085 WHERE categorycode = ? AND DATEDIFF( NOW(), dateenrolled ) > ? |;
1087 my $dbh = C4::Context->dbh;
1088 my $sth = $dbh->prepare($query);
1089 $sth->execute( $category_code, $delay );
1090 my $cnt=0;
1091 while ( my ($borrowernumber) = $sth->fetchrow_array() ) {
1092 Koha::Patrons->find($borrowernumber)->delete;
1093 $cnt++;
1095 return $cnt;
1098 =head2 DeleteUnverifiedOpacRegistrations
1100 Delete all unverified self registrations in borrower_modifications,
1101 older than the specified number of days.
1103 =cut
1105 sub DeleteUnverifiedOpacRegistrations {
1106 my ( $days ) = @_;
1107 my $dbh = C4::Context->dbh;
1108 my $sql=qq|
1109 DELETE FROM borrower_modifications
1110 WHERE borrowernumber = 0 AND DATEDIFF( NOW(), timestamp ) > ?|;
1111 my $cnt=$dbh->do($sql, undef, ($days) );
1112 return $cnt eq '0E0'? 0: $cnt;
1115 sub GetOverduesForPatron {
1116 my ( $borrowernumber ) = @_;
1118 my $sql = "
1119 SELECT *
1120 FROM issues, items, biblio, biblioitems
1121 WHERE items.itemnumber=issues.itemnumber
1122 AND biblio.biblionumber = items.biblionumber
1123 AND biblio.biblionumber = biblioitems.biblionumber
1124 AND issues.borrowernumber = ?
1125 AND date_due < NOW()
1128 my $sth = C4::Context->dbh->prepare( $sql );
1129 $sth->execute( $borrowernumber );
1131 return $sth->fetchall_arrayref({});
1134 END { } # module clean-up code here (global destructor)
1138 __END__
1140 =head1 AUTHOR
1142 Koha Team
1144 =cut